diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index beeb09d036..d968715b5a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,6 +44,7 @@ jobs: run: | dotnet-gitversion /updateprojectfiles dotnet build --no-incremental --nologo --force --configuration Release + dotnet test --configuration Release - name: Pack run: dotnet pack -c Release --include-symbols -p:Version='${{ steps.gitversion.outputs.SemVer }}' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e92d17f10b..42675db67a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,7 +90,13 @@ remote: Follow the template instructions found on Github. -## Terminal.Gui Coding Style +## Tenets for [gui-cs](www.github.com/gui-cs) Code Style (Unless you have better ones) + +* **Six-Year-Old Reading Level** - Our code style is biased towards code readability and away from terseness. This is *Systems Software* and needs to stand the test of time. Code should be structured and use variable names that make it readable by a 6-year-old, and comments in code are encouraged. +* **Consistency, Consistency, Consistency** - We adopt and document our standards for code style and then enforce them ruthlessly. For example, we require code reviews to pay attention to code style, not just functionality. +* **Don't be Weird** - Like all developers we have opinions, but our opinions on code style are tempered by existing standards. We are biased towards code style that used by Microsoft and other leading dotnet developers. For example, we choose 4 spaces for indentation instead of 8. +* **Set and Forget** - We embrace and encourage the use of technology that makes it easy for contributors to apply best-practice code-style, such as ReSharper. As we do so we are mindful that tools can cause hidden issues and merge hell. +* **Documentation is the Spec** - We care deeply about providing delightful developer documentation and are sticklers for grammar and clarity. If the code and the docs conflict, we are biased to believe what we wrote in the API documentation. This drives a virtuous cycle of clear thinking. **Terminal.Gui** uses a derivative of the [Microsoft C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions), with any deviations from those (somewhat older) conventions codified in the .editorconfig for the solution, as well as even more specific definitions in team-shared dotsettings files, used by ReSharper and Rider.\ Before you commit code, please run the formatting rules on **only the code file(s) you have modified**, in one of the following ways, in order of most preferred to least preferred: diff --git a/README.md b/README.md index 803d3e698a..5b793db2fe 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ dotnet run * [API Documentation](https://gui-cs.github.io/Terminal.GuiV2Docs/api/Terminal.Gui.html) * [Documentation Home](https://gui-cs.github.io/Terminal.GuiV2Docs) -_The Documentation matches the most recent Nuget release from the `v2_develop` branch. The documentation for v1 is here: ([![Version](https://img.shields.io/nuget/v/Terminal.Gui.svg)](https://www.nuget.org/packages/Terminal.Gui))_ +The above documentation matches the most recent Nuget release from the `v2_develop` branch. Get the [v1 documentation here](This is the v2 API documentation. For v1 go here: https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.html) See the [`Terminal.Gui/` README](https://github.com/gui-cs/Terminal.Gui/tree/master/Terminal.Gui) for an overview of how the library is structured. diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index e60ed61d2b..259af8fba9 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -1,45 +1,49 @@ #nullable enable -using System.Text.Json.Serialization; - namespace Terminal.Gui; public static partial class Application // Keyboard handling { + private static Key _nextTabGroupKey = Key.F6; // Resources/config.json overrrides private static Key _nextTabKey = Key.Tab; // Resources/config.json overrrides - /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - public static Key NextTabKey - { - get => _nextTabKey; - set - { - if (_nextTabKey != value) - { - ReplaceKey (_nextTabKey, value); - _nextTabKey = value; - } - } - } + private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrrides private static Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrrides - /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - public static Key PrevTabKey - { - get => _prevTabKey; - set - { - if (_prevTabKey != value) - { - ReplaceKey (_prevTabKey, value); - _prevTabKey = value; - } - } - } + private static Key _quitKey = Key.Esc; // Resources/config.json overrrides - private static Key _nextTabGroupKey = Key.F6; // Resources/config.json overrrides + static Application () { AddApplicationKeyBindings (); } + + /// Gets the key bindings for this view. + public static KeyBindings KeyBindings { get; internal set; } = new (); + + /// + /// Event fired when the user presses a key. Fired by . + /// + /// Set to to indicate the key was handled and to prevent + /// additional processing. + /// + /// + /// + /// All drivers support firing the event. Some drivers (Curses) do not support firing the + /// and events. + /// Fired after and before . + /// + public static event EventHandler? KeyDown; + + /// + /// Event fired when the user releases a key. Fired by . + /// + /// Set to to indicate the key was handled and to prevent + /// additional processing. + /// + /// + /// + /// All drivers support firing the event. Some drivers (Curses) do not support firing the + /// and events. + /// Fired after . + /// + public static event EventHandler? KeyUp; /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] @@ -56,71 +60,21 @@ public static Key NextTabGroupKey } } - private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrrides - - /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - public static Key PrevTabGroupKey - { - get => _prevTabGroupKey; - set - { - if (_prevTabGroupKey != value) - { - ReplaceKey (_prevTabGroupKey, value); - _prevTabGroupKey = value; - } - } - } - - private static Key _quitKey = Key.Esc; // Resources/config.json overrrides - - /// Gets or sets the key to quit the application. + /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - public static Key QuitKey + public static Key NextTabKey { - get => _quitKey; + get => _nextTabKey; set { - if (_quitKey != value) + if (_nextTabKey != value) { - ReplaceKey (_quitKey, value); - _quitKey = value; + ReplaceKey (_nextTabKey, value); + _nextTabKey = value; } } } - private static void ReplaceKey (Key oldKey, Key newKey) - { - if (KeyBindings.Bindings.Count == 0) - { - return; - } - - if (newKey == Key.Empty) - { - KeyBindings.Remove (oldKey); - } - else - { - KeyBindings.ReplaceKey (oldKey, newKey); - } - } - - /// - /// Event fired when the user presses a key. Fired by . - /// - /// Set to to indicate the key was handled and to prevent - /// additional processing. - /// - /// - /// - /// All drivers support firing the event. Some drivers (Curses) do not support firing the - /// and events. - /// Fired after and before . - /// - public static event EventHandler? KeyDown; - /// /// Called by the when the user presses a key. Fires the event /// then calls on all top level views. Called after and @@ -190,24 +144,7 @@ public static bool OnKeyDown (Key keyEvent) foreach (Command command in appBinding.Commands) { - if (!CommandImplementations!.ContainsKey (command)) - { - throw new NotSupportedException ( - @$"A KeyBinding was set up for the command {command} ({keyEvent}) but that command is not supported by Application." - ); - } - - if (CommandImplementations.TryGetValue (command, out Func? implementation)) - { - var context = new CommandContext (command, keyEvent, appBinding); // Create the context here - toReturn = implementation (context); - } - - // if ever see a true then that's what we will return - if (toReturn ?? false) - { - toReturn = true; - } + toReturn = InvokeCommand (command, keyEvent, appBinding); } return toReturn ?? true; @@ -218,18 +155,30 @@ public static bool OnKeyDown (Key keyEvent) } /// - /// Event fired when the user releases a key. Fired by . - /// - /// Set to to indicate the key was handled and to prevent - /// additional processing. - /// + /// INTENRAL method to invoke one of the commands in /// - /// - /// All drivers support firing the event. Some drivers (Curses) do not support firing the - /// and events. - /// Fired after . - /// - public static event EventHandler? KeyUp; + /// + /// + /// + /// + /// + private static bool? InvokeCommand (Command command, Key keyEvent, KeyBinding appBinding) + { + if (!CommandImplementations!.ContainsKey (command)) + { + throw new NotSupportedException ( + @$"A KeyBinding was set up for the command {command} ({keyEvent}) but that command is not supported by Application." + ); + } + + if (CommandImplementations.TryGetValue (command, out Func? implementation)) + { + var context = new CommandContext (command, keyEvent, appBinding); // Create the context here + return implementation (context); + } + + return false; + } /// /// Called by the when the user releases a key. Fires the event @@ -268,33 +217,50 @@ public static bool OnKeyUp (Key a) return false; } - /// Gets the key bindings for this view. - public static KeyBindings KeyBindings { get; internal set; } = new (); - - /// - /// Commands for Application. - /// - private static Dictionary>? CommandImplementations { get; set; } + /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static Key PrevTabGroupKey + { + get => _prevTabGroupKey; + set + { + if (_prevTabGroupKey != value) + { + ReplaceKey (_prevTabGroupKey, value); + _prevTabGroupKey = value; + } + } + } - /// - /// - /// Sets the function that will be invoked for a . - /// - /// - /// If AddCommand has already been called for will - /// replace the old one. - /// - /// - /// - /// - /// 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 (); } + /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static Key PrevTabKey + { + get => _prevTabKey; + set + { + if (_prevTabKey != value) + { + ReplaceKey (_prevTabKey, value); + _prevTabKey = value; + } + } + } - static Application () { AddApplicationKeyBindings (); } + /// Gets or sets the key to quit the application. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static Key QuitKey + { + get => _quitKey; + set + { + if (_quitKey != value) + { + ReplaceKey (_quitKey, value); + _quitKey = value; + } + } + } internal static void AddApplicationKeyBindings () { @@ -303,7 +269,7 @@ internal static void AddApplicationKeyBindings () // Things this view knows how to do AddCommand ( Command.QuitToplevel, // TODO: IRunnable: Rename to Command.Quit to make more generic. - () => + static () => { if (ApplicationOverlapped.OverlappedTop is { }) { @@ -320,7 +286,7 @@ internal static void AddApplicationKeyBindings () AddCommand ( Command.Suspend, - () => + static () => { Driver?.Suspend (); @@ -330,47 +296,47 @@ internal static void AddApplicationKeyBindings () AddCommand ( Command.NextView, - () => - { - ApplicationNavigation.MoveNextView (); - - return true; - } - ); + static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)); AddCommand ( Command.PreviousView, - () => - { - ApplicationNavigation.MovePreviousView (); - - return true; - } - ); + static () => Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop)); AddCommand ( Command.NextViewOrTop, - () => + static () => { - ApplicationNavigation.MoveNextViewOrTop (); + // TODO: This OverlapppedTop tomfoolery goes away in addressing #2491 + if (ApplicationOverlapped.OverlappedTop is { }) + { + ApplicationOverlapped.OverlappedMoveNext (); - return true; + return true; + } + + return Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); } ); AddCommand ( Command.PreviousViewOrTop, - () => + static () => { - ApplicationNavigation.MovePreviousViewOrTop (); + // TODO: This OverlapppedTop tomfoolery goes away in addressing #2491 + if (ApplicationOverlapped.OverlappedTop is { }) + { + ApplicationOverlapped.OverlappedMovePrevious (); - return true; + return true; + } + + return Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup); } ); AddCommand ( Command.Refresh, - () => + static () => { Refresh (); @@ -396,8 +362,8 @@ internal static void AddApplicationKeyBindings () KeyBindings.Add (NextTabKey, KeyBindingScope.Application, Command.NextView); KeyBindings.Add (PrevTabKey, KeyBindingScope.Application, Command.PreviousView); - KeyBindings.Add (NextTabGroupKey, KeyBindingScope.Application, Command.NextViewOrTop); // Needed on Unix - KeyBindings.Add (PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousViewOrTop); // Needed on Unix + KeyBindings.Add (NextTabGroupKey, KeyBindingScope.Application, Command.NextViewOrTop); + KeyBindings.Add (PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousViewOrTop); // TODO: Refresh Key should be configurable KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh); @@ -407,13 +373,6 @@ internal static void AddApplicationKeyBindings () { KeyBindings.Add (Key.Z.WithCtrl, KeyBindingScope.Application, Command.Suspend); } - -#if UNIX_KEY_BINDINGS - KeyBindings.Add (Key.L.WithCtrl, Command.Refresh); // Unix - KeyBindings.Add (Key.F.WithCtrl, Command.NextView); // Unix - KeyBindings.Add (Key.I.WithCtrl, Command.NextView); // Unix - KeyBindings.Add (Key.B.WithCtrl, Command.PreviousView); // Unix -#endif } /// @@ -432,4 +391,44 @@ internal static List GetViewKeyBindings () .Distinct () .ToList (); } + + /// + /// + /// Sets the function that will be invoked for a . + /// + /// + /// If AddCommand has already been called for will + /// replace the old one. + /// + /// + /// + /// + /// 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 (); } + + /// + /// Commands for Application. + /// + private static Dictionary>? CommandImplementations { get; set; } + + private static void ReplaceKey (Key oldKey, Key newKey) + { + if (KeyBindings.Bindings.Count == 0) + { + return; + } + + if (newKey == Key.Empty) + { + KeyBindings.Remove (oldKey); + } + else + { + KeyBindings.ReplaceKey (oldKey, newKey); + } + } } diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index b884eaefb8..2b16686d3d 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -143,6 +143,12 @@ internal static void OnMouseEvent (MouseEvent mouseEvent) if (view is { }) { +#if DEBUG_IDISPOSABLE + if (view.WasDisposed) + { + throw new ObjectDisposedException (view.GetType ().FullName); + } +#endif mouseEvent.View = view; } @@ -155,6 +161,13 @@ internal static void OnMouseEvent (MouseEvent mouseEvent) if (MouseGrabView is { }) { + +#if DEBUG_IDISPOSABLE + if (MouseGrabView.WasDisposed) + { + throw new ObjectDisposedException (MouseGrabView.GetType ().FullName); + } +#endif // If the mouse is grabbed, send the event to the view that grabbed it. // The coordinates are relative to the Bounds of the view that grabbed the mouse. Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.Position); @@ -174,8 +187,15 @@ internal static void OnMouseEvent (MouseEvent mouseEvent) } //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); - if (MouseGrabView.NewMouseEvent (viewRelativeMouseEvent) is true) + if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true) + { + return; + } + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (MouseGrabView is null && view is Adornment) { + // The view that grabbed the mouse has been disposed return; } } diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index e40a26750d..5099788fd8 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -99,7 +99,7 @@ public static RunState Begin (Toplevel toplevel) else if (ApplicationOverlapped.OverlappedTop is { } && toplevel != Top && TopLevels.Contains (Top!)) { // BUGBUG: Don't call OnLeave/OnEnter directly! Set HasFocus to false and let the system handle it. - Top!.OnLeave (toplevel); + //Top!.OnLeave (toplevel); } // BUGBUG: We should not depend on `Id` internally. @@ -186,7 +186,14 @@ public static RunState Begin (Toplevel toplevel) toplevel.LayoutSubviews (); toplevel.PositionToplevels (); - toplevel.FocusFirst (null); + + // TODO: Should this use FindDeepestFocusableView instead? + // Try to set initial focus to any TabStop + if (!toplevel.HasFocus) + { + toplevel.SetFocus (); + } + ApplicationOverlapped.BringOverlappedTopToFront (); if (refreshDriver) @@ -858,7 +865,6 @@ public static void End (RunState runState) if (Current is { HasFocus: false }) { Current.SetFocus (); - Current.RestoreFocus (); } } diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index bcb8d5b3bd..d37ba3513f 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -138,6 +138,8 @@ internal static List GetSupportedCultures () // starts running and after Shutdown returns. internal static void ResetState (bool ignoreDisposed = false) { + Application.Navigation = new ApplicationNavigation (); + // Shutdown is the bookend for Init. As such it needs to clean up all resources // Init created. Apps that do any threading will need to code defensively for this. // e.g. see Issue #537 diff --git a/Terminal.Gui/Application/ApplicationNavigation.cs b/Terminal.Gui/Application/ApplicationNavigation.cs index fe9c66b3b0..48aacb0b56 100644 --- a/Terminal.Gui/Application/ApplicationNavigation.cs +++ b/Terminal.Gui/Application/ApplicationNavigation.cs @@ -7,7 +7,6 @@ namespace Terminal.Gui; /// public class ApplicationNavigation { - /// /// Initializes a new instance of the class. /// @@ -16,38 +15,17 @@ public ApplicationNavigation () // TODO: Move navigation key bindings here from AddApplicationKeyBindings } - private View? _focused = null; - - /// - /// Gets the most focused in the application, if there is one. - /// - public View? GetFocused () { return _focused; } - - /// - /// INTERNAL method to record the most focused in the application. - /// - /// - /// Raises . - /// - internal void SetFocused (View? value) - { - if (_focused == value) - { - return; - } - - _focused = value; - - FocusedChanged?.Invoke (null, EventArgs.Empty); - - return; - } + private View? _focused; /// /// Raised when the most focused in the application has changed. /// public event EventHandler? FocusedChanged; + /// + /// Gets the most focused in the application, if there is one. + /// + public View? GetFocused () { return _focused; } /// /// Gets whether is in the Subview hierarchy of . @@ -55,14 +33,14 @@ internal void SetFocused (View? value) /// /// /// - public static bool IsInHierarchy (View start, View? view) + public static bool IsInHierarchy (View? start, View? view) { if (view is null) { return false; } - if (view == start) + if (view == start || start is null) { return true; } @@ -74,7 +52,8 @@ public static bool IsInHierarchy (View start, View? view) return true; } - var found = IsInHierarchy (subView, view); + bool found = IsInHierarchy (subView, view); + if (found) { return found; @@ -84,146 +63,41 @@ public static bool IsInHierarchy (View start, View? view) return false; } - - /// - /// 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 method to record the most focused in the application. /// - internal static void MoveNextView () + /// + /// Raises . + /// + internal void SetFocused (View? value) { - 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 + if (_focused == value) { - ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward); + return; } - } - - /// - /// 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); - //} + _focused = value; - //top.SetNeedsDisplay (); - ApplicationOverlapped.BringOverlappedTopToFront (); - } - else - { - ApplicationOverlapped.OverlappedMoveNext (); - } + FocusedChanged?.Invoke (null, EventArgs.Empty); } - // 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. + /// Advances the focus to the next or previous view in the focus chain, based on + /// . /// - 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 there is no next/previous view, the focus remains on the current view. + /// + /// + /// The direction to advance. + /// The tab behavior. + /// + /// if focus was changed to another subview (or stayed on this one), + /// otherwise. + /// + public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior) { - 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 (); - } + return Application.Current is { } && Application.Current.AdvanceFocus (direction, behavior); } } diff --git a/Terminal.Gui/Application/ApplicationOverlapped.cs b/Terminal.Gui/Application/ApplicationOverlapped.cs index 14a4163eae..328897580c 100644 --- a/Terminal.Gui/Application/ApplicationOverlapped.cs +++ b/Terminal.Gui/Application/ApplicationOverlapped.cs @@ -79,7 +79,7 @@ public static void BringOverlappedTopToFront () if (top is Toplevel && Application.Top?.Subviews.Count > 1 && Application.Top.Subviews [^1] != top) { - Application.Top.BringSubviewToFront (top); + Application.Top.MoveSubviewToStart (top); } } @@ -96,12 +96,12 @@ public static void BringOverlappedTopToFront () foreach (Toplevel top in OverlappedChildren) { - if (type is { } && top.GetType () == type && exclude?.Contains (top.Data.ToString ()) == false) + if (type is { } && top.GetType () == type && exclude?.Contains (top.Data?.ToString ()) == false) { return top; } - if ((type is { } && top.GetType () != type) || exclude?.Contains (top.Data.ToString ()) == true) + if ((type is { } && top.GetType () != type) || exclude?.Contains (top.Data?.ToString ()) == true) { continue; } diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index fdd2c56391..ee4bba220c 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -296,6 +296,7 @@ internal void Stop () /// Invoked when a new timeout is added. To be used in the case when /// is . /// + [CanBeNull] internal event EventHandler TimeoutAdded; /// Wakes up the that might be waiting on input. diff --git a/Terminal.Gui/Input/Responder.cs b/Terminal.Gui/Input/Responder.cs index 44ed5a2a8c..43cc082403 100644 --- a/Terminal.Gui/Input/Responder.cs +++ b/Terminal.Gui/Input/Responder.cs @@ -26,6 +26,7 @@ public void Dispose () } /// Event raised when has been called to signal that this object is being disposed. + [CanBeNull] public event EventHandler Disposing; /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. diff --git a/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs b/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs index 7e6ff1e558..39d725ca4b 100644 --- a/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs +++ b/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs @@ -36,17 +36,14 @@ public abstract class AutocompleteBase : IAutocomplete /// public abstract ColorScheme ColorScheme { get; set; } - // TODO: Update to use Key instead of KeyCode /// - public virtual KeyCode SelectionKey { get; set; } = KeyCode.Enter; + public virtual Key SelectionKey { get; set; } = Key.Enter; - // TODO: Update to use Key instead of KeyCode /// - public virtual KeyCode CloseKey { get; set; } = KeyCode.Esc; + public virtual Key CloseKey { get; set; } = Key.Esc; - // TODO: Update to use Key instead of KeyCode /// - public virtual KeyCode Reopen { get; set; } = (KeyCode)Key.Space.WithCtrl.WithAlt; + public virtual Key Reopen { get; set; } = Key.Space.WithCtrl.WithAlt; /// public virtual AutocompleteContext Context { get; set; } diff --git a/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs index 9eac868768..6305386bdb 100644 --- a/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs @@ -8,9 +8,11 @@ namespace Terminal.Gui; /// public interface IAutocomplete { - // TODO: Update to use Key instead of KeyCode + /// Clears + void ClearSuggestions (); + /// The key that the user can press to close the currently popped autocomplete menu - KeyCode CloseKey { get; set; } + Key CloseKey { get; set; } /// /// The colors to use to render the overlay. Accessing this property before the Application has been initialized @@ -21,6 +23,12 @@ public interface IAutocomplete /// The context used by the autocomplete menu. AutocompleteContext Context { get; set; } + /// + /// Populates with all proposed by + /// at the given (cursor position) + /// + void GenerateSuggestions (AutocompleteContext context); + /// The host control that will use autocomplete. View HostControl { get; set; } @@ -30,19 +38,38 @@ public interface IAutocomplete /// The maximum width of the autocomplete dropdown int MaxWidth { get; set; } + /// + /// Handle mouse events before e.g. to make mouse events like report/click apply to the + /// autocomplete control instead of changing the cursor position in the underlying text view. + /// + /// The mouse event. + /// If was called from the popup or from the host. + /// trueif the mouse can be handled falseotherwise. + bool OnMouseEvent (MouseEvent me, bool fromHost = false); + /// Gets or sets where the popup will be displayed. bool PopupInsideContainer { get; set; } - // TODO: Update to use Key instead of KeyCode + /// + /// Handle key events before e.g. to make key events like up/down apply to the + /// autocomplete control instead of changing the cursor position in the underlying text view. + /// + /// The key event. + /// trueif the key can be handled falseotherwise. + bool ProcessKey (Key a); + + /// Renders the autocomplete dialog inside the given at the given point. + /// + void RenderOverlay (Point renderAt); + /// The key that the user can press to reopen the currently popped autocomplete menu - KeyCode Reopen { get; set; } + Key Reopen { get; set; } /// The currently selected index into that the user has highlighted int SelectedIdx { get; set; } - // TODO: Update to use Key instead of KeyCode /// The key that the user must press to accept the currently selected autocomplete suggestion - KeyCode SelectionKey { get; set; } + Key SelectionKey { get; set; } /// /// Gets or Sets the class responsible for generating based on a given @@ -55,34 +82,4 @@ public interface IAutocomplete /// True if the autocomplete should be considered open and visible bool Visible { get; set; } - - /// Clears - void ClearSuggestions (); - - /// - /// Populates with all proposed by - /// at the given (cursor position) - /// - void GenerateSuggestions (AutocompleteContext context); - - /// - /// Handle mouse events before e.g. to make mouse events like report/click apply to the - /// autocomplete control instead of changing the cursor position in the underlying text view. - /// - /// The mouse event. - /// If was called from the popup or from the host. - /// trueif the mouse can be handled falseotherwise. - bool OnMouseEvent (MouseEvent me, bool fromHost = false); - - /// - /// Handle key events before e.g. to make key events like up/down apply to the - /// autocomplete control instead of changing the cursor position in the underlying text view. - /// - /// The key event. - /// trueif the key can be handled falseotherwise. - bool ProcessKey (Key a); - - /// Renders the autocomplete dialog inside the given at the given point. - /// - void RenderOverlay (Point renderAt); } diff --git a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs index 86fc949ecf..622e6620b9 100644 --- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs +++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs @@ -5,16 +5,15 @@ public abstract partial class PopupAutocomplete { private sealed class Popup : View { - private readonly PopupAutocomplete _autoComplete; - public Popup (PopupAutocomplete autoComplete) { - this._autoComplete = autoComplete; + _autoComplete = autoComplete; CanFocus = true; + TabStop = TabBehavior.NoStop; WantMousePositionReports = true; } - protected internal override bool OnMouseEvent (MouseEvent mouseEvent) { return _autoComplete.OnMouseEvent (mouseEvent); } + private readonly PopupAutocomplete _autoComplete; public override void OnDrawContent (Rectangle viewport) { @@ -25,5 +24,7 @@ public override void OnDrawContent (Rectangle viewport) _autoComplete.RenderOverlay (_autoComplete.LastPopupPos.Value); } + + protected internal override bool OnMouseEvent (MouseEvent mouseEvent) { return _autoComplete.OnMouseEvent (mouseEvent); } } } diff --git a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs index 4dfeb8a951..c17695ee7e 100644 --- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace Terminal.Gui; /// @@ -6,11 +8,12 @@ namespace Terminal.Gui; /// public abstract partial class PopupAutocomplete : AutocompleteBase { - private bool closed; - private ColorScheme colorScheme; - private View hostControl; - private View top, popup; - private int toRenderLength; + private bool _closed; + private ColorScheme _colorScheme; + private View _hostControl; + private View _top; // The _hostControl's SuperView + private View _popup; + private int _toRenderLength; /// Creates a new instance of the class. public PopupAutocomplete () { PopupInsideContainer = true; } @@ -23,43 +26,59 @@ public override ColorScheme ColorScheme { get { - if (colorScheme is null) + if (_colorScheme is null) { - colorScheme = Colors.ColorSchemes ["Menu"]; + _colorScheme = Colors.ColorSchemes ["Menu"]; } - return colorScheme; + return _colorScheme; } - set => colorScheme = value; + set => _colorScheme = value; } /// The host control to handle. public override View HostControl { - get => hostControl; + get => _hostControl; set { - hostControl = value; - top = hostControl.SuperView; + if (value == _hostControl) + { + return; + } + + _hostControl = value; + + if (_hostControl is null) + { + RemovePopupFromTop(); + _top.Removed -= _top_Removed; + _top = null; + + return; + } - if (top is { }) + _top = _hostControl.SuperView; + + if (_top is { }) { - top.DrawContent += Top_DrawContent; - top.DrawContentComplete += Top_DrawContentComplete; - top.Removed += Top_Removed; + if (_top.IsInitialized) + { + AddPopupToTop (); + } + else + { + _top.Initialized += _top_Initialized; + } + _top.Removed += _top_Removed; } } } - /// - /// When more suggestions are available than can be rendered the user can scroll down the dropdown list. This - /// indicates how far down they have gone - /// - public virtual int ScrollOffset { get; set; } - - #nullable enable - private Point? LastPopupPos { get; set; } - #nullable restore + private void _top_Added (object sender, SuperViewChangedEventArgs e) + { + throw new NotImplementedException (); + } /// public override void EnsureSelectedIdxIsValid () @@ -73,7 +92,7 @@ public override void EnsureSelectedIdxIsValid () } // if user moved selection down past bottom of current scroll window - while (toRenderLength > 0 && SelectedIdx >= ScrollOffset + toRenderLength) + while (_toRenderLength > 0 && SelectedIdx >= ScrollOffset + _toRenderLength) { ScrollOffset++; } @@ -119,7 +138,7 @@ public override bool OnMouseEvent (MouseEvent me, bool fromHost = false) if (Visible && HostControl is { }) { Visible = false; - closed = false; + _closed = false; } HostControl?.SetNeedsDisplay (); @@ -127,9 +146,10 @@ public override bool OnMouseEvent (MouseEvent me, bool fromHost = false) return false; } - if (popup is null || Suggestions.Count == 0) + if (_popup is null || Suggestions.Count == 0) { - ManipulatePopup (); + //AddPopupToTop (); + //Debug.Fail ("popup is null"); return false; } @@ -176,8 +196,7 @@ public override bool ProcessKey (Key key) if (SuggestionGenerator.IsWordChar ((Rune)key)) { Visible = true; - ManipulatePopup (); - closed = false; + _closed = false; return false; } @@ -189,11 +208,11 @@ public override bool ProcessKey (Key key) return ReopenSuggestions (); } - if (closed || Suggestions.Count == 0) + if (_closed || Suggestions.Count == 0) { Visible = false; - if (!closed) + if (!_closed) { Close (); } @@ -249,7 +268,7 @@ public override void RenderOverlay (Point renderAt) { if (!Context.Canceled && Suggestions.Count > 0 && !Visible && HostControl?.HasFocus == true) { - ProcessKey (new Key (Suggestions [0].Title [0])); + ProcessKey (new (Suggestions [0].Title [0])); } else if (!Visible || HostControl?.HasFocus == false || Suggestions.Count == 0) { @@ -287,13 +306,13 @@ public override void RenderOverlay (Point renderAt) else { // don't overspill vertically - height = Math.Min (Math.Min (top.Viewport.Height - HostControl.Frame.Bottom, MaxHeight), Suggestions.Count); + height = Math.Min (Math.Min (_top.Viewport.Height - HostControl.Frame.Bottom, MaxHeight), Suggestions.Count); // There is no space below, lets see if can popup on top - if (height < Suggestions.Count && HostControl.Frame.Y - top.Frame.Y >= height) + if (height < Suggestions.Count && HostControl.Frame.Y - _top.Frame.Y >= height) { // Verifies that the upper limit available is greater than the lower limit - if (HostControl.Frame.Y > top.Viewport.Height - HostControl.Frame.Y) + if (HostControl.Frame.Y > _top.Viewport.Height - HostControl.Frame.Y) { renderAt.Y = Math.Max (HostControl.Frame.Y - Math.Min (Suggestions.Count, MaxHeight), 0); height = Math.Min (Math.Min (Suggestions.Count, MaxHeight), HostControl.Frame.Y); @@ -311,7 +330,7 @@ public override void RenderOverlay (Point renderAt) } Suggestion [] toRender = Suggestions.Skip (ScrollOffset).Take (height).ToArray (); - toRenderLength = toRender.Length; + _toRenderLength = toRender.Length; if (toRender.Length == 0) { @@ -340,37 +359,37 @@ public override void RenderOverlay (Point renderAt) else { // don't overspill horizontally, let's see if it can be displayed on the left - if (width > top.Viewport.Width - (renderAt.X + HostControl.Frame.X)) + if (width > _top.Viewport.Width - (renderAt.X + HostControl.Frame.X)) { // Verifies that the left limit available is greater than the right limit - if (renderAt.X + HostControl.Frame.X > top.Viewport.Width - (renderAt.X + HostControl.Frame.X)) + if (renderAt.X + HostControl.Frame.X > _top.Viewport.Width - (renderAt.X + HostControl.Frame.X)) { renderAt.X -= Math.Min (width, LastPopupPos.Value.X); width = Math.Min (width, LastPopupPos.Value.X); } else { - width = Math.Min (width, top.Viewport.Width - renderAt.X); + width = Math.Min (width, _top.Viewport.Width - renderAt.X); } } } if (PopupInsideContainer) { - popup.Frame = new ( + _popup.Frame = new ( new (HostControl.Frame.X + renderAt.X, HostControl.Frame.Y + renderAt.Y), new (width, height) ); } else { - popup.Frame = new ( + _popup.Frame = new ( renderAt with { X = HostControl.Frame.X + renderAt.X }, new (width, height) ); } - popup.Move (0, 0); + _popup.Move (0, 0); for (var i = 0; i < toRender.Length; i++) { @@ -383,7 +402,7 @@ public override void RenderOverlay (Point renderAt) Application.Driver?.SetAttribute (ColorScheme.Normal); } - popup.Move (0, i); + _popup.Move (0, i); string text = TextFormatter.ClipOrPad (toRender [i].Title, width); @@ -391,6 +410,12 @@ public override void RenderOverlay (Point renderAt) } } + /// + /// When more suggestions are available than can be rendered the user can scroll down the dropdown list. This + /// indicates how far down they have gone + /// + public virtual int ScrollOffset { get; set; } + /// /// Closes the Autocomplete context menu if it is showing and /// @@ -398,9 +423,9 @@ protected void Close () { ClearSuggestions (); Visible = false; - closed = true; + _closed = true; HostControl?.SetNeedsDisplay (); - ManipulatePopup (); + //RemovePopupFromTop (); } /// Deletes the text backwards before insert the selected text in the . @@ -486,7 +511,7 @@ protected bool ReopenSuggestions () if (Suggestions.Count > 0) { Visible = true; - closed = false; + _closed = false; HostControl?.SetNeedsDisplay (); return true; @@ -516,42 +541,45 @@ protected bool Select () /// protected abstract void SetCursorPosition (int column); - private void ManipulatePopup () - { - if (Visible && popup is null) - { - popup = new Popup (this) { Frame = Rectangle.Empty }; - top?.Add (popup); - } +#nullable enable + private Point? LastPopupPos { get; set; } +#nullable restore - if (!Visible && popup is { }) + private void AddPopupToTop () + { + if (_popup is null) { - top?.Remove (popup); - popup.Dispose (); - popup = null; + _popup = new Popup (this) + { + CanFocus = false + }; + _top?.Add (_popup); } } - private void Top_DrawContent (object sender, DrawEventArgs e) + private void RemovePopupFromTop () { - if (!closed) + if (_popup is { } && _top.Subviews.Contains (_popup)) { - ReopenSuggestions (); - } + _top?.Remove (_popup); + _popup.Dispose (); + _popup = null; - ManipulatePopup (); + } + } - if (Visible) + private void _top_Initialized (object sender, EventArgs e) + { + if (_top is null) { - top.BringSubviewToFront (popup); + _top = sender as View; } + AddPopupToTop (); } - private void Top_DrawContentComplete (object sender, DrawEventArgs e) { ManipulatePopup (); } - - private void Top_Removed (object sender, SuperViewChangedEventArgs e) + private void _top_Removed (object sender, SuperViewChangedEventArgs e) { Visible = false; - ManipulatePopup (); + RemovePopupFromTop (); } } diff --git a/Terminal.Gui/Text/CollectionNavigatorBase.cs b/Terminal.Gui/Text/CollectionNavigatorBase.cs index df58dfbc45..4caa219cd9 100644 --- a/Terminal.Gui/Text/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Text/CollectionNavigatorBase.cs @@ -147,6 +147,7 @@ public static bool IsCompatibleKey (Key a) public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) { SearchStringChanged?.Invoke (this, e); } /// This event is invoked when changes. Useful for debugging. + [CanBeNull] public event EventHandler SearchStringChanged; /// Returns the collection being navigated element at . diff --git a/Terminal.Gui/View/Adornment/Adornment.cs b/Terminal.Gui/View/Adornment/Adornment.cs index e7cff5e8b4..6c575ad99e 100644 --- a/Terminal.Gui/View/Adornment/Adornment.cs +++ b/Terminal.Gui/View/Adornment/Adornment.cs @@ -86,7 +86,7 @@ public void OnThicknessChanged () /// Adornments cannot be used as sub-views (see ); setting this property will throw /// . /// - public override View SuperView + public override View? SuperView { get => null!; set => throw new InvalidOperationException (@"Adornments can not be Subviews or have SuperViews. Use Parent instead."); diff --git a/Terminal.Gui/View/Adornment/ShadowView.cs b/Terminal.Gui/View/Adornment/ShadowView.cs index 1ca027ade9..c9893c65ca 100644 --- a/Terminal.Gui/View/Adornment/ShadowView.cs +++ b/Terminal.Gui/View/Adornment/ShadowView.cs @@ -109,7 +109,7 @@ private void DrawHorizontalShadowTransparent (Rectangle viewport) Rectangle screen = ViewportToScreen (viewport); // Fill the rest of the rectangle - note we skip the last since vertical will draw it - for (int i = screen.X; i < screen.X + screen.Width - 1; i++) + for (int i = screen.X + 1; i < screen.X + screen.Width - 1; i++) { Driver.Move (i, screen.Y); diff --git a/Terminal.Gui/View/Navigation/FocusEventArgs.cs b/Terminal.Gui/View/Navigation/FocusEventArgs.cs index 6d8d282673..63c38bbe29 100644 --- a/Terminal.Gui/View/Navigation/FocusEventArgs.cs +++ b/Terminal.Gui/View/Navigation/FocusEventArgs.cs @@ -1,27 +1,23 @@ namespace Terminal.Gui; -/// Defines the event arguments for -public class FocusEventArgs : EventArgs +/// The event arguments for events. +public class HasFocusEventArgs : CancelEventArgs { - /// Constructs. - /// The view that is losing focus. - /// The view that is gaining focus. - public FocusEventArgs (View leaving, View entering) { - Leaving = leaving; - Entering = entering; + /// Initializes a new instance. + /// The current value of . + /// The value will have if the event is not cancelled. + /// The view that is losing focus. + /// The view that is gaining focus. + public HasFocusEventArgs (bool currentHasFocus, bool newHasFocus, View currentFocused, View newFocused) : base (ref currentHasFocus, ref newHasFocus) + { + CurrentFocused = currentFocused; + NewFocused = newFocused; } - /// - /// Indicates if the current focus event has already been processed and the driver should stop notifying any other - /// event subscriber. It's important to set this value to true specially when updating any View's layout from inside the - /// subscriber method. - /// - public bool Handled { get; set; } + /// Gets or sets the view that is losing focus. + public View CurrentFocused { get; set; } - /// Indicates the view that is losing focus. - public View Leaving { get; set; } - - /// Indicates the view that is gaining focus. - public View Entering { get; set; } + /// Gets or sets the view that is gaining focus. + public View NewFocused { get; set; } } diff --git a/Terminal.Gui/View/Navigation/TabBehavior.cs b/Terminal.Gui/View/Navigation/TabBehavior.cs index e1957718d1..2192a60754 100644 --- a/Terminal.Gui/View/Navigation/TabBehavior.cs +++ b/Terminal.Gui/View/Navigation/TabBehavior.cs @@ -8,6 +8,13 @@ public enum TabBehavior /// /// The View will not be a stop-poknt for keyboard-based navigation. /// + /// + /// + /// This flag has no impact on whether the view can be focused via means other than the keyboard. Use + /// + /// to control whether a View can focus or not. + /// + /// NoStop = 0, /// @@ -16,7 +23,8 @@ public enum TabBehavior TabStop = 1, /// - /// The View will be a stop-point for keyboard-based navigation across groups (e.g. if the user presses (`Ctrl-PageDown`). + /// The View will be a stop-point for keyboard-based navigation across groups (e.g. if the user presses + /// (`Ctrl-PageDown`). /// - TabGroup = 2, + TabGroup = 2 } diff --git a/Terminal.Gui/View/Orientation/IOrientation.cs b/Terminal.Gui/View/Orientation/IOrientation.cs index 34470878e9..db76a6f83a 100644 --- a/Terminal.Gui/View/Orientation/IOrientation.cs +++ b/Terminal.Gui/View/Orientation/IOrientation.cs @@ -1,4 +1,5 @@ - +#nullable enable + namespace Terminal.Gui; using System; @@ -18,7 +19,7 @@ public interface IOrientation /// /// Raised when is changing. Can be cancelled. /// - public event EventHandler> OrientationChanging; + public event EventHandler>? OrientationChanging; /// /// Called when is changing. @@ -31,7 +32,7 @@ public interface IOrientation /// /// Raised when has changed. /// - public event EventHandler> OrientationChanged; + public event EventHandler>? OrientationChanged; /// /// Called when has been changed. diff --git a/Terminal.Gui/View/Orientation/Orientation.cs b/Terminal.Gui/View/Orientation/Orientation.cs index fa8c7b2b8c..480bb435ce 100644 --- a/Terminal.Gui/View/Orientation/Orientation.cs +++ b/Terminal.Gui/View/Orientation/Orientation.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// Direction of an element (horizontal or vertical) public enum Orientation diff --git a/Terminal.Gui/View/Orientation/OrientationHelper.cs b/Terminal.Gui/View/Orientation/OrientationHelper.cs index 2227494dc0..33c33c70f3 100644 --- a/Terminal.Gui/View/Orientation/OrientationHelper.cs +++ b/Terminal.Gui/View/Orientation/OrientationHelper.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// /// Helper class for implementing . @@ -119,7 +120,7 @@ public Orientation Orientation /// it was not canceled). /// /// - public event EventHandler> OrientationChanging; + public event EventHandler>? OrientationChanging; /// /// Raised when the orientation has changed. @@ -134,5 +135,5 @@ public Orientation Orientation /// This event will be raised after the method is called. /// /// - public event EventHandler> OrientationChanged; + public event EventHandler>? OrientationChanged; } diff --git a/Terminal.Gui/View/SuperViewChangedEventArgs.cs b/Terminal.Gui/View/SuperViewChangedEventArgs.cs index 4f25cf47a9..59ddc4ce6f 100644 --- a/Terminal.Gui/View/SuperViewChangedEventArgs.cs +++ b/Terminal.Gui/View/SuperViewChangedEventArgs.cs @@ -7,22 +7,20 @@ public class SuperViewChangedEventArgs : EventArgs { /// Creates a new instance of the class. - /// - /// - public SuperViewChangedEventArgs (View parent, View child) + /// + /// + public SuperViewChangedEventArgs (View superView, View subView) { - Parent = parent; - Child = child; + SuperView = superView; + SubView = subView; } - // TODO: Child is the wrong name. It should be View. /// The view that is having it's changed - public View Child { get; } + public View SubView { get; } - // TODO: Parent is the wrong name. It should be SuperView. /// /// The parent. For this is the old parent (new parent now being null). For /// it is the new parent to whom view now belongs. /// - public View Parent { get; } + public View SuperView { get; } } diff --git a/Terminal.Gui/View/View.Adornments.cs b/Terminal.Gui/View/View.Adornments.cs index 2d179079e8..6013f5b5bf 100644 --- a/Terminal.Gui/View/View.Adornments.cs +++ b/Terminal.Gui/View/View.Adornments.cs @@ -194,6 +194,7 @@ public virtual void SetBorderStyle (LineStyle value) /// /// Fired when the is changing. Allows the event to be cancelled. /// + [CanBeNull] public event EventHandler> BorderStyleChanging; /// diff --git a/Terminal.Gui/View/View.Content.cs b/Terminal.Gui/View/View.Content.cs index 08443a851b..12851d12cb 100644 --- a/Terminal.Gui/View/View.Content.cs +++ b/Terminal.Gui/View/View.Content.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; public partial class View { @@ -153,7 +154,7 @@ public bool ContentSizeTracksViewport /// /// Event raised when the changes. /// - public event EventHandler ContentSizeChanged; + public event EventHandler? ContentSizeChanged; /// /// Converts a Content-relative location to a Screen-relative location. @@ -356,8 +357,7 @@ void ApplySettings (ref Rectangle newViewport) /// Fired when the changes. This event is fired after the has been /// updated. /// - [CanBeNull] - public event EventHandler ViewportChanged; + public event EventHandler? ViewportChanged; /// /// Called when the changes. Invokes the event. diff --git a/Terminal.Gui/View/View.Cursor.cs b/Terminal.Gui/View/View.Cursor.cs index bdba7d85f1..700398526f 100644 --- a/Terminal.Gui/View/View.Cursor.cs +++ b/Terminal.Gui/View/View.Cursor.cs @@ -1,3 +1,4 @@ +#nullable enable namespace Terminal.Gui; public partial class View diff --git a/Terminal.Gui/View/View.Diagnostics.cs b/Terminal.Gui/View/View.Diagnostics.cs index c7ac7b851a..6e5797e26c 100644 --- a/Terminal.Gui/View/View.Diagnostics.cs +++ b/Terminal.Gui/View/View.Diagnostics.cs @@ -1,6 +1,4 @@ - - - +#nullable enable namespace Terminal.Gui; /// Enables diagnostic functions for . diff --git a/Terminal.Gui/View/View.Drawing.cs b/Terminal.Gui/View/View.Drawing.cs index d1a0f77344..9762ba98b4 100644 --- a/Terminal.Gui/View/View.Drawing.cs +++ b/Terminal.Gui/View/View.Drawing.cs @@ -202,11 +202,6 @@ public Rectangle SetClip () /// public void Draw () { - if (!CanBeVisible (this)) - { - return; - } - OnDrawAdornments (); if (ColorScheme is { }) @@ -262,6 +257,7 @@ public void Draw () /// . /// /// + [CanBeNull] public event EventHandler DrawContent; /// Event invoked when the content area of the View is completed drawing. @@ -272,6 +268,7 @@ public void Draw () /// . /// /// + [CanBeNull] public event EventHandler DrawContentComplete; /// Utility function to draw strings that contain a hotkey. @@ -475,6 +472,11 @@ public virtual void OnDrawContent (Rectangle viewport) Clear (); } + if (!CanBeVisible (this)) + { + return; + } + if (!string.IsNullOrEmpty (TextFormatter.Text)) { if (TextFormatter is { }) @@ -505,7 +507,7 @@ public virtual void OnDrawContent (Rectangle viewport) if (TabStop == TabBehavior.TabGroup && _subviews.Count(v => v.Arrangement.HasFlag (ViewArrangement.Overlapped)) > 0) { // TODO: This is a temporary hack to make overlapped non-Toplevels have a zorder. See also View.SetFocus - subviewsNeedingDraw = _tabIndexes.Where ( + subviewsNeedingDraw = _subviews.Where ( view => view.Visible && (view.NeedsDisplay || view.SubViewNeedsDisplay || view.LayoutNeeded) ).Reverse (); diff --git a/Terminal.Gui/View/View.Hierarchy.cs b/Terminal.Gui/View/View.Hierarchy.cs index 70f9d51423..19cef890b3 100644 --- a/Terminal.Gui/View/View.Hierarchy.cs +++ b/Terminal.Gui/View/View.Hierarchy.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Diagnostics; namespace Terminal.Gui; @@ -5,27 +6,31 @@ namespace Terminal.Gui; public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.) { private static readonly IList _empty = new List (0).AsReadOnly (); - private List _subviews; // This is null, and allocated on demand. - private View _superView; - /// Indicates whether the view was added to . - public bool IsAdded { get; private set; } + private List? _subviews; // This is null, and allocated on demand. + + // Internally, we use InternalSubviews rather than subviews, as we do not expect us + // to make the same mistakes our users make when they poke at the Subviews. + internal IList InternalSubviews => _subviews ?? _empty; /// This returns a list of the subviews contained by this view. /// The subviews. public IList Subviews => _subviews?.AsReadOnly () ?? _empty; + private View? _superView; + /// Returns the container for this view, or null if this view has not been added to a container. /// The super view. - public virtual View SuperView + public virtual View? SuperView { - get => _superView; + get => _superView!; set => throw new NotImplementedException (); } - // Internally, we use InternalSubviews rather than subviews, as we do not expect us - // to make the same mistakes our users make when they poke at the Subviews. - internal IList InternalSubviews => _subviews ?? _empty; + #region AddRemove + + /// Indicates whether the view was added to . + public bool IsAdded { get; private set; } /// Adds a subview (child) to this view. /// @@ -42,42 +47,26 @@ public virtual View SuperView /// The view that was added. public virtual View Add (View view) { - if (view is null) - { - return view; - } - if (_subviews is null) { - _subviews = new (); + _subviews = []; } - if (_tabIndexes is null) - { - _tabIndexes = new (); - } + Debug.WriteLineIf (_subviews.Contains (view), $"WARNING: {view} has already been added to {this}."); + + // TileView likes to add views that were previously added and have HasFocus = true. No bueno. + view.HasFocus = false; - Debug.WriteLineIf (_subviews.Contains (view), $"BUGBUG: {view} has already been added to {this}."); _subviews.Add (view); - _tabIndexes.Add (view); view._superView = this; - if (view.CanFocus) + if (view is { Enabled: true, Visible: true, CanFocus: true }) { - // BUGBUG: This is a poor API design. Automatic behavior like this is non-obvious and should be avoided. Instead, callers to Add should be explicit about what they want. - _addingViewSoCanFocusAlsoUpdatesSuperView = true; - - if (SuperView?.CanFocus == false) + // Add will cause the newly added subview to gain focus if it's focusable + if (HasFocus) { - SuperView._addingViewSoCanFocusAlsoUpdatesSuperView = true; - SuperView.CanFocus = true; - SuperView._addingViewSoCanFocusAlsoUpdatesSuperView = false; + view.SetFocus (); } - - // QUESTION: This automatic behavior of setting CanFocus to true on the SuperView is not documented, and is annoying. - CanFocus = true; - view._tabIndex = _tabIndexes.IndexOf (view); - _addingViewSoCanFocusAlsoUpdatesSuperView = false; } if (view.Enabled && !Enabled) @@ -113,7 +102,7 @@ public virtual View Add (View view) /// the lifecycle of the subviews to be transferred to this View. /// /// - public void Add (params View [] views) + public void Add (params View []? views) { if (views is null) { @@ -127,32 +116,13 @@ public void Add (params View [] views) } /// Event fired when this view is added to another. - public event EventHandler Added; - - /// Get the top superview of a given . - /// The superview view. - public View GetTopSuperView (View view = null, View superview = null) - { - View top = superview ?? Application.Top; - - for (View v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView) - { - top = v; - - if (top == superview) - { - break; - } - } - - return top; - } + public event EventHandler? Added; /// Method invoked when a subview is being added to this view. /// Event where is the subview being added. public virtual void OnAdded (SuperViewChangedEventArgs e) { - View view = e.Child; + View view = e.SubView; view.IsAdded = true; view.OnResizeNeeded (); view.Added?.Invoke (this, e); @@ -162,7 +132,7 @@ public virtual void OnAdded (SuperViewChangedEventArgs e) /// Event args describing the subview being removed. public virtual void OnRemoved (SuperViewChangedEventArgs e) { - View view = e.Child; + View view = e.SubView; view.IsAdded = false; view.Removed?.Invoke (this, e); } @@ -175,18 +145,27 @@ public virtual void OnRemoved (SuperViewChangedEventArgs e) /// lifecycle to be transferred to the caller; the caller muse call . /// /// - public virtual View Remove (View view) + /// + /// The removed View. if the View could not be removed. + /// + public virtual View? Remove (View view) { - if (view is null || _subviews is null) + if (_subviews is null) { return view; } Rectangle touched = view.Frame; + + // If a view being removed is focused, it should lose focus. + if (view.HasFocus) + { + view.HasFocus = false; + } + _subviews.Remove (view); - _tabIndexes.Remove (view); - view._superView = null; - //view._tabIndex = -1; + view._superView = null; // Null this AFTER removing focus + SetNeedsLayout (); SetNeedsDisplay (); @@ -198,13 +177,13 @@ public virtual View Remove (View view) } } - OnRemoved (new (this, view)); - - if (Focused == view) + if (HasFocus) { - Focused = null; + FocusDeepest (NavigationDirection.Forward, TabStop); } + OnRemoved (new (this, view)); + return view; } @@ -233,18 +212,43 @@ public virtual void RemoveAll () } /// Event fired when this view is removed from another. - public event EventHandler Removed; + public event EventHandler? Removed; + #endregion AddRemove - /// Moves one position towards the start of the list - /// The subview to move forward. - public void BringSubviewForward (View subview) + // TODO: Mark as internal. Or nuke. + /// Get the top superview of a given . + /// The superview view. + public View? GetTopSuperView (View? view = null, View? superview = null) + { + View? top = superview ?? Application.Top; + + for (View? v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView) + { + top = v; + + if (top == superview) + { + break; + } + } + + return top; + } + + #region SubViewOrdering + + /// + /// Moves one position towards the end of the list. + /// + /// The subview to move. + public void MoveSubviewTowardsEnd (View subview) { PerformActionForSubview ( subview, x => { - int idx = _subviews.IndexOf (x); + int idx = _subviews!.IndexOf (x); if (idx + 1 < _subviews.Count) { @@ -255,30 +259,33 @@ public void BringSubviewForward (View subview) ); } - /// Moves to the start of the list. - /// The subview to send to the start. - public void BringSubviewToFront (View subview) + /// + /// Moves to the end of the list. + /// + /// The subview to move. + public void MoveSubviewToEnd (View subview) { PerformActionForSubview ( subview, x => { - _subviews.Remove (x); + _subviews!.Remove (x); _subviews.Add (x); } ); } - - /// Moves one position towards the end of the list - /// The subview to move backwards. - public void SendSubviewBackwards (View subview) + /// + /// Moves one position towards the start of the list. + /// + /// The subview to move. + public void MoveSubviewTowardsStart (View subview) { PerformActionForSubview ( subview, x => { - int idx = _subviews.IndexOf (x); + int idx = _subviews!.IndexOf (x); if (idx > 0) { @@ -289,15 +296,17 @@ public void SendSubviewBackwards (View subview) ); } - /// Moves to the end of the list. - /// The subview to send to the end. - public void SendSubviewToBack (View subview) + /// + /// Moves to the start of the list. + /// + /// The subview to move. + public void MoveSubviewToStart (View subview) { PerformActionForSubview ( subview, x => { - _subviews.Remove (x); + _subviews!.Remove (x); _subviews.Insert (0, subview); } ); @@ -310,7 +319,7 @@ public void SendSubviewToBack (View subview) /// private void PerformActionForSubview (View subview, Action action) { - if (_subviews.Contains (subview)) + if (_subviews!.Contains (subview)) { action (subview); } @@ -320,4 +329,5 @@ private void PerformActionForSubview (View subview, Action action) subview.SetNeedsDisplay (); } + #endregion SubViewOrdering } diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 7a7fd35c51..ce72d068a2 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -1,12 +1,12 @@ -using System.ComponentModel; +#nullable enable using System.Diagnostics; namespace Terminal.Gui; -public partial class View // Keyboard APIs +public partial class View // Keyboard APIs { /// - /// Helper to configure all things keyboard related for a View. Called from the View constructor. + /// Helper to configure all things keyboard related for a View. Called from the View constructor. /// private void SetupKeyboard () { @@ -22,17 +22,14 @@ private void SetupKeyboard () } /// - /// Helper to dispose all things keyboard related for a View. Called from the View Dispose method. + /// Helper to dispose all things keyboard related for a View. Called from the View Dispose method. /// - private void DisposeKeyboard () - { - TitleTextFormatter.HotKeyChanged -= TitleTextFormatter_HotKeyChanged; - } + private void DisposeKeyboard () { TitleTextFormatter.HotKeyChanged -= TitleTextFormatter_HotKeyChanged; } #region HotKey Support /// - /// Called when the HotKey command () is invoked. Causes this view to be focused. + /// Called when the HotKey command () is invoked. Causes this view to be focused. /// /// If the command was canceled. private bool? OnHotKey () @@ -40,6 +37,7 @@ private void DisposeKeyboard () if (CanFocus) { SetFocus (); + return true; } @@ -47,10 +45,10 @@ private void DisposeKeyboard () } /// Invoked when the is changed. - public event EventHandler HotKeyChanged; + public event EventHandler? HotKeyChanged; private Key _hotKey = new (); - private void TitleTextFormatter_HotKeyChanged (object sender, KeyChangedEventArgs e) { HotKeyChanged?.Invoke (this, e); } + private void TitleTextFormatter_HotKeyChanged (object? sender, KeyChangedEventArgs e) { HotKeyChanged?.Invoke (this, e); } /// /// Gets or sets the hot key defined for this view. Pressing the hot key on the keyboard while this view has focus will @@ -117,7 +115,8 @@ public virtual Key HotKey /// /// /// By default, key bindings are added for both the base key (e.g. ) and the Alt-shifted key - /// (e.g. Key.D3.WithAlt) This behavior can be overriden by overriding . + /// (e.g. Key.D3.WithAlt) This behavior can be overriden by overriding + /// . /// /// /// By default, when is through key bindings @@ -131,7 +130,7 @@ public virtual Key HotKey /// Arbitrary context that can be associated with this key binding. /// if the HotKey bindings were added. /// - public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, [CanBeNull] object context = null) + public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, object? context = null) { if (_hotKey == hotKey) { @@ -195,6 +194,7 @@ public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, [CanBeN if (newKey != Key.Empty) { KeyBinding keyBinding = new ([Command.HotKey], KeyBindingScope.HotKey, context); + // Add the base and Alt key KeyBindings.Remove (newKey); KeyBindings.Add (newKey, keyBinding); @@ -220,10 +220,7 @@ public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, [CanBeN /// public virtual Rune HotKeySpecifier { - get - { - return TitleTextFormatter.HotKeySpecifier; - } + get => TitleTextFormatter.HotKeySpecifier; set { TitleTextFormatter.HotKeySpecifier = TextFormatter.HotKeySpecifier = value; @@ -363,7 +360,7 @@ public virtual bool OnKeyDown (Key keyEvent) /// /// See for an overview of Terminal.Gui keyboard APIs. /// - public event EventHandler KeyDown; + public event EventHandler? KeyDown; /// /// Low-level API called when the user presses a key, allowing views do things during key down events. This is @@ -411,7 +408,7 @@ public virtual bool OnProcessKeyDown (Key keyEvent) /// /// See for an overview of Terminal.Gui keyboard APIs. /// - public event EventHandler ProcessKeyDown; + public event EventHandler? ProcessKeyDown; #endregion KeyDown Event @@ -502,7 +499,7 @@ public virtual bool OnKeyUp (Key keyEvent) /// See for an overview of Terminal.Gui keyboard APIs. /// /// - public event EventHandler KeyUp; + public event EventHandler? KeyUp; #endregion KeyUp Event @@ -511,7 +508,7 @@ public virtual bool OnKeyUp (Key keyEvent) #region Key Bindings /// Gets the key bindings for this view. - public KeyBindings KeyBindings { get; internal set; } + public KeyBindings KeyBindings { get; internal set; } = null!; private Dictionary> CommandImplementations { get; } = new (); @@ -535,11 +532,11 @@ public virtual bool OnKeyUp (Key keyEvent) if (KeyBindings.TryGet (keyEvent, scope, out KeyBinding kb)) { InvokingKeyBindings?.Invoke (this, keyEvent); + if (keyEvent.Handled) { return true; } - } // * If no key binding was found, `InvokeKeyBindings` returns `null`. @@ -614,6 +611,7 @@ private bool ProcessSubViewKeyBindings (Key keyEvent, KeyBindingScope scope, ref { continue; } + if (subview.KeyBindings.TryGet (keyEvent, scope, out KeyBinding binding)) { if (binding.Scope == KeyBindingScope.Focused && !subview.HasFocus) @@ -631,6 +629,7 @@ private bool ProcessSubViewKeyBindings (Key keyEvent, KeyBindingScope scope, ref if (subViewHandled is { }) { handled = subViewHandled; + if ((bool)subViewHandled) { return true; @@ -639,6 +638,7 @@ private bool ProcessSubViewKeyBindings (Key keyEvent, KeyBindingScope scope, ref } bool recurse = subview.ProcessSubViewKeyBindings (keyEvent, scope, ref handled, invoke); + if (recurse || (handled is { } && (bool)handled)) { return true; @@ -652,12 +652,12 @@ private bool ProcessSubViewKeyBindings (Key keyEvent, KeyBindingScope scope, ref // 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 + /// 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 IsHotKeyKeyBound (Key key, out View boundView) + public bool IsHotKeyKeyBound (Key key, out View? boundView) { // recurse through the subviews to find the views that has the key bound boundView = null; @@ -667,6 +667,7 @@ public bool IsHotKeyKeyBound (Key key, out View boundView) if (subview.KeyBindings.TryGet (key, KeyBindingScope.HotKey, out _)) { boundView = subview; + return true; } @@ -674,8 +675,8 @@ public bool IsHotKeyKeyBound (Key key, out View boundView) { return true; } - } + return false; } @@ -683,7 +684,7 @@ public bool IsHotKeyKeyBound (Key key, out View boundView) /// Invoked when a key is pressed that may be mapped to a key binding. Set to true to /// stop the key from being processed by other views. /// - public event EventHandler InvokingKeyBindings; + public event EventHandler? InvokingKeyBindings; /// /// Invokes any binding that is registered on this and matches the @@ -713,20 +714,19 @@ public bool IsHotKeyKeyBound (Key key, out View boundView) //var boundView = views [0]; //var commandBinding = boundView.KeyBindings.Get (key); Debug.WriteLine ( - $"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command.");//{commandBinding.Commands [0]}: {boundView}."); + $"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command."); //{commandBinding.Commands [0]}: {boundView}."); } // 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?.IsHotKeyKeyBound (key, out View previouslyBoundView) ?? false) + if (SuperView?.IsHotKeyKeyBound (key, out View? previouslyBoundView) ?? false) { Debug.WriteLine ($"WARNING: InvokeKeyBindings ({key}) - A subview or peer has bound this Key and will not see it: {previouslyBoundView}."); } #endif - foreach (Command command in binding.Commands) { if (!CommandImplementations.ContainsKey (command)) @@ -763,7 +763,7 @@ public bool IsHotKeyKeyBound (Key key, out View boundView) /// if the command was invoked the command was handled. /// if the command was invoked and the command was not handled. /// - public bool? InvokeCommands (Command [] commands, [CanBeNull] Key key = null, [CanBeNull] KeyBinding? keyBinding = null) + public bool? InvokeCommands (Command [] commands, Key? key = null, KeyBinding? keyBinding = null) { bool? toReturn = null; @@ -798,11 +798,12 @@ public bool IsHotKeyKeyBound (Key key, out View boundView) /// if no command was found. if the command was invoked, and it /// handled the command. if the command was invoked, and it did not handle the command. /// - public bool? InvokeCommand (Command command, [CanBeNull] Key key = null, [CanBeNull] KeyBinding? keyBinding = null) + public bool? InvokeCommand (Command command, Key? key = null, KeyBinding? keyBinding = null) { - if (CommandImplementations.TryGetValue (command, out Func implementation)) + if (CommandImplementations.TryGetValue (command, out Func? implementation)) { var context = new CommandContext (command, key, keyBinding); // Create the context here + return implementation (context); } @@ -812,7 +813,7 @@ public bool IsHotKeyKeyBound (Key key, out View boundView) /// /// /// Sets the function that will be invoked for a . Views should call - /// AddCommand for each command they support. + /// AddCommand for each command they support. /// /// /// If AddCommand has already been called for will @@ -820,22 +821,20 @@ public bool IsHotKeyKeyBound (Key key, out View boundView) /// /// /// - /// - /// This version of AddCommand is for commands that require . Use - /// in cases where the command does not require a . - /// + /// + /// This version of AddCommand is for commands that require . Use + /// + /// in cases where the command does not require a . + /// /// /// The command. /// The function. - protected void AddCommand (Command command, Func f) - { - CommandImplementations [command] = f; - } + protected void AddCommand (Command command, Func f) { CommandImplementations [command] = f; } /// /// /// Sets the function that will be invoked for a . Views should call - /// AddCommand for each command they support. + /// AddCommand for each command they support. /// /// /// If AddCommand has already been called for will @@ -843,17 +842,15 @@ protected void AddCommand (Command command, Func f) /// /// /// - /// - /// This version of AddCommand is for commands that do not require a . - /// If the command requires context, use - /// + /// + /// This version of AddCommand is for commands that do not require a . + /// If the command requires context, use + /// + /// /// /// The command. /// The function. - protected void AddCommand (Command command, Func f) - { - CommandImplementations [command] = ctx => f (); - } + protected void AddCommand (Command command, Func f) { CommandImplementations [command] = ctx => f (); } /// Returns all commands that are supported by this . /// diff --git a/Terminal.Gui/View/View.Layout.cs b/Terminal.Gui/View/View.Layout.cs index d77ceba2b3..b407d4fa18 100644 --- a/Terminal.Gui/View/View.Layout.cs +++ b/Terminal.Gui/View/View.Layout.cs @@ -121,7 +121,7 @@ out StatusBar? statusBar View? superView; statusBar = null!; - if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + if (viewToMove is not Toplevel || viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) { maxDimension = Driver.Cols; superView = Application.Top; @@ -163,7 +163,7 @@ out StatusBar? statusBar } else { - View t = viewToMove!.SuperView; + View? t = viewToMove!.SuperView; while (t is { } and not Toplevel) { @@ -194,7 +194,7 @@ out StatusBar? statusBar } else { - View t = viewToMove!.SuperView; + View? t = viewToMove!.SuperView; while (t is { } and not Toplevel) { @@ -311,7 +311,7 @@ private void SetFrame (in Rectangle frame) public virtual Rectangle FrameToScreen () { Rectangle screen = Frame; - View current = SuperView; + View? current = SuperView; while (current is { }) { @@ -547,14 +547,14 @@ public Dim? Width /// Subscribe to this event to perform tasks when the has been resized or the layout has /// otherwise changed. /// - public event EventHandler LayoutComplete; + public event EventHandler? LayoutComplete; /// Fired after the View's method has completed. /// /// Subscribe to this event to perform tasks when the has been resized or the layout has /// otherwise changed. /// - public event EventHandler LayoutStarted; + public event EventHandler? LayoutStarted; /// /// Adjusts given the SuperView's ContentSize (nominally the same as @@ -694,7 +694,7 @@ public virtual void LayoutSubviews () HashSet nodes = new (); HashSet<(View, View)> edges = new (); CollectAll (this, ref nodes, ref edges); - List ordered = TopologicalSort (SuperView, nodes, edges); + List ordered = TopologicalSort (SuperView!, nodes, edges); foreach (View v in ordered) { diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index 44553f6ae9..b66c07fe35 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -1,17 +1,17 @@ -using System.ComponentModel; +#nullable enable +using System.ComponentModel; namespace Terminal.Gui; public partial class View // Mouse APIs { - [CanBeNull] - private ColorScheme _savedHighlightColorScheme; + private ColorScheme? _savedHighlightColorScheme; /// /// Fired when the view is highlighted. Set to /// to implement a custom highlight scheme or prevent the view from being highlighted. /// - public event EventHandler> Highlight; + public event EventHandler>? Highlight; /// /// Gets or sets whether the will be highlighted visually while the mouse button is @@ -29,10 +29,10 @@ public partial class View // Mouse APIs /// The coordinates are relative to . /// /// - public event EventHandler MouseClick; + public event EventHandler? MouseClick; /// Event fired when the mouse moves into the View's . - public event EventHandler MouseEnter; + public event EventHandler? MouseEnter; /// Event fired when a mouse event occurs. /// @@ -40,10 +40,10 @@ public partial class View // Mouse APIs /// The coordinates are relative to . /// /// - public event EventHandler MouseEvent; + public event EventHandler? MouseEvent; /// Event fired when the mouse leaves the View's . - public event EventHandler MouseLeave; + public event EventHandler? MouseLeave; /// /// Processes a . This method is called by when a mouse @@ -244,7 +244,7 @@ protected bool OnMouseClick (MouseEventEventArgs args) if (!Enabled) { // QUESTION: Is this right? Should a disabled view eat mouse clicks? - return args.Handled = true; + return args.Handled = false; } MouseClick?.Invoke (this, args); diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index 4cc22448d5..ed8e2b8445 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -1,26 +1,14 @@ +#nullable enable using System.Diagnostics; -using static Terminal.Gui.FakeDriver; namespace Terminal.Gui; public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...) { - // BUGBUG: This is a poor API design. Automatic behavior like this is non-obvious and should be avoided. Instead, callers to Add should be explicit about what they want. - // Set to true in Add() to indicate that the view being added to a SuperView has CanFocus=true. - // Makes it so CanFocus will update the SuperView's CanFocus property. - internal bool _addingViewSoCanFocusAlsoUpdatesSuperView; - - private NavigationDirection _focusDirection; - - private bool _hasFocus; - - // Used to cache CanFocus on subviews when CanFocus is set to false so that it can be restored when CanFocus is changed back to true - private bool _oldCanFocus; - private bool _canFocus; /// - /// Advances the focus to the next or previous view in , based on + /// Advances the focus to the next or previous view in the focus chain, based on /// . /// itself. /// @@ -37,137 +25,110 @@ public partial class View // Focus and cross-view navigation management (TabStop /// public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior) { - if (!CanBeVisible (this)) + if (!CanBeVisible (this)) // TODO: is this check needed? { return false; } - FocusDirection = direction; + View? focused = Focused; - if (TabIndexes is null || TabIndexes.Count == 0) + if (focused is { } && focused.AdvanceFocus (direction, behavior)) + { + return true; + } + + // AdvanceFocus did not advance + View [] index = GetSubviewFocusChain (direction, behavior); + + if (index.Length == 0) { return false; } - if (Focused is null) + if (behavior == TabBehavior.TabGroup) { - switch (direction) + if (direction == NavigationDirection.Forward && focused == index [^1] && SuperView is null) { - case NavigationDirection.Forward: - FocusFirst (behavior); + // We're at the top of the focus chain. Go back down the focus chain and focus the first TabGroup + View [] views = GetSubviewFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup); - break; - case NavigationDirection.Backward: - FocusLast (behavior); + if (views.Length > 0) + { + View [] subViews = views [0].GetSubviewFocusChain (NavigationDirection.Forward, TabBehavior.TabStop); - break; - default: - throw new ArgumentOutOfRangeException (nameof (direction), direction, null); + if (subViews.Length > 0) + { + if (subViews [0].SetFocus ()) + { + return true; + } + } + } } - return Focused is { }; - } - - if (Focused is { }) - { - if (Focused.AdvanceFocus (direction, behavior)) + if (direction == NavigationDirection.Backward && focused == index [0]) { - // TODO: Temporary hack to make Application.Navigation.FocusChanged work - if (Focused.Focused is null) + // We're at the bottom of the focus chain + View [] views = GetSubviewFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup); + + if (views.Length > 0) { - Application.Navigation?.SetFocused (Focused); + View [] subViews = views [^1].GetSubviewFocusChain (NavigationDirection.Forward, TabBehavior.TabStop); + + if (subViews.Length > 0) + { + if (subViews [0].SetFocus ()) + { + return true; + } + } } - return true; } } - var index = GetScopedTabIndexes (behavior, direction); - if (index.Length == 0) - { - return false; - } - var focusedIndex = index.IndexOf (Focused); + int focusedIndex = index.IndexOf (Focused); // Will return -1 if Focused can't be found or is null int next = 0; if (focusedIndex < index.Length - 1) { + // We're moving w/in the subviews next = focusedIndex + 1; } else { - // focusedIndex is at end of list. If we are going backwards,... - if (behavior == TabStop) - { - // Go up the hierarchy - // Leave - Focused.SetHasFocus (false, this); + // We're moving beyond the last subview - // Signal that nothing is focused, and callers should try a peer-subview - Focused = null; + // Determine if focus should remain in this focus chain, or move to the superview's focus chain + // If we are TabStop and our SuperView has at least one other TabStop subview, move to the SuperView's chain + if (TabStop == TabBehavior.TabStop && SuperView is { } && SuperView.GetSubviewFocusChain (direction, behavior).Length > 1) + { return false; } - // Wrap around - //if (SuperView is {}) - //{ - // if (direction == NavigationDirection.Forward) - // { - // return false; - // } - // else - // { - // return false; - - // //SuperView.FocusFirst (groupOnly); - // } - // return true; - //} - //next = index.Length - 1; + // TabGroup is special-cased. + if (focused?.TabStop == TabBehavior.TabGroup) + { + if (SuperView?.GetSubviewFocusChain (direction, TabBehavior.TabGroup)?.Length > 0) + { + // Our superview has a TabGroup subview; signal we couldn't move so we nav out to it + return false; + } + } } View view = index [next]; - - // The subview does not have focus, but at least one other that can. Can this one be focused? - if (view.CanFocus && view.Visible && view.Enabled) + if (view.HasFocus) { - // Make Focused Leave - Focused.SetHasFocus (false, view); - - switch (direction) - { - case NavigationDirection.Forward: - view.FocusFirst (TabBehavior.TabStop); - - break; - case NavigationDirection.Backward: - view.FocusLast (TabBehavior.TabStop); - - break; - } - - SetFocus (view); - - // TODO: Temporary hack to make Application.Navigation.FocusChanged work - if (view.Focused is null) - { - Application.Navigation?.SetFocused (view); - } - - return true; + // We could not advance + return view == this; } - if (Focused is { }) - { - // Leave - Focused.SetHasFocus (false, this); - - // Signal that nothing is focused, and callers should try a peer-subview - Focused = null; - } + // The subview does not have focus, but at least one other that can. Can this one be focused? + (bool focusSet, bool _) = view.SetHasFocusTrue (Focused); - return false; + return focusSet; } /// Gets or sets a value indicating whether this can be focused. @@ -180,16 +141,13 @@ public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior) /// the next focusable view. /// /// - /// When set to , the will be set to -1. - /// - /// - /// When set to , the values of and for all + /// When set to , the value of for all /// subviews will be cached so that when is set back to , the subviews /// will be restored to their previous values. /// /// - /// Changing this peroperty to will cause to be set to - /// " as a convenience. Changing this peroperty to + /// Changing this property to will cause to be set to + /// " as a convenience. Changing this property to /// will have no effect on . /// /// @@ -198,11 +156,6 @@ public bool CanFocus get => _canFocus; set { - if (!_addingViewSoCanFocusAlsoUpdatesSuperView && IsInitialized && SuperView?.CanFocus == false && value) - { - throw new InvalidOperationException ("Cannot set CanFocus to true if the SuperView CanFocus is false!"); - } - if (_canFocus == value) { return; @@ -210,87 +163,24 @@ public bool CanFocus _canFocus = value; - switch (_canFocus) - { - case false when _tabIndex > -1: - // BUGBUG: This is a poor API design. Automatic behavior like this is non-obvious and should be avoided. Callers should adjust TabIndex explicitly. - //TabIndex = -1; - - break; - - case true when SuperView?.CanFocus == false && _addingViewSoCanFocusAlsoUpdatesSuperView: - SuperView.CanFocus = true; - - break; - } - if (TabStop is null && _canFocus) { TabStop = TabBehavior.TabStop; } - if (!_canFocus && SuperView?.Focused == this) - { - SuperView.Focused = null; - } - if (!_canFocus && HasFocus) { - SetHasFocus (false, this); - SuperView?.RestoreFocus (); - - // If EnsureFocus () didn't set focus to a view, focus the next focusable view in the application - if (SuperView is { Focused: null }) - { - SuperView.AdvanceFocus (NavigationDirection.Forward, null); - - if (SuperView.Focused is null && Application.Current is { }) - { - Application.Current.AdvanceFocus (NavigationDirection.Forward, null); - } - - ApplicationOverlapped.BringOverlappedTopToFront (); - } + // If CanFocus is set to false and this view has focus, make it leave focus + HasFocus = false; } - if (_subviews is { } && IsInitialized) + if (_canFocus && !HasFocus && Visible && SuperView is { } && SuperView.Focused is null) { - foreach (View view in _subviews) - { - if (view.CanFocus != value) - { - if (!value) - { - // Cache the old CanFocus and TabIndex so that they can be restored when CanFocus is changed back to true - view._oldCanFocus = view.CanFocus; - view._oldTabIndex = view._tabIndex; - view.CanFocus = false; - - //view._tabIndex = -1; - } - else - { - if (_addingViewSoCanFocusAlsoUpdatesSuperView) - { - view._addingViewSoCanFocusAlsoUpdatesSuperView = true; - } - - // Restore the old CanFocus and TabIndex to the values they held before CanFocus was set to false - view.CanFocus = view._oldCanFocus; - view._tabIndex = view._oldTabIndex; - view._addingViewSoCanFocusAlsoUpdatesSuperView = false; - } - } - } - - if (this is Toplevel && Application.Current!.Focused != this) - { - ApplicationOverlapped.BringOverlappedTopToFront (); - } + // If CanFocus is set to true and this view does not have focus, make it enter focus + SetFocus (); } OnCanFocusChanged (); - SetNeedsDisplay (); } } @@ -298,115 +188,51 @@ public bool CanFocus /// /// Raised by the virtual method. /// - public event EventHandler CanFocusChanged; - - /// Raised when the view is gaining (entering) focus. Can be cancelled. - /// - /// Raised by the virtual method. - /// - public event EventHandler Enter; - - /// Returns the currently focused Subview inside this view, or if nothing is focused. - /// The currently focused Subview. - public View Focused { get; private set; } + public event EventHandler? CanFocusChanged; /// - /// Focuses the first focusable view in if one exists. If there are no views in - /// then the focus is set to the view itself. - /// - /// - public void FocusFirst (TabBehavior? behavior) - { - if (!CanBeVisible (this)) - { - return; - } - - if (_tabIndexes is null) - { - SuperView?.SetFocus (this); - - return; - } - - var indicies = GetScopedTabIndexes (behavior, NavigationDirection.Forward); - if (indicies.Length > 0) - { - SetFocus (indicies [0]); - } - } - - /// - /// Focuses the last focusable view in if one exists. If there are no views in - /// then the focus is set to the view itself. + /// Focuses the deepest focusable Subview if one exists. If there are no focusable Subviews then the focus is set to + /// the view itself. /// + /// /// - public void FocusLast (TabBehavior? behavior) + /// if a subview other than this was focused. + public bool FocusDeepest (NavigationDirection direction, TabBehavior? behavior) { - if (!CanBeVisible (this)) - { - return; - } + View? deepest = FindDeepestFocusableView (direction, behavior); - if (_tabIndexes is null) + if (deepest is { }) { - SuperView?.SetFocus (this); - - return; + return deepest.SetFocus (); } - var indicies = GetScopedTabIndexes (behavior, NavigationDirection.Forward); - if (indicies.Length > 0) - { - SetFocus (indicies [^1]); - } + return SetFocus (); } - /// - /// Gets or sets whether this view has focus. - /// - /// - /// - /// Causes the and virtual methods (and and - /// events to be raised) when the value changes. - /// - /// - /// Setting this property to will recursively set to - /// - /// for any focused subviews. - /// - /// - public bool HasFocus + /// Gets the currently focused Subview of this view, or if nothing is focused. + public View? Focused { - // Force the specified view to have focus - set => SetHasFocus (value, this, true); - get => _hasFocus; + get { return Subviews.FirstOrDefault (v => v.HasFocus); } } /// Returns a value indicating if this View is currently on Top (Active) public bool IsCurrentTop => Application.Current == this; - /// Raised when the view is losing (leaving) focus. Can be cancelled. - /// - /// Raised by the virtual method. - /// - public event EventHandler Leave; - /// - /// Returns the most focused Subview in the chain of subviews (the leaf view that has the focus), or - /// if nothing is focused. + /// Returns the most focused Subview down the subview-hierarchy. /// - /// The most focused Subview. - public View MostFocused + /// The most focused Subview, or if no Subview is focused. + public View? MostFocused { get { + // TODO: Remove this API. It's duplicative of Application.Navigation.GetFocused. if (Focused is null) { return null; } - View most = Focused.MostFocused; + View? most = Focused.MostFocused; if (most is { }) { @@ -423,407 +249,462 @@ public View MostFocused /// public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); } - // BUGBUG: The focus API is poorly defined and implemented. It deeply intertwines the view hierarchy with the tab order. - - /// Invoked when this view is gaining focus (entering). - /// The view that is leaving focus. - /// , if the event was handled, otherwise. - /// - /// - /// Overrides must call the base class method to ensure that the event is raised. If the event - /// is handled, the method should return . - /// - /// - public virtual bool OnEnter (View leavingView) + /// + /// INTERNAL API to restore focus to the subview that had focus before this view lost focus. + /// + /// + /// Returns true if focus was restored to a subview, false otherwise. + /// + internal bool RestoreFocus () { - var args = new FocusEventArgs (leavingView, this); - Enter?.Invoke (this, args); - - if (args.Handled) + if (Focused is null && _subviews?.Count > 0) { - return true; + if (_previouslyMostFocused is { } /* && (behavior is null || _previouslyMostFocused.TabStop == behavior)*/) + { + return _previouslyMostFocused.SetFocus (); + } + + return false; } return false; } - /// Invoked when this view is losing focus (leaving). - /// The view that is entering focus. - /// , if the event was handled, otherwise. - /// - /// - /// Overrides must call the base class method to ensure that the event is raised. If the event - /// is handled, the method should return . - /// - /// - public virtual bool OnLeave (View enteringView) + private View? FindDeepestFocusableView (NavigationDirection direction, TabBehavior? behavior) { - // BUGBUG: _hasFocus should ALWAYS be false when this method is called. - if (_hasFocus) - { - Debug.WriteLine ($"BUGBUG: HasFocus should ALWAYS be false when OnLeave is called."); - return true; - } - var args = new FocusEventArgs (this, enteringView); - Leave?.Invoke (this, args); + View [] indicies = GetSubviewFocusChain (direction, behavior); - if (args.Handled) + foreach (View v in indicies) { - return true; + return v.FindDeepestFocusableView (direction, behavior); } - return false; + return null; } - /// - /// Causes this view to be focused. All focusable views up the Superview hierarchy will also be focused. - /// - public void SetFocus () - { - if (!CanBeVisible (this) || !Enabled) - { - if (HasFocus) - { - // If this view is focused, make it leave focus - SetHasFocus (false, this); - } + #region HasFocus - return; - } - - // Recursively set focus upwards in the view hierarchy - if (SuperView is { }) - { - SuperView.SetFocus (this); - } - else - { - SetFocus (this); - } - } + // Backs `HasFocus` and is the ultimate source of truth whether a View has focus or not. + private bool _hasFocus; /// - /// INTERNAL API that gets or sets the focus direction for this view and all subviews. - /// Setting this property will set the focus direction for all views up the SuperView hierarchy. + /// Gets or sets whether this view has focus. /// - internal NavigationDirection FocusDirection + /// + /// + /// Only Views that are visible, enabled, and have set to are + /// focusable. If + /// these conditions are not met when this property is set to will + /// not change. + /// + /// + /// Setting this property causes the and virtual + /// methods (and and + /// events to be raised). If the event is cancelled, will not + /// be changed. + /// + /// + /// Setting this property to will recursively set to + /// for all SuperViews up the hierarchy. + /// + /// + /// Setting this property to will cause the subview furthest down the hierarchy that is + /// focusable to also gain focus (as long as + /// + /// + /// Setting this property to will cause to set + /// the focus on the next view to be focused. + /// + /// + public bool HasFocus { - get => SuperView?.FocusDirection ?? _focusDirection; set { - if (SuperView is { }) + if (HasFocus == value) { - SuperView.FocusDirection = value; + return; + } + + if (value) + { + // NOTE: If Application.Navigation is null, we pass null to FocusChanging. For unit tests. + (bool focusSet, bool _) = SetHasFocusTrue (Application.Navigation?.GetFocused ()); + + if (focusSet) + { + // The change happened + // HasFocus is now true + } } else { - _focusDirection = value; + SetHasFocusFalse (null); } } + get => _hasFocus; } /// - /// INTERNAL helper for calling or based on - /// . - /// FocusDirection is not public. This API is thus non-deterministic from a public API perspective. + /// Causes this view to be focused. Calling this method has the same effect as setting to + /// but with the added benefit of returning a value indicating whether the focus was set. /// - internal void RestoreFocus () + public bool SetFocus () { - if (Focused is null && _subviews?.Count > 0) - { - if (FocusDirection == NavigationDirection.Forward) - { - FocusFirst (null); - } - else - { - FocusLast (null); - } - } + (bool focusSet, bool _) = SetHasFocusTrue (Application.Navigation?.GetFocused ()); + + return focusSet; } /// - /// Internal API that causes to enter focus. - /// does not need to be a subview. - /// Recursively sets focus upwards in the view hierarchy. + /// INTERNAL: Called when focus is going to change to this view. This method is called by and + /// other methods that + /// set or remove focus from a view. /// - /// - private void SetFocus (View viewToEnterFocus) + /// + /// The previously focused view. If there is no previously focused + /// view. + /// + /// + /// if was changed to . + /// + private (bool focusSet, bool cancelled) SetHasFocusTrue (View? previousFocusedView, bool traversingUp = false) { - if (viewToEnterFocus is null) - { - return; - } + Debug.Assert (ApplicationNavigation.IsInHierarchy (SuperView, this)); - if (!viewToEnterFocus.CanFocus || !viewToEnterFocus.Visible || !viewToEnterFocus.Enabled) + // Pre-conditions + if (_hasFocus) { - return; + return (false, false); } - // If viewToEnterFocus is already the focused view, don't do anything - if (Focused?._hasFocus == true && Focused == viewToEnterFocus) + if (CanFocus && SuperView is { CanFocus: false }) { - return; - } + Debug.WriteLine ($@"WARNING: Attempt to FocusChanging where SuperView.CanFocus == false. {this}"); - // If a subview has focus and viewToEnterFocus is the focused view's superview OR viewToEnterFocus is this view, - // then make viewToEnterFocus.HasFocus = true and return - if ((Focused?._hasFocus == true && Focused?.SuperView == viewToEnterFocus) || viewToEnterFocus == this) - { - if (!viewToEnterFocus._hasFocus) - { - viewToEnterFocus._hasFocus = true; - } - - return; + return (false, false); } - // Make sure that viewToEnterFocus is a subview of this view - View c; - - for (c = viewToEnterFocus._superView; c != null; c = c._superView) + if (!CanBeVisible (this) || !Enabled) { - if (c == this) - { - break; - } + return (false, false); } - if (c is null) + if (!CanFocus) { - throw new ArgumentException (@$"The specified view {viewToEnterFocus} is not part of the hierarchy of {this}."); + return (false, false); } - // If a subview has focus, make it leave focus - Focused?.SetHasFocus (false, viewToEnterFocus); - - // make viewToEnterFocus Focused and enter focus - View f = Focused; - Focused = viewToEnterFocus; - Focused.SetHasFocus (true, f); + bool previousValue = HasFocus; - // Ensure on either the first or last focusable subview of Focused - // BUGBUG: With Groups, this means the previous focus is lost - Focused.RestoreFocus (); + bool cancelled = NotifyFocusChanging (false, true, previousFocusedView, this); - // Recursively set focus upwards in the view hierarchy - if (SuperView is { }) + if (cancelled) { - SuperView.SetFocus (this); + return (false, true); } - else + + // Make sure superviews up the superview hierarchy have focus. + // Any of them may cancel gaining focus. In which case we need to back out. + if (SuperView is { HasFocus: false } sv) { - // If there is no SuperView, then this is a top-level view - SetFocus (this); + (bool focusSet, bool svCancelled) = sv.SetHasFocusTrue (previousFocusedView, true); + if (!focusSet) + { + return (false, svCancelled); + } } - // TODO: Temporary hack to make Application.Navigation.FocusChanged work - if (HasFocus && Focused.Focused is null) + if (_hasFocus) { - Application.Navigation?.SetFocused (Focused); + // Something else beat us to the change (likely a FocusChanged handler). + return (true, false); } - // TODO: This is a temporary hack to make overlapped non-Toplevels have a zorder. See also: View.OnDrawContent. - if (viewToEnterFocus is { } && (viewToEnterFocus.TabStop == TabBehavior.TabGroup && viewToEnterFocus.Arrangement.HasFlag (ViewArrangement.Overlapped))) - { - viewToEnterFocus.TabIndex = 0; - } + // By setting _hasFocus to true we definitively change HasFocus for this view. - } + // Get whatever peer has focus, if any + View? focusedPeer = SuperView?.Focused; - /// - /// Internal API that sets . This method is called by HasFocus_set and other methods that - /// need to set or remove focus from a view. - /// - /// The new setting for . - /// The view that will be gaining or losing focus. - /// - /// to force Enter/Leave on regardless of whether it - /// already HasFocus or not. - /// - /// - /// If is and there is a focused subview ( - /// is not ), - /// this method will recursively remove focus from any focused subviews of . - /// - private void SetHasFocus (bool newHasFocus, View view, bool force = false) - { - if (HasFocus != newHasFocus || force) - { - _hasFocus = newHasFocus; + _hasFocus = true; - if (newHasFocus) - { - OnEnter (view); - } - else + // Ensure that the peer loses focus + focusedPeer?.SetHasFocusFalse (this, true); + + if (!traversingUp) + { + // Restore focus to the previously most focused subview in the subview-hierarchy + if (!RestoreFocus ()) { - OnLeave (view); + // Couldn't restore focus, so use Advance to navigate to the next focusable subview + if (!AdvanceFocus (NavigationDirection.Forward, null)) + { + // Couldn't advance, so we're the most focused view in the application + _previouslyMostFocused = null; + Application.Navigation?.SetFocused (this); + } } - - SetNeedsDisplay (); } - // Remove focus down the chain of subviews if focus is removed - if (!newHasFocus && Focused is { }) + if (previousFocusedView is { HasFocus: true } && Subviews.Contains (previousFocusedView)) { - View f = Focused; - f.OnLeave (view); - f.SetHasFocus (false, view); - Focused = null; + previousFocusedView.SetHasFocusFalse (this); } - } - #region Tab/Focus Handling + if (Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + SuperView?.MoveSubviewToStart (this); + } -#nullable enable + NotifyFocusChanged (HasFocus, previousFocusedView, this); - private List _tabIndexes; + // Post-conditions - prove correctness + if (HasFocus == previousValue) + { + throw new InvalidOperationException ("NotifyFocusChanging was not cancelled and the HasFocus value did not change."); + } - // TODO: This should be a get-only property? - // BUGBUG: This returns an AsReadOnly list, but isn't declared as such. - /// Gets a list of the subviews that are a . - /// The tabIndexes. - public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty; + return (true, false); + } - /// - /// Gets TabIndexes that are scoped to the specified behavior and direction. If behavior is null, all TabIndexes are returned. - /// - /// - /// - /// GetScopedTabIndexes - private View [] GetScopedTabIndexes (TabBehavior? behavior, NavigationDirection direction) + private bool NotifyFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) { - IEnumerable indicies; - - if (behavior.HasValue) - { - indicies = _tabIndexes.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true }); - } - else + // Call the virtual method + if (OnHasFocusChanging (currentHasFocus, newHasFocus, currentFocused, newFocused)) { - indicies = _tabIndexes.Where (v => v is { CanFocus: true, Visible: true, Enabled: true }); + // The event was cancelled + return true; } - if (direction == NavigationDirection.Backward) + var args = new HasFocusEventArgs (currentHasFocus, newHasFocus, currentFocused, newFocused); + HasFocusChanging?.Invoke (this, args); + + if (args.Cancel) { - indicies = indicies.Reverse (); + // The event was cancelled + return true; } - return indicies.ToArray (); - + return false; } - private int? _tabIndex; // null indicates the view has not yet been added to TabIndexes - private int? _oldTabIndex; - /// - /// Indicates the order of the current in list. + /// Invoked when is about to change. This method is called before the + /// event is raised. /// /// /// - /// If , the view is not part of the tab order. - /// - /// - /// On set, if is or has not TabStops, will - /// be set to 0. + /// Use to be notified after the focus has changed. /// + /// + /// The current value of . + /// The value will have if the focus change happens. + /// The view that is currently Focused. May be . + /// The view that will be focused. May be . + /// + /// , if the change to is to be cancelled, + /// otherwise. + /// + protected virtual bool OnHasFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) { return false; } + + /// + /// Raised when is about to change. + /// + /// /// - /// On set, if has only one TabStop, will be set to 0. + /// Cancel the event to prevent the focus from changing. /// /// - /// See also . + /// Use to be notified after the focus has changed. /// /// - public int? TabIndex - { - get => _tabIndex; + public event EventHandler? HasFocusChanging; - // TOOD: This should be a get-only property. Introduce SetTabIndex (int value) (or similar). - set + /// + /// Called when this view should stop being focused. + /// + /// + /// The new focused view. If it is not known which view will be + /// focused. + /// + /// + /// Set to true to indicate method is being called recurively, traversing down the focus + /// chain. + /// + /// + private void SetHasFocusFalse (View? newFocusedView, bool traversingDown = false) + { + // Pre-conditions + if (!_hasFocus) { - // Once a view is in the tab order, it should not be removed from the tab order; set TabStop to NoStop instead. - Debug.Assert (value >= 0); - Debug.Assert (value is { }); + throw new InvalidOperationException ("SetHasFocusFalse should not be called if the view does not have focus."); + } - if (SuperView?._tabIndexes is null || SuperView?._tabIndexes.Count == 1) + // If newFocusedVew is null, we need to find the view that should get focus, and SetFocus on it. + if (!traversingDown && newFocusedView is null) + { + if (SuperView?._previouslyMostFocused is { } && SuperView?._previouslyMostFocused != this) { - // BUGBUG: Property setters should set the property to the value passed in and not have side effects. - _tabIndex = 0; + SuperView?._previouslyMostFocused?.SetFocus (); + // The above will cause SetHasFocusFalse, so we can return return; } - if (_tabIndex == value && TabIndexes.IndexOf (this) == value) + if (SuperView is { } && SuperView.AdvanceFocus (NavigationDirection.Forward, TabStop)) { + // The above will cause SetHasFocusFalse, so we can return return; } - _tabIndex = value > SuperView!.TabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : - value < 0 ? 0 : value; - _tabIndex = GetGreatestTabIndexInSuperView ((int)_tabIndex); - - if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) + if (Application.Navigation is { } && Application.Current is { }) { - // BUGBUG: we have to use _tabIndexes and not TabIndexes because TabIndexes returns is a read-only version of _tabIndexes - SuperView._tabIndexes.Remove (this); - SuperView._tabIndexes.Insert ((int)_tabIndex, this); - ReorderSuperViewTabIndexes (); + // Temporarily ensure this view can't get focus + bool prevCanFocus = _canFocus; + _canFocus = false; + bool restoredFocus = Application.Current!.RestoreFocus (); + _canFocus = prevCanFocus; + + if (restoredFocus) + { + // The above caused SetHasFocusFalse, so we can return + return; + } } + + // No other focusable view to be found. Just "leave" us... } - } - /// - /// Gets the greatest of the 's that is less - /// than or equal to . - /// - /// - /// The minimum of and the 's . - private int GetGreatestTabIndexInSuperView (int idx) - { - if (SuperView is null) + // Before we can leave focus, we need to make sure that all views down the subview-hierarchy have left focus. + View? mostFocused = MostFocused; + + if (mostFocused is { } && (newFocusedView is null || mostFocused != newFocusedView)) { - return 0; + // Start at the bottom and work our way up to us + View? bottom = mostFocused; + + while (bottom is { } && bottom != this) + { + if (bottom.HasFocus) + { + bottom.SetHasFocusFalse (newFocusedView, true); + } + + bottom = bottom.SuperView; + } + + _previouslyMostFocused = mostFocused; } - var i = 0; + bool previousValue = HasFocus; + + // Note, can't be cancelled. + NotifyFocusChanging (HasFocus, !HasFocus, newFocusedView, this); + + // Get whatever peer has focus, if any + View? focusedPeer = SuperView?.Focused; + _hasFocus = false; - foreach (View superViewTabStop in SuperView._tabIndexes) + if (Application.Navigation is { }) { - if (superViewTabStop._tabIndex is null || superViewTabStop == this) + View? appFocused = Application.Navigation.GetFocused (); + + if (appFocused is { } || appFocused == this) { - continue; + Application.Navigation.SetFocused (newFocusedView ?? SuperView); } + } + + NotifyFocusChanged (HasFocus, this, newFocusedView); - i++; + if (_hasFocus) + { + // Notify caused HasFocus to change to true. + return; } - return Math.Min (i, idx); + if (SuperView is { }) + { + SuperView._previouslyMostFocused = focusedPeer; + } + + // Post-conditions - prove correctness + if (HasFocus == previousValue) + { + throw new InvalidOperationException ("SetHasFocusFalse and the HasFocus value did not change."); + } + + SetNeedsDisplay (); } /// - /// Re-orders the s of the views in the 's . + /// Caches the most focused subview when this view is losing focus. This is used by . /// - private void ReorderSuperViewTabIndexes () + private View? _previouslyMostFocused; + + private void NotifyFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) { - if (SuperView is null) - { - return; - } + // Call the virtual method + OnHasFocusChanged (newHasFocus, previousFocusedView, focusedVew); + + // Raise the event + var args = new HasFocusEventArgs (newHasFocus, newHasFocus, previousFocusedView, focusedVew); + HasFocusChanged?.Invoke (this, args); + } + + /// + /// Invoked after has changed. This method is called before the + /// event is raised. + /// + /// + /// + /// This event cannot be cancelled. + /// + /// + /// The new value of . + /// + /// The view that is now focused. May be + protected virtual void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) { } + + /// Raised after has changed. + /// + /// + /// This event cannot be cancelled. + /// + /// + public event EventHandler? HasFocusChanged; - var i = 0; + #endregion HasFocus - foreach (View superViewTabStop in SuperView._tabIndexes) + #region Tab/Focus Handling + + /// + /// Gets TabIndexes that are scoped to the specified behavior and direction. If behavior is null, all TabIndexes are + /// returned. + /// + /// + /// + /// + /// GetScopedTabIndexes + internal View [] GetSubviewFocusChain (NavigationDirection direction, TabBehavior? behavior) + { + IEnumerable? fitleredSubviews; + + if (behavior.HasValue) { - if (superViewTabStop._tabIndex is null) - { - continue; - } + fitleredSubviews = _subviews?.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true }); + } + else + { + fitleredSubviews = _subviews?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true }); + } - superViewTabStop._tabIndex = i; - i++; + if (direction == NavigationDirection.Backward) + { + fitleredSubviews = fitleredSubviews?.Reverse (); } + + return fitleredSubviews?.ToArray () ?? Array.Empty (); } private TabBehavior? _tabStop; @@ -843,10 +724,12 @@ private void ReorderSuperViewTabIndexes () /// focus even if this property is set and vice-versa. /// /// - /// The default keys are (Key.Tab) and (Key>Tab.WithShift). + /// The default keys are (Key.Tab) + /// and (Key>Tab.WithShift). /// /// - /// The default keys are (Key.F6) and (Key>Key.F6.WithShift). + /// The default keys are ( + /// Key.F6) and (Key>Key.F6.WithShift). /// /// public TabBehavior? TabStop @@ -854,19 +737,11 @@ public TabBehavior? TabStop get => _tabStop; set { - if (_tabStop == value) + if (_tabStop is { } && _tabStop == value) { return; } - Debug.Assert (value is { }); - - if (_tabStop is null && TabIndex is null) - { - // This view has not yet been added to TabIndexes (TabStop has not been set previously). - TabIndex = GetGreatestTabIndexInSuperView (SuperView is { } ? SuperView._tabIndexes.Count : 0); - } - _tabStop = value; } } diff --git a/Terminal.Gui/View/View.Text.cs b/Terminal.Gui/View/View.Text.cs index 163aa967a6..508637b567 100644 --- a/Terminal.Gui/View/View.Text.cs +++ b/Terminal.Gui/View/View.Text.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui; public partial class View // Text Property APIs { - private string _text; + private string _text = null!; /// /// Called when the has changed. Fires the event. diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 9977dc8813..f01c9c78d0 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +#nullable enable +using System.ComponentModel; using System.Diagnostics; namespace Terminal.Gui; @@ -112,11 +113,11 @@ public partial class View : Responder, ISupportInitializeNotification /// /// to cancel the event. /// - public event EventHandler Accept; + public event EventHandler? Accept; /// Gets or sets arbitrary data for the view. /// This property is not used internally. - public object Data { get; set; } + public object? Data { get; set; } /// Gets or sets an identifier for the view; /// The identifier. @@ -168,7 +169,7 @@ protected override void Dispose (bool disposing) /// Points to the current driver in use by the view, it is a convenience property for simplifying the development /// of new views. /// - public static ConsoleDriver Driver => Application.Driver; + public static ConsoleDriver Driver => Application.Driver!; /// Initializes a new instance of . /// @@ -191,7 +192,7 @@ public View () /// configurations and assignments to be performed before the being shown. /// View implements to allow for more sophisticated initialization. /// - public event EventHandler Initialized; + public event EventHandler? Initialized; /// /// Get or sets if the has been initialized (via @@ -228,9 +229,10 @@ public virtual void BeginInit () { throw new InvalidOperationException ("The view is already initialized."); } - +#if AUTO_CANFOCUS _oldCanFocus = CanFocus; _oldTabIndex = _tabIndex; +#endif BeginInitAdornments (); @@ -285,15 +287,17 @@ public virtual void EndInit () Initialized?.Invoke (this, EventArgs.Empty); } - #endregion Constructors and Initialization +#endregion Constructors and Initialization #region Visibility private bool _enabled = true; + + // This is a cache of the Enabled property so that we can restore it when the superview is re-enabled. private bool _oldEnabled; /// Gets or sets a value indicating whether this can respond to user interaction. - public virtual bool Enabled + public bool Enabled { get => _enabled; set @@ -307,7 +311,13 @@ public virtual bool Enabled if (!_enabled && HasFocus) { - SetHasFocus (false, this); + HasFocus = false; + } + + if (_enabled && CanFocus && Visible && !HasFocus + && SuperView is null or { HasFocus: true, Visible: true, Enabled: true, Focused: null }) + { + SetFocus (); } OnEnabledChanged (); @@ -328,14 +338,16 @@ public virtual bool Enabled else { view.Enabled = view._oldEnabled; +#if AUTO_CANFOCUS view._addingViewSoCanFocusAlsoUpdatesSuperView = _enabled; +#endif } } } } /// Event fired when the value is being changed. - public event EventHandler EnabledChanged; + public event EventHandler? EnabledChanged; /// Method invoked when the property from a view is changed. public virtual void OnEnabledChanged () { EnabledChanged?.Invoke (this, EventArgs.Empty); } @@ -359,13 +371,14 @@ public virtual bool Visible { if (HasFocus) { - SetHasFocus (false, this); + HasFocus = false; } + } - if (IsInitialized && ClearOnVisibleFalse) - { - Clear (); - } + if (_visible && CanFocus && Enabled && !HasFocus + && SuperView is null or { HasFocus: true, Visible: true, Enabled: true, Focused: null }) + { + SetFocus (); } OnVisibleChanged (); @@ -376,20 +389,23 @@ public virtual bool Visible /// Method invoked when the property from a view is changed. public virtual void OnVisibleChanged () { VisibleChanged?.Invoke (this, EventArgs.Empty); } - /// Gets or sets whether a view is cleared if the property is . - public bool ClearOnVisibleFalse { get; set; } = true; - /// Event fired when the value is being changed. - public event EventHandler VisibleChanged; + public event EventHandler? VisibleChanged; - private static bool CanBeVisible (View view) + // TODO: This API is a hack. We should make Visible propogate automatically, no? See https://github.com/gui-cs/Terminal.Gui/issues/3703 + /// + /// INTERNAL Indicates whether all views up the Superview hierarchy are visible. + /// + /// The view to test. + /// if `view.Visible` is or any Superview is not visible, otherwise. + internal static bool CanBeVisible (View view) { if (!view.Visible) { return false; } - for (View c = view.SuperView; c != null; c = c.SuperView) + for (View? c = view.SuperView; c != null; c = c.SuperView) { if (!c.Visible) { @@ -400,7 +416,7 @@ private static bool CanBeVisible (View view) return true; } - #endregion Visibility +#endregion Visibility #region Title @@ -463,7 +479,7 @@ public string Title SetHotKeyFromTitle (); SetNeedsDisplay (); #if DEBUG - if (_title is { } && string.IsNullOrEmpty (Id)) + if (string.IsNullOrEmpty (Id)) { Id = _title; } @@ -504,13 +520,13 @@ protected bool OnTitleChanging (ref string newTitle) } /// Event fired after the has been changed. - public event EventHandler> TitleChanged; + public event EventHandler>? TitleChanged; /// /// Event fired when the is changing. Set to `true` /// to cancel the Title change. /// - public event EventHandler> TitleChanging; + public event EventHandler>? TitleChanging; #endregion } diff --git a/Terminal.Gui/Views/Bar.cs b/Terminal.Gui/Views/Bar.cs index 0871a13f4c..d30ced79e8 100644 --- a/Terminal.Gui/Views/Bar.cs +++ b/Terminal.Gui/Views/Bar.cs @@ -1,3 +1,4 @@ +#nullable enable namespace Terminal.Gui; /// @@ -43,7 +44,7 @@ public Bar (IEnumerable shortcuts) } } - private void Bar_Initialized (object sender, EventArgs e) { ColorScheme = Colors.ColorSchemes ["Menu"]; } + private void Bar_Initialized (object? sender, EventArgs e) { ColorScheme = Colors.ColorSchemes ["Menu"]; } /// public override void SetBorderStyle (LineStyle value) @@ -72,10 +73,10 @@ public Orientation Orientation } /// - public event EventHandler> OrientationChanging; + public event EventHandler>? OrientationChanging; /// - public event EventHandler> OrientationChanged; + public event EventHandler>? OrientationChanged; /// Called when has changed. /// @@ -132,9 +133,9 @@ public void AddShortcutAt (int index, Shortcut item) /// Removes a at specified index of . /// The zero-based index of the item to remove. /// The removed. - public Shortcut RemoveShortcut (int index) + public Shortcut? RemoveShortcut (int index) { - View toRemove = null; + View? toRemove = null; for (var i = 0; i < Subviews.Count; i++) { @@ -158,7 +159,7 @@ internal override void OnLayoutStarted (LayoutEventArgs args) { base.OnLayoutStarted (args); - View prevBarItem = null; + View? prevBarItem = null; switch (Orientation) { diff --git a/Terminal.Gui/Views/ColorBar.cs b/Terminal.Gui/Views/ColorBar.cs index 8ca0e45545..54dc54cb69 100644 --- a/Terminal.Gui/Views/ColorBar.cs +++ b/Terminal.Gui/Views/ColorBar.cs @@ -121,7 +121,7 @@ protected internal override bool OnMouseEvent (MouseEvent mouseEvent) } mouseEvent.Handled = true; - FocusFirst (null); + SetFocus (); return true; } diff --git a/Terminal.Gui/Views/ColorModelStrategy.cs b/Terminal.Gui/Views/ColorModelStrategy.cs index 50a9582962..75d26da397 100644 --- a/Terminal.Gui/Views/ColorModelStrategy.cs +++ b/Terminal.Gui/Views/ColorModelStrategy.cs @@ -43,6 +43,10 @@ public Color GetColorFromBars (IList bars, ColorModel model) public void SetBarsToColor (IList bars, Color newValue, ColorModel model) { + if (bars.Count == 0) + { + return; + } switch (model) { case ColorModel.RGB: diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs index 5865e7307e..9a5bde98d9 100644 --- a/Terminal.Gui/Views/ColorPicker.cs +++ b/Terminal.Gui/Views/ColorPicker.cs @@ -1,5 +1,7 @@ #nullable enable +using System; + namespace Terminal.Gui; /// @@ -15,6 +17,7 @@ public class ColorPicker : View public ColorPicker () { CanFocus = true; + TabStop = TabBehavior.TabStop; Height = Dim.Auto (); Width = Dim.Auto (); ApplyStyleChanges (); @@ -51,17 +54,17 @@ public void ApplyStyleChanges () bar.Y = y; bar.Width = Dim.Fill (Style.ShowTextFields ? textFieldWidth : 0); + TextField? tfValue = null; if (Style.ShowTextFields) { - var tfValue = new TextField + tfValue = new TextField { X = Pos.AnchorEnd (textFieldWidth), Y = y, Width = textFieldWidth }; - tfValue.Leave += UpdateSingleBarValueFromTextField; + tfValue.HasFocusChanged += UpdateSingleBarValueFromTextField; _textFields.Add (bar, tfValue); - Add (tfValue); } y++; @@ -71,6 +74,11 @@ public void ApplyStyleChanges () _bars.Add (bar); Add (bar); + + if (tfValue is { }) + { + Add (tfValue); + } } if (Style.ShowColorName) @@ -81,7 +89,10 @@ public void ApplyStyleChanges () CreateTextField (); SelectedColor = oldValue; - LayoutSubviews (); + if (IsInitialized) + { + LayoutSubviews (); + } } /// @@ -141,7 +152,7 @@ private void CreateNameField () }; _tfName.Autocomplete = auto; - _tfName.Leave += UpdateValueFromName; + _tfName.HasFocusChanged += UpdateValueFromName; } private void CreateTextField () @@ -164,13 +175,13 @@ private void CreateTextField () { Y = y, X = 4, - Width = 8 + Width = 8, }; Add (_lbHex); Add (_tfHex); - _tfHex.Leave += UpdateValueFromTextField; + _tfHex.HasFocusChanged += UpdateValueFromTextField; } private void DisposeOldViews () @@ -181,7 +192,7 @@ private void DisposeOldViews () if (_textFields.TryGetValue (bar, out TextField? tf)) { - tf.Leave -= UpdateSingleBarValueFromTextField; + tf.HasFocusChanged -= UpdateSingleBarValueFromTextField; Remove (tf); tf.Dispose (); } @@ -203,7 +214,7 @@ private void DisposeOldViews () if (_tfHex != null) { Remove (_tfHex); - _tfHex.Leave -= UpdateValueFromTextField; + _tfHex.HasFocusChanged -= UpdateValueFromTextField; _tfHex.Dispose (); _tfHex = null; } @@ -218,7 +229,7 @@ private void DisposeOldViews () if (_tfName != null) { Remove (_tfName); - _tfName.Leave -= UpdateValueFromName; + _tfName.HasFocusChanged -= UpdateValueFromName; _tfName.Dispose (); _tfName = null; } @@ -266,8 +277,13 @@ private void SyncSubViewValues (bool syncBars) } } - private void UpdateSingleBarValueFromTextField (object? sender, FocusEventArgs e) + private void UpdateSingleBarValueFromTextField (object? sender, HasFocusEventArgs e) { + if (e.NewValue) + { + return; + } + foreach (KeyValuePair kvp in _textFields) { if (kvp.Value == sender) @@ -280,8 +296,13 @@ private void UpdateSingleBarValueFromTextField (object? sender, FocusEventArgs e } } - private void UpdateValueFromName (object? sender, FocusEventArgs e) + private void UpdateValueFromName (object? sender, HasFocusEventArgs e) { + if (e.NewValue) + { + return; + } + if (_tfName == null) { return; @@ -298,8 +319,13 @@ private void UpdateValueFromName (object? sender, FocusEventArgs e) } } - private void UpdateValueFromTextField (object? sender, FocusEventArgs e) + private void UpdateValueFromTextField (object? sender, HasFocusEventArgs e) { + if (e.NewValue) + { + return; + } + if (_tfHex == null) { return; @@ -315,4 +341,11 @@ private void UpdateValueFromTextField (object? sender, FocusEventArgs e) SyncSubViewValues (false); } } + + + protected override void Dispose (bool disposing) + { + DisposeOldViews (); + base.Dispose (disposing); + } } diff --git a/Terminal.Gui/Views/ColorPicker16.cs b/Terminal.Gui/Views/ColorPicker16.cs index a413c8633e..7312f113be 100644 --- a/Terminal.Gui/Views/ColorPicker16.cs +++ b/Terminal.Gui/Views/ColorPicker16.cs @@ -51,6 +51,7 @@ public int BoxWidth } /// Fired when a color is picked. + [CanBeNull] public event EventHandler ColorChanged; /// Cursor for the selected color. diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 20a149ea3c..bfd207aa75 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -8,6 +8,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; +using System.Threading.Channels; namespace Terminal.Gui; @@ -28,9 +29,10 @@ public class ComboBox : View, IDesignable /// Public constructor public ComboBox () { + CanFocus = true; _search = new TextField () { CanFocus = true, TabStop = TabBehavior.NoStop }; - _listview = new ComboListView (this, HideDropdownListOnClick) { CanFocus = true, TabStop = TabBehavior.NoStop}; + _listview = new ComboListView (this, HideDropdownListOnClick) { CanFocus = true, TabStop = TabBehavior.NoStop }; _search.TextChanged += Search_Changed; _search.Accept += Search_Accept; @@ -298,44 +300,41 @@ public override void OnDrawContent (Rectangle viewport) Driver.AddRune (Glyphs.DownArrow); } - /// - public override bool OnEnter (View view) - { - if (!_search.HasFocus && !_listview.HasFocus) - { - _search.SetFocus (); - } - - _search.CursorPosition = _search.Text.GetRuneCount (); - - return base.OnEnter (view); - } /// Virtual method which invokes the event. public virtual void OnExpanded () { Expanded?.Invoke (this, EventArgs.Empty); } /// - public override bool OnLeave (View view) + protected override void OnHasFocusChanged (bool newHasFocus, View previousFocusedView, View view) { - if (_source?.Count > 0 - && _selectedItem > -1 - && _selectedItem < _source.Count - 1 - && _text != _source.ToList () [_selectedItem].ToString ()) + if (newHasFocus) { - SetValue (_source.ToList () [_selectedItem].ToString ()); + if (!_search.HasFocus && !_listview.HasFocus) + { + _search.SetFocus (); + } + _search.CursorPosition = _search.Text.GetRuneCount (); } + else + { + if (_source?.Count > 0 + && _selectedItem > -1 + && _selectedItem < _source.Count - 1 + && _text != _source.ToList () [_selectedItem].ToString ()) + { + SetValue (_source.ToList () [_selectedItem].ToString ()); + } - if (_autoHide && IsShow && view != this && view != _search && view != _listview) - { - IsShow = false; - HideList (); - } - else if (_listview.TabStop?.HasFlag (TabBehavior.TabStop) ?? false) - { - _listview.TabStop = TabBehavior.NoStop; + if (_autoHide && IsShow && view != this && view != _search && view != _listview) + { + IsShow = false; + HideList (); + } + else if (_listview.TabStop?.HasFlag (TabBehavior.TabStop) ?? false) + { + _listview.TabStop = TabBehavior.NoStop; + } } - - return base.OnLeave (view); } /// Invokes the OnOpenSelectedItem event if it is defined. @@ -415,7 +414,10 @@ private int CalculateHeight () private bool CancelSelected () { - _search.SetFocus (); + if (HasFocus) + { + _search.SetFocus (); + } if (ReadOnly || HideDropdownListOnClick) { @@ -493,7 +495,7 @@ private void HideList () Reset (true); _listview.Clear (); _listview.TabStop = TabBehavior.NoStop; - SuperView?.SendSubviewToBack (this); + SuperView?.MoveSubviewToStart (this); Rectangle rect = _listview.ViewportToScreen (_listview.IsInitialized ? _listview.Viewport : Rectangle.Empty); SuperView?.SetNeedsDisplay (rect); OnCollapsed (); @@ -563,7 +565,7 @@ private void HideList () { if (HasItems ()) { - return _listview.MoveUp (); + return _listview.MoveUp (); } return false; @@ -793,7 +795,7 @@ private void ShowList () _listview.Clear (); _listview.Height = CalculateHeight (); - SuperView?.BringSubviewToFront (this); + SuperView?.MoveSubviewToStart (this); } private bool UnixEmulation () @@ -824,6 +826,7 @@ public bool HideDropdownListOnClick set => _hideDropdownListOnClick = WantContinuousButtonPressed = value; } + // BUGBUG: OnMouseEvent is internal! protected internal override bool OnMouseEvent (MouseEvent me) { var res = false; @@ -940,28 +943,26 @@ public override void OnDrawContent (Rectangle viewport) } } - public override bool OnEnter (View view) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedVew) { - if (_hideDropdownListOnClick) + if (newHasFocus) { - _isFocusing = true; - _highlighted = _container.SelectedItem; - Application.GrabMouse (this); + if (_hideDropdownListOnClick) + { + _isFocusing = true; + _highlighted = _container.SelectedItem; + Application.GrabMouse (this); + } } - - return base.OnEnter (view); - } - - public override bool OnLeave (View view) - { - if (_hideDropdownListOnClick) + else { - _isFocusing = false; - _highlighted = _container.SelectedItem; - Application.UngrabMouse (); + if (_hideDropdownListOnClick) + { + _isFocusing = false; + _highlighted = _container.SelectedItem; + Application.UngrabMouse (); + } } - - return base.OnLeave (view); } public override bool OnSelectedChanged () diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs index d93f80e038..1676f6df2d 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -13,7 +13,7 @@ namespace Terminal.Gui; public class DatePicker : View { private TableView _calendar; - private DateTime _date = DateTime.Now; + private DateTime _date; private DateField _dateField; private Label _dateLabel; private Button _nextMonthButton; @@ -21,7 +21,7 @@ public class DatePicker : View private DataTable _table; /// Initializes a new instance of . - public DatePicker () { SetInitialProperties (_date); } + public DatePicker () { SetInitialProperties (DateTime.Now); } /// Initializes a new instance of with the specified date. public DatePicker (DateTime date) { SetInitialProperties (date); } @@ -183,11 +183,12 @@ private void SelectDayOnCalendar (int day) private void SetInitialProperties (DateTime date) { + _date = date; Title = "Date Picker"; BorderStyle = LineStyle.Single; Date = date; _dateLabel = new Label { X = 0, Y = 0, Text = "Date: " }; - TabStop = TabBehavior.TabGroup; + CanFocus = true; _calendar = new TableView { diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 2547287ccd..ba45c59d7f 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -89,10 +89,11 @@ internal FileDialog (IFileSystem fileSystem) NavigateIf (k, KeyCode.CursorUp, _tableView); NavigateIf (k, KeyCode.CursorRight, _btnOk); }; - _btnCancel.Accept += (s, e) => { - Canceled = true; - Application.RequestStop (); - }; + _btnCancel.Accept += (s, e) => + { + Canceled = true; + Application.RequestStop (); + }; _btnUp = new Button { X = 0, Y = 1, NoPadding = true }; _btnUp.Text = GetUpButtonText (); @@ -290,6 +291,8 @@ internal FileDialog (IFileSystem fileSystem) UpdateNavigationVisibility (); + // BUGBUG: This TabOrder is counter-intuitive. The tab order for a dialog should match the + // order the Views' are presented, left to right, top to bottom. // Determines tab order Add (_btnToggleSplitterCollapse); Add (_tbFind); @@ -458,19 +461,6 @@ public override void OnLoaded () _btnForward.Text = GetForwardButtonText (); _btnToggleSplitterCollapse.Text = GetToggleSplitterText (false); - if (Style.FlipOkCancelButtonLayoutOrder) - { - _btnCancel.X = Pos.Func (CalculateOkButtonPosX); - _btnOk.X = Pos.Right (_btnCancel) + 1; - - // Flip tab order too for consistency - int? p1 = _btnOk.TabIndex; - int? p2 = _btnCancel.TabIndex; - - _btnOk.TabIndex = p2; - _btnCancel.TabIndex = p1; - } - _tbPath.Caption = Style.PathCaption; _tbFind.Caption = Style.SearchCaption; @@ -518,8 +508,7 @@ public override void OnLoaded () }; AllowedTypeMenuClicked (0); - _allowedTypeMenuBar.Enter += (s, e) => { _allowedTypeMenuBar.OpenMenu (0); }; - + // TODO: Using v1's menu bar here is a hack. Need to upgrade this. _allowedTypeMenuBar.DrawContentComplete += (s, e) => { _allowedTypeMenuBar.Move (e.NewViewport.Width - 1, 0); @@ -538,7 +527,7 @@ public override void OnLoaded () // to streamline user experience and allow direct typing of paths // with zero navigation we start with focus in the text box and any // default/current path fully selected and ready to be overwritten - _tbPath.FocusFirst (null); + _tbPath.SetFocus (); _tbPath.SelectAll (); if (string.IsNullOrEmpty (Title)) @@ -546,6 +535,12 @@ public override void OnLoaded () Title = GetDefaultTitle (); } + if (Style.FlipOkCancelButtonLayoutOrder) + { + _btnCancel.X = Pos.Func (CalculateOkButtonPosX); + _btnOk.X = Pos.Right (_btnCancel) + 1; + MoveSubviewTowardsStart (_btnCancel); + } LayoutSubviews (); } @@ -588,7 +583,7 @@ protected virtual string GetDefaultTitle () internal void ApplySort () { - FileSystemInfoStats [] stats = State?.Children ?? new FileSystemInfoStats[0]; + FileSystemInfoStats [] stats = State?.Children ?? new FileSystemInfoStats [0]; // This portion is never reordered (always .. at top then folders) IOrderedEnumerable forcedOrder = stats @@ -1050,7 +1045,7 @@ private bool NavigateIf (Key keyEvent, KeyCode isKey, View to) { if (keyEvent.KeyCode == isKey) { - to.FocusFirst (null); + to.FocusDeepest (NavigationDirection.Forward, null); if (to == _tbPath) { @@ -1439,7 +1434,7 @@ private bool TreeView_KeyDown (Key keyEvent) { if (_treeView.HasFocus && Separators.Contains ((char)keyEvent)) { - _tbPath.FocusFirst (null); + _tbPath.FocusDeepest (NavigationDirection.Forward, null); // let that keystroke go through on the tbPath instead return true; @@ -1546,7 +1541,7 @@ internal class SearchState : FileDialogState public SearchState (IDirectoryInfo dir, FileDialog parent, string searchTerms) : base (dir, parent) { parent.SearchMatcher.Initialize (searchTerms); - Children = new FileSystemInfoStats[0]; + Children = new FileSystemInfoStats [0]; BeginSearch (); } diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index e96eceb17d..49228ecc9b 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -760,6 +760,10 @@ private bool MoveUp (int bytes) private void RedisplayLine (long pos) { + if (bytesPerLine == 0) + { + return; + } var delta = (int)(pos - DisplayStart); int line = delta / bytesPerLine; diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 5cea4cd53f..2c15717802 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -739,14 +739,12 @@ public override void OnDrawContent (Rectangle viewport) } /// - public override bool OnEnter (View view) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View currentFocused, [CanBeNull] View newFocused) { - if (_lastSelectedItem != _selected) + if (newHasFocus && _lastSelectedItem != _selected) { EnsureSelectedItemVisible (); } - - return base.OnEnter (view); } // TODO: This should be cancelable diff --git a/Terminal.Gui/Views/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs index 1158eec3a6..6a6131195f 100644 --- a/Terminal.Gui/Views/Menu/ContextMenu.cs +++ b/Terminal.Gui/Views/Menu/ContextMenu.cs @@ -105,6 +105,10 @@ public MouseFlags MouseFlags /// Disposes the context menu object. public void Dispose () { + if (_menuBar is null) + { + return; + } _menuBar.MenuAllClosed -= MenuBar_MenuAllClosed; Application.UngrabMouse (); _menuBar?.Dispose (); diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 6bff5c9ad5..7119c28aaf 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -587,7 +587,13 @@ public void Run (Action action) _host.Run (action); } - public override bool OnLeave (View view) { return _host.OnLeave (view); } + protected override void OnHasFocusChanged (bool newHasFocus, View previousFocusedView, View view) + { + if (!newHasFocus) + { + _host.LostFocus (previousFocusedView); + } + } private void RunSelected () { diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 65ce66ecd8..766e89cdf0 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -1339,14 +1339,14 @@ private bool Select (int index) #region Mouse Handling /// - public override bool OnLeave (View view) + internal void LostFocus (View view) { - if (((!(view is MenuBar) && !(view is Menu)) || (!(view is MenuBar) && !(view is Menu) && _openMenu is { })) && !_isCleaning && !_reopen) + if (((!(view is MenuBar) && !(view is Menu))) && !_isCleaning && !_reopen) { CleanUp (); } - return base.OnLeave (view); + return; } /// diff --git a/Terminal.Gui/Views/NumericUpDown.cs b/Terminal.Gui/Views/NumericUpDown.cs index 7ca7d463ae..d250b03900 100644 --- a/Terminal.Gui/Views/NumericUpDown.cs +++ b/Terminal.Gui/Views/NumericUpDown.cs @@ -67,7 +67,7 @@ public NumericUpDown () Width = Dim.Auto (minimumContentDim: Dim.Func (() => string.Format (Format, Value).Length)), Height = 1, TextAlignment = Alignment.Center, - CanFocus = true + CanFocus = true, }; _up = new () diff --git a/Terminal.Gui/Views/ScrollBarView.cs b/Terminal.Gui/Views/ScrollBarView.cs index e8b5e487c8..ce7bd62eb0 100644 --- a/Terminal.Gui/Views/ScrollBarView.cs +++ b/Terminal.Gui/Views/ScrollBarView.cs @@ -38,9 +38,8 @@ public class ScrollBarView : View public ScrollBarView () { WantContinuousButtonPressed = true; - ClearOnVisibleFalse = false; - Added += (s, e) => CreateBottomRightCorner (e.Parent); + Added += (s, e) => CreateBottomRightCorner (e.SuperView); Initialized += ScrollBarView_Initialized; } @@ -103,7 +102,6 @@ public ScrollBarView (View host, bool isVertical, bool showBothScrollIndicator = ShowScrollIndicator = true; CreateBottomRightCorner (Host); - ClearOnVisibleFalse = false; } /// If true the vertical/horizontal scroll bars won't be showed if it's not needed. @@ -216,7 +214,7 @@ public int Position /// true if show vertical or horizontal scroll indicator; otherwise, false. public bool ShowScrollIndicator { - get => _showScrollIndicator; + get => _showScrollIndicator && Visible; set { //if (value == showScrollIndicator) { @@ -266,7 +264,7 @@ public int Size } } - private bool _showBothScrollIndicator => OtherScrollBarView?._showScrollIndicator == true && _showScrollIndicator; + private bool _showBothScrollIndicator => OtherScrollBarView?.ShowScrollIndicator == true && ShowScrollIndicator; /// This event is raised when the position on the scrollbar has changed. public event EventHandler ChangedPosition; @@ -316,7 +314,7 @@ protected internal override bool OnMouseEvent (MouseEvent mouseEvent) return true; } - if (_showScrollIndicator + if (ShowScrollIndicator && (mouseEvent.Flags == MouseFlags.WheeledDown || mouseEvent.Flags == MouseFlags.WheeledUp || mouseEvent.Flags == MouseFlags.WheeledRight @@ -448,9 +446,9 @@ protected internal override bool OnMouseEvent (MouseEvent mouseEvent) /// public override void OnDrawContent (Rectangle viewport) { - if (ColorScheme is null || ((!_showScrollIndicator || Size == 0) && AutoHideScrollBars && Visible)) + if (ColorScheme is null || ((!ShowScrollIndicator || Size == 0) && AutoHideScrollBars && Visible)) { - if ((!_showScrollIndicator || Size == 0) && AutoHideScrollBars && Visible) + if ((!ShowScrollIndicator || Size == 0) && AutoHideScrollBars && Visible) { ShowHideScrollBars (false); } @@ -696,7 +694,7 @@ private bool CheckBothScrollBars (ScrollBarView scrollBarView, bool pending = fa if (barsize == 0 || barsize >= scrollBarView._size) { - if (scrollBarView._showScrollIndicator) + if (scrollBarView.ShowScrollIndicator) { scrollBarView.ShowScrollIndicator = false; } @@ -708,7 +706,7 @@ private bool CheckBothScrollBars (ScrollBarView scrollBarView, bool pending = fa } else if (barsize > 0 && barsize == scrollBarView._size && scrollBarView.OtherScrollBarView is { } && pending) { - if (scrollBarView._showScrollIndicator) + if (scrollBarView.ShowScrollIndicator) { scrollBarView.ShowScrollIndicator = false; } @@ -747,7 +745,7 @@ private bool CheckBothScrollBars (ScrollBarView scrollBarView, bool pending = fa } } - if (!scrollBarView._showScrollIndicator) + if (!scrollBarView.ShowScrollIndicator) { scrollBarView.ShowScrollIndicator = true; } @@ -953,13 +951,13 @@ private void SetWidthHeight () ? Host != SuperView ? Dim.Height (Host) - 1 : Dim.Fill () - 1 : 1; } - else if (_showScrollIndicator) + else if (ShowScrollIndicator) { Width = _vertical ? 1 : Host != SuperView ? Dim.Width (Host) : Dim.Fill (); Height = _vertical ? Host != SuperView ? Dim.Height (Host) : Dim.Fill () : 1; } - else if (_otherScrollBarView?._showScrollIndicator == true) + else if (_otherScrollBarView?.ShowScrollIndicator == true) { _otherScrollBarView.Width = _otherScrollBarView._vertical ? 1 : Host != SuperView ? Dim.Width (Host) : Dim.Fill () - 0; @@ -1014,7 +1012,7 @@ private void ShowHideScrollBars (bool redraw = true) _otherScrollBarView._contentBottomRightCorner.Visible = true; } } - else if (!_showScrollIndicator) + else if (!ShowScrollIndicator) { if (_contentBottomRightCorner is { }) { @@ -1039,12 +1037,12 @@ private void ShowHideScrollBars (bool redraw = true) _otherScrollBarView._contentBottomRightCorner.Visible = false; } - if (Host?.Visible == true && _showScrollIndicator && !Visible) + if (Host?.Visible == true && ShowScrollIndicator && !Visible) { Visible = true; } - if (Host?.Visible == true && _otherScrollBarView?._showScrollIndicator == true && !_otherScrollBarView.Visible) + if (Host?.Visible == true && _otherScrollBarView?.ShowScrollIndicator == true && !_otherScrollBarView.Visible) { _otherScrollBarView.Visible = true; } @@ -1054,12 +1052,12 @@ private void ShowHideScrollBars (bool redraw = true) return; } - if (_showScrollIndicator) + if (ShowScrollIndicator) { Draw (); } - if (_otherScrollBarView is { } && _otherScrollBarView._showScrollIndicator) + if (_otherScrollBarView is { } && _otherScrollBarView.ShowScrollIndicator) { _otherScrollBarView.Draw (); } @@ -1078,7 +1076,6 @@ internal class ContentBottomRightCorner : View { public ContentBottomRightCorner () { - ClearOnVisibleFalse = false; ColorScheme = ColorScheme; } } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 0ce5da0724..e7b9bc5dbc 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -742,6 +742,10 @@ private void UpdateKeyBinding (Key oldKey) break; case KeyBindingScope.HotKey: + //if (!CanBeVisible(this)) + //{ + // return true; + //} cancel = base.OnAccept () == true; if (CanFocus) @@ -834,24 +838,10 @@ internal void SetColors () } } - private View _lastFocusedView; - - /// - public override bool OnEnter (View view) - { - SetColors (); - _lastFocusedView = view; - - return base.OnEnter (view); - } - /// - public override bool OnLeave (View view) + protected override void OnHasFocusChanged (bool newHasFocus, View previousFocusedView, View view) { SetColors (); - _lastFocusedView = this; - - return base.OnLeave (view); } #endregion Focus diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 272db95279..e88ec14bd3 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace Terminal.Gui; /// Control that hosts multiple sub views, presenting a single one at once. @@ -25,13 +27,12 @@ public class TabView : View public TabView () { CanFocus = true; - TabStop = TabBehavior.TabStop; + TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup _tabsBar = new TabRowView (this); _contentView = new View () { - Id = "TabView._contentView" + //Id = "TabView._contentView", }; - ApplyStyleChanges (); base.Add (_tabsBar); @@ -64,42 +65,6 @@ public TabView () } ); - AddCommand ( - Command.NextView, - () => - { - if (Style.TabsOnBottom) - { - return false; - } - - if (_contentView is { HasFocus: false }) - { - _contentView.SetFocus (); - - return _contentView.Focused is { }; - } - - return false; - } - ); - - AddCommand (Command.PreviousView, () => - { - if (!Style.TabsOnBottom) - { - return false; - } - if (_contentView is { HasFocus: false }) - { - _contentView.SetFocus (); - - return _contentView.Focused is { }; - } - - return false; - }); - AddCommand ( Command.PageDown, () => @@ -127,8 +92,6 @@ public TabView () KeyBindings.Add (Key.CursorRight, Command.Right); KeyBindings.Add (Key.Home, Command.LeftHome); KeyBindings.Add (Key.End, Command.RightEnd); - KeyBindings.Add (Key.CursorDown, Command.NextView); - KeyBindings.Add (Key.CursorUp, Command.PreviousView); KeyBindings.Add (Key.PageDown, Command.PageDown); KeyBindings.Add (Key.PageUp, Command.PageUp); } @@ -167,9 +130,12 @@ public Tab SelectedTab if (_selectedTab.View is { }) { _contentView.Add (_selectedTab.View); + // _contentView.Id = $"_contentView for {_selectedTab.DisplayText}"; } } + _contentView.CanFocus = _contentView.Subviews.Count (v => v.CanFocus) > 0; + EnsureSelectedTabIsVisible (); if (old != value) @@ -267,7 +233,7 @@ public void ApplyStyleChanges () int tabHeight = GetTabHeight (true); //move content down to make space for tabs - _contentView.Y = Pos.Bottom (_tabsBar); + _contentView.Y = Pos.Bottom (_tabsBar) ; // Fill client area leaving space at bottom for border _contentView.Height = Dim.Fill (); @@ -439,7 +405,10 @@ protected override void Dispose (bool disposing) } /// Raises the event. - protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) { SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab)); } + protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) + { + SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab)); + } /// Returns which tabs to render at each x location. /// @@ -539,7 +508,10 @@ private int GetTabHeight (bool top) return Style.ShowTopLine ? 3 : 2; } - private void Tab_MouseClick (object sender, MouseEventEventArgs e) { e.Handled = _tabsBar.NewMouseEvent (e.MouseEvent) == true; } + private void Tab_MouseClick (object sender, MouseEventEventArgs e) + { + e.Handled = _tabsBar.NewMouseEvent (e.MouseEvent) == true; + } private void UnSetCurrentTabs () { @@ -568,9 +540,9 @@ private class TabRowView : View public TabRowView (TabView host) { _host = host; + Id = "tabRowView"; CanFocus = true; - TabStop = TabBehavior.TabStop; Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize (). Width = Dim.Fill (); @@ -1349,7 +1321,7 @@ private void RenderUnderline () _leftScrollIndicator.Visible = true; // Ensures this is clicked instead of the first tab - BringSubviewToFront (_leftScrollIndicator); + MoveSubviewToEnd (_leftScrollIndicator); _leftScrollIndicator.Draw (); } else @@ -1367,7 +1339,7 @@ private void RenderUnderline () _rightScrollIndicator.Visible = true; // Ensures this is clicked instead of the last tab if under this - BringSubviewToFront (_rightScrollIndicator); + MoveSubviewToStart (_rightScrollIndicator); _rightScrollIndicator.Draw (); } else diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 464451cb3b..908012a3b7 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -39,10 +39,17 @@ public TextField () Used = true; WantMousePositionReports = true; + // By default, disable hotkeys (in case someome sets Title) + HotKeySpecifier = new ('\xffff'); + _historyText.ChangeText += HistoryText_ChangeText; Initialized += TextField_Initialized; + Added += TextField_Added; + + Removed += TextField_Removed; + // Things this view knows how to do AddCommand ( Command.DeleteCharRight, @@ -134,7 +141,7 @@ public TextField () } ); - AddCommand (Command.Left, () => MoveLeft ()); + AddCommand (Command.Left, () => MoveLeft ()); AddCommand ( Command.RightEnd, @@ -405,6 +412,7 @@ public TextField () KeyBindings.Add (Key.Enter, Command.Accept); } + /// /// Provides autocomplete context menu based on suggestions at the current cursor position. Configure /// to enable this feature. @@ -1033,7 +1041,7 @@ public override void OnDrawContent (Rectangle viewport) } /// - public override bool OnLeave (View view) + protected override void OnHasFocusChanged (bool newHasFocus, View previousFocusedView, View view) { if (Application.MouseGrabView is { } && Application.MouseGrabView == this) { @@ -1043,7 +1051,7 @@ public override bool OnLeave (View view) //if (SelectedLength != 0 && !(Application.MouseGrabView is MenuBar)) // ClearAllSelection (); - return base.OnLeave (view); + return; } /// TODO: Flush out these docs @@ -1855,6 +1863,20 @@ private void ShowContextMenu () ContextMenu.Show (); } + private void TextField_Added (object sender, SuperViewChangedEventArgs e) + { + if (Autocomplete.HostControl is null) + { + Autocomplete.HostControl = this; + Autocomplete.PopupInsideContainer = false; + } + } + + private void TextField_Removed (object sender, SuperViewChangedEventArgs e) + { + Autocomplete.HostControl = null; + } + private void TextField_Initialized (object sender, EventArgs e) { _cursorPosition = Text.GetRuneCount (); @@ -1864,8 +1886,11 @@ private void TextField_Initialized (object sender, EventArgs e) ScrollOffset = _cursorPosition > Viewport.Width + 1 ? _cursorPosition - Viewport.Width + 1 : 0; } - Autocomplete.HostControl = this; - Autocomplete.PopupInsideContainer = false; + if (Autocomplete.HostControl is null) + { + Autocomplete.HostControl = this; + Autocomplete.PopupInsideContainer = false; + } } } diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 93c25b0f0b..23f27f144c 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1997,11 +1997,16 @@ public TextView () CursorVisibility = CursorVisibility.Default; Used = true; + // By default, disable hotkeys (in case someome sets Title) + HotKeySpecifier = new ('\xffff'); + _model.LinesLoaded += Model_LinesLoaded!; _historyText.ChangeText += HistoryText_ChangeText!; Initialized += TextView_Initialized!; + Added += TextView_Added!; + LayoutComplete += TextView_LayoutComplete; // Things this view knows how to do @@ -2499,6 +2504,11 @@ public TextView () KeyBindings.Add ((KeyCode)ContextMenu.Key, KeyBindingScope.HotKey, Command.ShowContextMenu); } + private void TextView_Added1 (object? sender, SuperViewChangedEventArgs e) + { + throw new NotImplementedException (); + } + // BUGBUG: AllowsReturn is mis-named. It should be EnterKeyAccepts. /// /// Gets or sets whether pressing ENTER in a creates a new line of text @@ -3650,14 +3660,14 @@ public override bool OnKeyUp (Key key) } /// - public override bool OnLeave (View view) + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) { if (Application.MouseGrabView is { } && Application.MouseGrabView == this) { Application.UngrabMouse (); } - return base.OnLeave (view); + return; } /// @@ -6334,9 +6344,21 @@ private string StringFromRunes (List cells) return StringExtensions.ToString (encoded); } + private void TextView_Added (object sender, SuperViewChangedEventArgs e) + { + if (Autocomplete.HostControl is null) + { + Autocomplete.HostControl = this; + } + } + + private void TextView_Initialized (object sender, EventArgs e) { - Autocomplete.HostControl = this; + if (Autocomplete.HostControl is null) + { + Autocomplete.HostControl = this; + } OnContentsChanged (); } diff --git a/Terminal.Gui/Views/Tile.cs b/Terminal.Gui/Views/Tile.cs index 5224db8b4b..89149f0ec8 100644 --- a/Terminal.Gui/Views/Tile.cs +++ b/Terminal.Gui/Views/Tile.cs @@ -14,7 +14,12 @@ public class Tile /// Creates a new instance of the class. public Tile () { - ContentView = new View { Width = Dim.Fill (), Height = Dim.Fill () }; + ContentView = new View + { + Width = Dim.Fill (), + Height = Dim.Fill (), + CanFocus = true + }; #if DEBUG_IDISPOSABLE ContentView.Data = "Tile.ContentView"; #endif diff --git a/Terminal.Gui/Views/TileView.cs b/Terminal.Gui/Views/TileView.cs index cc27c00b3b..c6e010d5d7 100644 --- a/Terminal.Gui/Views/TileView.cs +++ b/Terminal.Gui/Views/TileView.cs @@ -10,14 +10,20 @@ public class TileView : View private List _splitterDistances; private List _splitterLines; private List _tiles; - private TileView parentTileView; + private TileView _parentTileView; /// Creates a new instance of the class with 2 tiles (i.e. left and right). - public TileView () : this (2) { } + public TileView () : this (2) + { + } /// Creates a new instance of the class with number of tiles. /// - public TileView (int tiles) { RebuildForTileCount (tiles); } + public TileView (int tiles) + { + CanFocus = true; + RebuildForTileCount (tiles); + } /// The line style to use when drawing the splitter lines. public LineStyle LineStyle { get; set; } = LineStyle.None; @@ -57,7 +63,7 @@ public Orientation Orientation /// /// Use to determine if the returned value is the root. /// - public TileView GetParentTileView () { return parentTileView; } + public TileView GetParentTileView () { return _parentTileView; } /// /// Returns the index of the first in which contains @@ -116,6 +122,7 @@ public Tile InsertTile (int idx) // restore old Tile and View _tiles [i] = oldTile; + _tiles [i].ContentView.TabStop = TabStop; Add (_tiles [i].ContentView); } else @@ -146,7 +153,7 @@ public Tile InsertTile (int idx) /// if you want to subdivide a . /// /// - public bool IsRootTileView () { return parentTileView == null; } + public bool IsRootTileView () { return _parentTileView == null; } /// public override void LayoutSubviews () @@ -354,6 +361,7 @@ public void RebuildForTileCount (int count) var tile = new Tile (); _tiles.Add (tile); + tile.ContentView.Id = $"Tile.ContentView {i}"; Add (tile.ContentView); tile.TitleChanged += (s, e) => SetNeedsDisplay (); } @@ -475,7 +483,7 @@ public bool TrySplitTile (int idx, int numberOfPanels, out TileView result) var newContainer = new TileView (numberOfPanels) { - Width = Dim.Fill (), Height = Dim.Fill (), parentTileView = this + Width = Dim.Fill (), Height = Dim.Fill (), _parentTileView = this }; // Take everything out of the View we are moving @@ -583,9 +591,9 @@ private TileView GetRootTileView () { TileView root = this; - while (root.parentTileView is { }) + while (root._parentTileView is { }) { - root = root.parentTileView; + root = root._parentTileView; } return root; diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index dcab31e11d..d4dc3e1cfd 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -76,13 +76,6 @@ public Toplevel () /// Gets the latest added into this Toplevel. public StatusBar? StatusBar => (StatusBar?)Subviews?.LastOrDefault (s => s is StatusBar); - /// - public override View Add (View view) - { - CanFocus = true; - - return base.Add (view); - } // TODO: Overlapped - Rename to AllSubviewsClosed - Move to View? /// @@ -355,16 +348,6 @@ public override void OnDrawContent (Rectangle viewport) #endregion - #region Navigation - - /// - public override bool OnEnter (View view) { return MostFocused?.OnEnter (view) ?? base.OnEnter (view); } - - /// - public override bool OnLeave (View view) { return MostFocused?.OnLeave (view) ?? base.OnLeave (view); } - - #endregion - #region Size / Position Management // TODO: Make cancelable? @@ -375,11 +358,6 @@ public override void OnDrawContent (Rectangle viewport) { if (!IsOverlappedContainer) { - if (Focused is null) - { - RestoreFocus (); - } - return null; } diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index 5fbde234c1..0468dd8738 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -34,6 +34,8 @@ public class TreeView : TreeView /// public TreeView () { + CanFocus = true; + TreeBuilder = new TreeNodeBuilder (); AspectGetter = o => o is null ? "Null" : o.Text ?? o?.ToString () ?? "Unnamed Node"; } @@ -975,6 +977,7 @@ public void GoToFirst () /// public bool IsSelected (T model) { return Equals (SelectedObject, model) || (MultiSelect && multiSelectedRegions.Any (s => s.Contains (model))); } + // BUGBUG: OnMouseEvent is internal. TreeView should not be overriding. /// protected internal override bool OnMouseEvent (MouseEvent me) { @@ -1155,14 +1158,16 @@ public override void OnDrawContent (Rectangle viewport) } /// - public override bool OnEnter (View view) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View currentFocused, [CanBeNull] View newFocused) { - if (SelectedObject is null && Objects.Any ()) + if (newHasFocus) { - SelectedObject = Objects.First (); + // If there is no selected object and there are objects in the tree, select the first one + if (SelectedObject is null && Objects.Any ()) + { + SelectedObject = Objects.First (); + } } - - return base.OnEnter (view); } /// diff --git a/UICatalog/KeyBindingsDialog.cs b/UICatalog/KeyBindingsDialog.cs index 4fbfad24a2..0701266e93 100644 --- a/UICatalog/KeyBindingsDialog.cs +++ b/UICatalog/KeyBindingsDialog.cs @@ -60,7 +60,7 @@ public KeyBindingsDialog () AddButton (cancel); // Register event handler as the last thing in constructor to prevent early calls - // before it is even shown (e.g. OnEnter) + // before it is even shown (e.g. OnHasFocusChanging) _commandsListView.SelectedItemChanged += CommandsListView_SelectedItemChanged; // Setup to show first ListView entry @@ -209,7 +209,7 @@ private void RecordView (View view) // (and always was wrong). Parents don't get to be told when new views are added // to them - view.Added += (s, e) => RecordView (e.Child); + view.Added += (s, e) => RecordView (e.SubView); } } } diff --git a/UICatalog/Scenarios/ASCIICustomButton.cs b/UICatalog/Scenarios/ASCIICustomButton.cs index 8d25f94709..62442d8a78 100644 --- a/UICatalog/Scenarios/ASCIICustomButton.cs +++ b/UICatalog/Scenarios/ASCIICustomButton.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; +using JetBrains.Annotations; using Terminal.Gui; namespace UICatalog.Scenarios; @@ -110,27 +111,19 @@ public void CustomInitialize () Add (_border, _fill, title); } - public override bool OnEnter (View view) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedVew) { - _border.Visible = false; - _fill.Visible = true; - PointerEnter.Invoke (this); - view = this; - - return base.OnEnter (view); - } - - public override bool OnLeave (View view) - { - _border.Visible = true; - _fill.Visible = false; - - if (view == null) + if (newHasFocus) { - view = this; + _border.Visible = false; + _fill.Visible = true; + PointerEnter?.Invoke (this); + } + else + { + _border.Visible = true; + _fill.Visible = false; } - - return base.OnLeave (view); } public event Action PointerEnter; diff --git a/UICatalog/Scenarios/AdornmentEditor.cs b/UICatalog/Scenarios/AdornmentEditor.cs index 8edd97f96f..4a5a7a38dd 100644 --- a/UICatalog/Scenarios/AdornmentEditor.cs +++ b/UICatalog/Scenarios/AdornmentEditor.cs @@ -91,7 +91,8 @@ public AdornmentEditor () BorderStyle = LineStyle.Dashed; Initialized += AdornmentEditor_Initialized; - TabStop = TabBehavior.TabGroup; + CanFocus = true; + TabStop = TabBehavior.TabStop; } private void AdornmentEditor_Initialized (object sender, EventArgs e) diff --git a/UICatalog/Scenarios/Adornments.cs b/UICatalog/Scenarios/Adornments.cs index 0f46fedd49..26d78a05a6 100644 --- a/UICatalog/Scenarios/Adornments.cs +++ b/UICatalog/Scenarios/Adornments.cs @@ -4,7 +4,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Adornments Demo", "Demonstrates Margin, Border, and Padding on Views.")] [ScenarioCategory ("Layout")] -[ScenarioCategory ("Borders")] +[ScenarioCategory ("Adornments")] public class Adornments : Scenario { public override void Main () @@ -19,11 +19,13 @@ public override void Main () var editor = new AdornmentsEditor { AutoSelectViewToEdit = true, + // This is for giggles, to show that the editor can be moved around. Arrangement = ViewArrangement.Movable, - X = Pos.AnchorEnd(), + X = Pos.AnchorEnd () }; - editor.Border.Thickness = new Thickness (1, 2, 1, 1); + + editor.Border.Thickness = new (1, 2, 1, 1); app.Add (editor); @@ -31,7 +33,8 @@ public override void Main () { Title = "The _Window", Arrangement = ViewArrangement.Movable, - // X = Pos.Center (), + + // X = Pos.Center (), Width = Dim.Percent (60), Height = Dim.Percent (80) }; @@ -127,6 +130,10 @@ public override void Main () #endif }; + editor.AutoSelectViewToEdit = true; + editor.AutoSelectSuperView = window; + editor.AutoSelectAdornments = true; + Application.Run (app); app.Dispose (); diff --git a/UICatalog/Scenarios/AdornmentsEditor.cs b/UICatalog/Scenarios/AdornmentsEditor.cs index 59e283b975..2bfecec0f1 100644 --- a/UICatalog/Scenarios/AdornmentsEditor.cs +++ b/UICatalog/Scenarios/AdornmentsEditor.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Text; using Terminal.Gui; @@ -19,33 +20,43 @@ public AdornmentsEditor () //SuperViewRendersLineCanvas = true; + CanFocus = true; + TabStop = TabBehavior.TabGroup; - //Application.MouseEvent += Application_MouseEvent; - Application.Navigation!.FocusedChanged += ApplicationNavigationOnFocusedChanged; Initialized += AdornmentsEditor_Initialized; } private readonly ViewDiagnosticFlags _savedDiagnosticFlags = Diagnostics; - private View _viewToEdit; + private View? _viewToEdit; - private Label _lblView; // Text describing the vi + private Label? _lblView; // Text describing the vi - private MarginEditor _marginEditor; - private BorderEditor _borderEditor; - private PaddingEditor _paddingEditor; + private MarginEditor? _marginEditor; + private BorderEditor? _borderEditor; + private PaddingEditor? _paddingEditor; // TODO: Move Diagnostics to a separate Editor class (DiagnosticsEditor?). - private CheckBox _diagPaddingCheckBox; - private CheckBox _diagRulerCheckBox; + private CheckBox? _diagPaddingCheckBox; + private CheckBox? _diagRulerCheckBox; /// - /// Gets or sets whether the AdornmentsEditor should automatically select the View to edit when the mouse is clicked - /// anywhere outside the editor. + /// Gets or sets whether the AdornmentsEditor should automatically select the View to edit + /// based on the values of and . /// public bool AutoSelectViewToEdit { get; set; } - public View ViewToEdit + /// + /// Gets or sets the View that will scope the behavior of . + /// + public View? AutoSelectSuperView { get; set; } + + /// + /// Gets or sets whether auto select with the mouse will select Adornments or just Views. + /// + public bool AutoSelectAdornments { get; set; } + + public View? ViewToEdit { get => _viewToEdit; set @@ -57,11 +68,66 @@ public View ViewToEdit _viewToEdit = value; - _marginEditor.AdornmentToEdit = _viewToEdit?.Margin ?? null; - _borderEditor.AdornmentToEdit = _viewToEdit?.Border ?? null; - _paddingEditor.AdornmentToEdit = _viewToEdit?.Padding ?? null; + if (_viewToEdit is not Adornment) + { + _marginEditor!.AdornmentToEdit = _viewToEdit?.Margin ?? null; + _borderEditor!.AdornmentToEdit = _viewToEdit?.Border ?? null; + _paddingEditor!.AdornmentToEdit = _viewToEdit?.Padding ?? null; + } + + if (_lblView is { }) + { + _lblView.Text = $"{_viewToEdit?.GetType ().Name}: {_viewToEdit?.Id}" ?? string.Empty; + } + } + } + + + private void NavigationOnFocusedChanged (object? sender, EventArgs e) + { + if (AutoSelectSuperView is null) + { + return; + } + + if (ApplicationNavigation.IsInHierarchy (this, Application.Navigation!.GetFocused ())) + { + return; + } - _lblView.Text = $"{_viewToEdit?.GetType ().Name}: {_viewToEdit?.Id}" ?? string.Empty; + if (!ApplicationNavigation.IsInHierarchy (AutoSelectSuperView, Application.Navigation!.GetFocused ())) + { + return; + } + + ViewToEdit = Application.Navigation!.GetFocused (); + } + + private void ApplicationOnMouseEvent (object? sender, MouseEvent e) + { + if (e.Flags != MouseFlags.Button1Clicked || !AutoSelectViewToEdit) + { + return; + } + + if ((AutoSelectSuperView is { } && !AutoSelectSuperView.FrameToScreen ().Contains (e.Position)) + || FrameToScreen ().Contains (e.Position)) + { + return; + } + + View view = e.View; + + if (view is { }) + { + if (view is Adornment adornment) + { + ViewToEdit = AutoSelectAdornments ? adornment : adornment.Parent; + } + else + { + ViewToEdit = view; + } } } @@ -72,7 +138,7 @@ protected override void Dispose (bool disposing) base.Dispose (disposing); } - private void AdornmentsEditor_Initialized (object sender, EventArgs e) + private void AdornmentsEditor_Initialized (object? sender, EventArgs e) { BorderStyle = LineStyle.Dotted; @@ -154,45 +220,8 @@ private void AdornmentsEditor_Initialized (object sender, EventArgs e) Add (_diagRulerCheckBox); _diagRulerCheckBox.Y = Pos.Bottom (_diagPaddingCheckBox); - } - - private void Application_MouseEvent (object sender, MouseEvent e) - { - if (!AutoSelectViewToEdit || FrameToScreen ().Contains (e.Position)) - { - return; - } - - // TODO: Add a setting (property) so only subviews of a specified view are considered. - View view = e.View; - if (view is { } && e.Flags == MouseFlags.Button1Clicked) - { - if (view is Adornment adornment) - { - ViewToEdit = adornment.Parent; - } - else - { - ViewToEdit = view; - } - } - } - - private void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e) - { - if (ApplicationNavigation.IsInHierarchy (this, Application.Navigation!.GetFocused ())) - { - return; - } - - if (Application.Navigation!.GetFocused () is Adornment adornment) - { - ViewToEdit = adornment.Parent; - } - else - { - ViewToEdit = Application.Navigation.GetFocused (); - } + Application.MouseEvent += ApplicationOnMouseEvent; + Application.Navigation!.FocusedChanged += NavigationOnFocusedChanged; } } diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index ed5d74bf60..b744aae2fb 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -11,7 +11,8 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("All Views Tester", "Provides a test UI for all classes derived from View.")] [ScenarioCategory ("Layout")] [ScenarioCategory ("Tests")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Controls")] +[ScenarioCategory ("Adornments")] public class AllViewsTester : Scenario { private readonly List _dimNames = new () { "Auto", "Percent", "Fill", "Absolute" }; @@ -68,7 +69,7 @@ public override void Main () Y = 0, Width = Dim.Auto (DimAutoStyle.Content), Height = Dim.Fill (), - CanFocus = false, + CanFocus = true, ColorScheme = Colors.ColorSchemes ["TopLevel"], Title = "Classes" }; @@ -84,7 +85,6 @@ public override void Main () SelectedItem = 0, Source = new ListWrapper (new (_viewClasses.Keys.ToList ())) }; - _classListView.OpenSelectedItem += (s, a) => { _settingsPane.SetFocus (); }; _classListView.SelectedItemChanged += (s, args) => { @@ -95,10 +95,19 @@ public override void Main () _hostPane.Remove (_curView); _curView.Dispose (); _curView = null; - _hostPane.Clear (); } _curView = CreateClass (_viewClasses.Values.ToArray () [_classListView.SelectedItem]); + // Add + _hostPane.Add (_curView); + + // Force ViewToEdit to be the view and not a subview + if (_adornmentsEditor is { }) + { + _adornmentsEditor.AutoSelectSuperView = _curView; + + _adornmentsEditor.ViewToEdit = _curView; + } }; _leftPane.Add (_classListView); @@ -109,7 +118,9 @@ public override void Main () Width = Dim.Auto (), Height = Dim.Fill (), ColorScheme = Colors.ColorSchemes ["TopLevel"], - BorderStyle = LineStyle.Single + BorderStyle = LineStyle.Single, + AutoSelectViewToEdit = true, + AutoSelectAdornments = false, }; var expandButton = new ExpanderButton @@ -125,7 +136,7 @@ public override void Main () Y = 0, // for menu Width = Dim.Fill (), Height = Dim.Auto (), - CanFocus = false, + CanFocus = true, ColorScheme = Colors.ColorSchemes ["TopLevel"], Title = "Settings" }; @@ -138,14 +149,15 @@ public override void Main () Y = 0, Height = Dim.Auto (), Width = Dim.Auto (), - Title = "Location (Pos)" + Title = "Location (Pos)", + TabStop = TabBehavior.TabStop, }; _settingsPane.Add (_locationFrame); var label = new Label { X = 0, Y = 0, Text = "X:" }; _locationFrame.Add (label); _xRadioGroup = new () { X = 0, Y = Pos.Bottom (label), RadioLabels = radioItems }; - _xRadioGroup.SelectedItemChanged += (s, selected) => DimPosChanged (_curView); + _xRadioGroup.SelectedItemChanged += OnXRadioGroupOnSelectedItemChanged; _xText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{_xVal}" }; _xText.Accept += (s, args) => @@ -179,7 +191,7 @@ public override void Main () }; _locationFrame.Add (_yText); _yRadioGroup = new () { X = Pos.X (label), Y = Pos.Bottom (label), RadioLabels = radioItems }; - _yRadioGroup.SelectedItemChanged += (s, selected) => DimPosChanged (_curView); + _yRadioGroup.SelectedItemChanged += OnYRadioGroupOnSelectedItemChanged; _locationFrame.Add (_yRadioGroup); _sizeFrame = new () @@ -188,14 +200,15 @@ public override void Main () Y = Pos.Y (_locationFrame), Height = Dim.Auto (), Width = Dim.Auto (), - Title = "Size (Dim)" + Title = "Size (Dim)", + TabStop = TabBehavior.TabStop, }; radioItems = new [] { "Auto", "_Percent(width)", "_Fill(width)", "A_bsolute(width)" }; label = new () { X = 0, Y = 0, Text = "Width:" }; _sizeFrame.Add (label); _wRadioGroup = new () { X = 0, Y = Pos.Bottom (label), RadioLabels = radioItems }; - _wRadioGroup.SelectedItemChanged += (s, selected) => DimPosChanged (_curView); + _wRadioGroup.SelectedItemChanged += OnWRadioGroupOnSelectedItemChanged; _wText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{_wVal}" }; _wText.Accept += (s, args) => @@ -255,7 +268,7 @@ public override void Main () _sizeFrame.Add (_hText); _hRadioGroup = new () { X = Pos.X (label), Y = Pos.Bottom (label), RadioLabels = radioItems }; - _hRadioGroup.SelectedItemChanged += (s, selected) => DimPosChanged (_curView); + _hRadioGroup.SelectedItemChanged += OnHRadioGroupOnSelectedItemChanged; _sizeFrame.Add (_hRadioGroup); _settingsPane.Add (_sizeFrame); @@ -308,18 +321,29 @@ public override void Main () Y = Pos.Bottom (_settingsPane), Width = Dim.Fill (), Height = Dim.Fill (), // + 1 for status bar + CanFocus = true, + TabStop = TabBehavior.TabGroup, ColorScheme = Colors.ColorSchemes ["Dialog"] }; app.Add (_leftPane, _adornmentsEditor, _settingsPane, _hostPane); _classListView.SelectedItem = 0; + _leftPane.SetFocus (); Application.Run (app); app.Dispose (); Application.Shutdown (); } + private void OnHRadioGroupOnSelectedItemChanged (object s, SelectedItemChangedArgs selected) { DimPosChanged (_curView); } + + private void OnWRadioGroupOnSelectedItemChanged (object s, SelectedItemChangedArgs selected) { DimPosChanged (_curView); } + + private void OnYRadioGroupOnSelectedItemChanged (object s, SelectedItemChangedArgs selected) { DimPosChanged (_curView); } + + private void OnXRadioGroupOnSelectedItemChanged (object s, SelectedItemChangedArgs selected) { DimPosChanged (_curView); } + // TODO: Add Command.HotKey handler (pop a message box?) private View CreateClass (Type type) { @@ -342,12 +366,6 @@ private View CreateClass (Type type) // Instantiate view var view = (View)Activator.CreateInstance (type); - // Set the colorscheme to make it stand out if is null by default - if (view.ColorScheme == null) - { - view.ColorScheme = Colors.ColorSchemes ["Base"]; - } - if (view is IDesignable designable) { designable.EnableForDesign (ref _demoText); @@ -370,10 +388,6 @@ private View CreateClass (Type type) view.Initialized += View_Initialized; - // Add - _hostPane.Add (view); - _hostPane.SetNeedsDisplay (); - return view; } @@ -387,40 +401,40 @@ private void DimPosChanged (View view) try { view.X = _xRadioGroup.SelectedItem switch - { - 0 => Pos.Percent (_xVal), - 1 => Pos.AnchorEnd (), - 2 => Pos.Center (), - 3 => Pos.Absolute (_xVal), - _ => view.X - }; + { + 0 => Pos.Percent (_xVal), + 1 => Pos.AnchorEnd (), + 2 => Pos.Center (), + 3 => Pos.Absolute (_xVal), + _ => view.X + }; view.Y = _yRadioGroup.SelectedItem switch - { - 0 => Pos.Percent (_yVal), - 1 => Pos.AnchorEnd (), - 2 => Pos.Center (), - 3 => Pos.Absolute (_yVal), - _ => view.Y - }; + { + 0 => Pos.Percent (_yVal), + 1 => Pos.AnchorEnd (), + 2 => Pos.Center (), + 3 => Pos.Absolute (_yVal), + _ => view.Y + }; view.Width = _wRadioGroup.SelectedItem switch - { - 0 => Dim.Auto (), - 1 => Dim.Percent (_wVal), - 2 => Dim.Fill (_wVal), - 3 => Dim.Absolute (_wVal), - _ => view.Width - }; + { + 0 => Dim.Auto (), + 1 => Dim.Percent (_wVal), + 2 => Dim.Fill (_wVal), + 3 => Dim.Absolute (_wVal), + _ => view.Width + }; view.Height = _hRadioGroup.SelectedItem switch - { - 0 => Dim.Auto (), - 1 => Dim.Percent (_hVal), - 2 => Dim.Fill (_hVal), - 3 => Dim.Absolute (_hVal), - _ => view.Height - }; + { + 0 => Dim.Auto (), + 1 => Dim.Percent (_hVal), + 2 => Dim.Fill (_hVal), + 3 => Dim.Absolute (_hVal), + _ => view.Height + }; } catch (Exception e) { @@ -476,12 +490,8 @@ private void LayoutCompleteHandler (object sender, LayoutEventArgs args) UpdateTitle (_curView); } - private void Quit () { Application.RequestStop (); } - private void UpdateSettings (View view) { - _adornmentsEditor.ViewToEdit = view; - var x = view.X.ToString (); var y = view.Y.ToString (); @@ -493,7 +503,7 @@ private void UpdateSettings (View view) catch (InvalidOperationException e) { // This is a hack to work around the fact that the Pos enum doesn't have an "Align" value yet - Debug.WriteLine($"{e}"); + Debug.WriteLine ($"{e}"); } _xText.Text = $"{view.Frame.X}"; @@ -546,7 +556,16 @@ private void View_Initialized (object sender, EventArgs e) view.Height = Dim.Fill (); } + _xRadioGroup.SelectedItemChanged -= OnXRadioGroupOnSelectedItemChanged; + _yRadioGroup.SelectedItemChanged -= OnYRadioGroupOnSelectedItemChanged; + _hRadioGroup.SelectedItemChanged -= OnHRadioGroupOnSelectedItemChanged; + _wRadioGroup.SelectedItemChanged -= OnWRadioGroupOnSelectedItemChanged; UpdateSettings (view); + _xRadioGroup.SelectedItemChanged += OnXRadioGroupOnSelectedItemChanged; + _yRadioGroup.SelectedItemChanged += OnYRadioGroupOnSelectedItemChanged; + _hRadioGroup.SelectedItemChanged += OnHRadioGroupOnSelectedItemChanged; + _wRadioGroup.SelectedItemChanged += OnWRadioGroupOnSelectedItemChanged; + UpdateTitle (view); } } diff --git a/UICatalog/Scenarios/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs index d1ce6b825c..3522f059ea 100644 --- a/UICatalog/Scenarios/BackgroundWorkerCollection.cs +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -10,7 +10,8 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("BackgroundWorker Collection", "A persisting multi Toplevel BackgroundWorker threading")] [ScenarioCategory ("Threading")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] +[ScenarioCategory ("Runnable")] [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Controls")] public class BackgroundWorkerCollection : Scenario @@ -77,7 +78,7 @@ public OverlappedMain () () => Quit (), null, null, - (KeyCode)Application.QuitKey + Application.QuitKey ) } ), @@ -281,7 +282,7 @@ public StagingUIController () _listView = new ListView { X = 0, Y = 2, Width = Dim.Fill (), Height = Dim.Fill (2), Enabled = false }; Add (_listView); - _start = new Button { Text = "Start", IsDefault = true, ClearOnVisibleFalse = false }; + _start = new Button { Text = "Start", IsDefault = true }; _start.Accept += (s, e) => { @@ -302,19 +303,28 @@ public StagingUIController () } }; - LayoutStarted += (s, e) => - { - int btnsWidth = _start.Frame.Width + _close.Frame.Width + 2 - 1; - int shiftLeft = Math.Max ((Viewport.Width - btnsWidth) / 2 - 2, 0); + LayoutStarted += StagingUIController_LayoutStarted; + Disposing += StagingUIController_Disposing; + } - shiftLeft += _close.Frame.Width + 1; - _close.X = Pos.AnchorEnd (shiftLeft); - _close.Y = Pos.AnchorEnd (1); + private void StagingUIController_Disposing (object sender, EventArgs e) + { + LayoutStarted -= StagingUIController_LayoutStarted; + Disposing -= StagingUIController_Disposing; + } - shiftLeft += _start.Frame.Width + 1; - _start.X = Pos.AnchorEnd (shiftLeft); - _start.Y = Pos.AnchorEnd (1); - }; + private void StagingUIController_LayoutStarted (object sender, LayoutEventArgs e) + { + int btnsWidth = _start.Frame.Width + _close.Frame.Width + 2 - 1; + int shiftLeft = Math.Max ((Viewport.Width - btnsWidth) / 2 - 2, 0); + + shiftLeft += _close.Frame.Width + 1; + _close.X = Pos.AnchorEnd (shiftLeft); + _close.Y = Pos.AnchorEnd (1); + + shiftLeft += _start.Frame.Width + 1; + _start.X = Pos.AnchorEnd (shiftLeft); + _start.Y = Pos.AnchorEnd (1); } public Staging Staging { get; private set; } @@ -371,11 +381,12 @@ private void WorkerApp_Closed (object sender, ToplevelEventArgs e) { CancelWorker (); } + private void WorkerApp_Closing (object sender, ToplevelClosingEventArgs e) { Toplevel top = ApplicationOverlapped.OverlappedChildren!.Find (x => x.Data.ToString () == "WorkerApp"); - if (Visible && top == this) + if (e.RequestingTop == this && Visible && top == this) { Visible = false; e.Cancel = true; diff --git a/UICatalog/Scenarios/ChineseUI.cs b/UICatalog/Scenarios/ChineseUI.cs index 059b6143cc..e94a98d09a 100644 --- a/UICatalog/Scenarios/ChineseUI.cs +++ b/UICatalog/Scenarios/ChineseUI.cs @@ -3,7 +3,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("ChineseUI", "Chinese UI")] -[ScenarioCategory ("Unicode")] +[ScenarioCategory ("Text and Formatting")] public class ChineseUI : Scenario { public override void Main () diff --git a/UICatalog/Scenarios/ColorPicker.cs b/UICatalog/Scenarios/ColorPicker.cs index 04c58528d9..b1c94720f7 100644 --- a/UICatalog/Scenarios/ColorPicker.cs +++ b/UICatalog/Scenarios/ColorPicker.cs @@ -45,7 +45,7 @@ public override void Main () // Foreground ColorPicker. foregroundColorPicker = new ColorPicker { - Title = "Foreground Color", + Title = "_Foreground Color", BorderStyle = LineStyle.Single, Width = Dim.Percent (50) }; @@ -61,7 +61,7 @@ public override void Main () // Background ColorPicker. backgroundColorPicker = new ColorPicker { - Title = "Background Color", + Title = "_Background Color", X = Pos.AnchorEnd (), Width = Dim.Percent (50), BorderStyle = LineStyle.Single @@ -86,7 +86,7 @@ public override void Main () // Foreground ColorPicker 16. foregroundColorPicker16 = new ColorPicker16 { - Title = "Foreground Color", + Title = "_Foreground Color", BorderStyle = LineStyle.Single, Width = Dim.Percent (50), Visible = false // We default to HSV so hide old one @@ -97,7 +97,7 @@ public override void Main () // Background ColorPicker 16. backgroundColorPicker16 = new ColorPicker16 { - Title = "Background Color", + Title = "_Background Color", X = Pos.AnchorEnd (), Width = Dim.Percent (50), BorderStyle = LineStyle.Single, @@ -132,10 +132,10 @@ public override void Main () Height = Dim.Auto (), RadioLabels = new [] { - "RGB", - "HSV", - "HSL", - "16 Colors" + "_RGB", + "_HSV", + "H_SL", + "_16 Colors" }, SelectedItem = (int)foregroundColorPicker.Style.ColorModel, }; @@ -180,7 +180,7 @@ public override void Main () // Checkbox for switching show text fields on and off var cbShowTextFields = new CheckBox () { - Text = "Show Text Fields", + Text = "Show _Text Fields", Y = Pos.Bottom (rgColorModel)+1, Width = Dim.Auto (), Height = Dim.Auto (), @@ -199,7 +199,7 @@ public override void Main () // Checkbox for switching show text fields on and off var cbShowName = new CheckBox () { - Text = "Show Color Name", + Text = "Show Color _Name", Y = Pos.Bottom (cbShowTextFields) + 1, Width = Dim.Auto (), Height = Dim.Auto (), diff --git a/UICatalog/Scenarios/ConfigurationEditor.cs b/UICatalog/Scenarios/ConfigurationEditor.cs index 1e095a527c..ac123217eb 100644 --- a/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/UICatalog/Scenarios/ConfigurationEditor.cs @@ -44,7 +44,11 @@ public override void Main () _tileView = new TileView (0) { - Width = Dim.Fill (), Height = Dim.Fill (1), Orientation = Orientation.Vertical, LineStyle = LineStyle.Single + Width = Dim.Fill (), + Height = Dim.Fill (1), + Orientation = Orientation.Vertical, + LineStyle = LineStyle.Single, + TabStop = TabBehavior.TabGroup }; top.Add (_tileView); @@ -79,7 +83,11 @@ public override void Main () top.Add (statusBar); - top.Loaded += (s, a) => Open (); + top.Loaded += (s, a) => + { + Open (); + //_tileView.AdvanceFocus (NavigationDirection.Forward, null); + }; _editorColorSchemeChanged += () => { @@ -133,7 +141,18 @@ private void Open () textView.Read (); - textView.Enter += (s, e) => { _lenShortcut.Title = $"Len:{textView.Text.Length}"; }; + textView.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + _lenShortcut.Title = $"Len:{textView.Text.Length}"; + } + }; + } + + if (_tileView.Tiles.Count > 2) + { + _tileView.Tiles.ToArray () [1].ContentView.SetFocus (); } Application.Top.LayoutSubviews (); @@ -150,9 +169,9 @@ private void Quit () int result = MessageBox.Query ( "Save Changes", $"Save changes to {editor.FileInfo.FullName}", - "Yes", - "No", - "Cancel" + "_Yes", + "_No", + "_Cancel" ); if (result == -1 || result == 2) @@ -196,6 +215,8 @@ internal ConfigTextView () } } }; + TabStop = TabBehavior.TabGroup; + } internal FileInfo FileInfo { get; set; } diff --git a/UICatalog/Scenarios/ContentScrolling.cs b/UICatalog/Scenarios/ContentScrolling.cs index d94e0a7e41..543432a4c4 100644 --- a/UICatalog/Scenarios/ContentScrolling.cs +++ b/UICatalog/Scenarios/ContentScrolling.cs @@ -407,6 +407,10 @@ void ClipVisibleContentOnly_Toggle (object sender, CancelEventArgs e app.Closed += (s, e) => View.Diagnostics = _diagnosticFlags; + editor.AutoSelectViewToEdit = true; + editor.AutoSelectSuperView = view; + editor.AutoSelectAdornments = false; + Application.Run (app); app.Dispose (); Application.Shutdown (); diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index eda8599321..29b655c38e 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -16,7 +16,7 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Text and Formatting")] [ScenarioCategory ("Dialogs")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] [ScenarioCategory ("Files and IO")] public class CsvEditor : Scenario { diff --git a/UICatalog/Scenarios/Dialogs.cs b/UICatalog/Scenarios/Dialogs.cs index ded47a5fa7..5872cd093b 100644 --- a/UICatalog/Scenarios/Dialogs.cs +++ b/UICatalog/Scenarios/Dialogs.cs @@ -311,10 +311,10 @@ Label buttonPressedLabel buttons.Add (button); dialog.AddButton (button); - if (buttons.Count > 1) - { - button.TabIndex = buttons [buttons.Count - 2].TabIndex + 1; - } + //if (buttons.Count > 1) + //{ + // button.TabIndex = buttons [buttons.Count - 2].TabIndex + 1; + //} }; dialog.Add (add); diff --git a/UICatalog/Scenarios/DynamicMenuBar.cs b/UICatalog/Scenarios/DynamicMenuBar.cs index 1cd3ae0ef6..ad00a9a691 100644 --- a/UICatalog/Scenarios/DynamicMenuBar.cs +++ b/UICatalog/Scenarios/DynamicMenuBar.cs @@ -9,7 +9,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Dynamic MenuBar", "Demonstrates how to change a MenuBar dynamically.")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] [ScenarioCategory ("Menus")] public class DynamicMenuBar : Scenario { @@ -640,10 +640,10 @@ public DynamicMenuBarSample () }; frmMenu.Add (_lstMenus); - lblMenuBar.TabIndex = btnPrevious.TabIndex + 1; - _lstMenus.TabIndex = lblMenuBar.TabIndex + 1; - btnNext.TabIndex = _lstMenus.TabIndex + 1; - btnAdd.TabIndex = btnNext.TabIndex + 1; + //lblMenuBar.TabIndex = btnPrevious.TabIndex + 1; + //_lstMenus.TabIndex = lblMenuBar.TabIndex + 1; + //btnNext.TabIndex = _lstMenus.TabIndex + 1; + //btnAdd.TabIndex = btnNext.TabIndex + 1; var btnRemove = new Button { X = Pos.Left (btnAdd), Y = Pos.Top (btnAdd) + 1, Text = "Remove" }; frmMenu.Add (btnRemove); @@ -938,7 +938,7 @@ public DynamicMenuBarSample () SetFrameDetails (menuBarItem); }; - _lstMenus.Enter += (s, e) => + _lstMenus.HasFocusChanging += (s, e) => { MenuItem menuBarItem = _lstMenus.SelectedItem > -1 && DataContext.Menus.Count > 0 ? DataContext.Menus [_lstMenus.SelectedItem].MenuItem @@ -966,7 +966,7 @@ public DynamicMenuBarSample () SelectCurrentMenuBarItem (); }; - lblMenuBar.Enter += (s, e) => + lblMenuBar.HasFocusChanging += (s, e) => { if (_menuBar?.Menus != null) { diff --git a/UICatalog/Scenarios/DynamicStatusBar.cs b/UICatalog/Scenarios/DynamicStatusBar.cs index 828b2dab71..f9205257f3 100644 --- a/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/UICatalog/Scenarios/DynamicStatusBar.cs @@ -10,7 +10,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Dynamic StatusBar", "Demonstrates how to add and remove a StatusBar and change items dynamically.")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] public class DynamicStatusBar : Scenario { public override void Main () @@ -440,7 +440,7 @@ public DynamicStatusBarSample () } }; - _lstItems.Enter += (s, e) => + _lstItems.HasFocusChanging += (s, e) => { Shortcut statusItem = DataContext.Items.Count > 0 ? DataContext.Items [_lstItems.SelectedItem].Shortcut diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index a1cec8e42f..65d2506709 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -16,7 +16,7 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Controls")] [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Text and Formatting")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] [ScenarioCategory ("Files and IO")] [ScenarioCategory ("TextView")] [ScenarioCategory ("Menus")] @@ -722,7 +722,7 @@ private void FindReplaceWindow_VisibleChanged (object sender, EventArgs e) } else { - FocusFirst (null); + FocusDeepest (NavigationDirection.Forward, null); } } @@ -736,10 +736,10 @@ private void FindReplaceWindow_Initialized (object sender, EventArgs e) private void ShowFindReplace (bool isFind = true) { _findReplaceWindow.Visible = true; - _findReplaceWindow.SuperView.BringSubviewToFront (_findReplaceWindow); + _findReplaceWindow.SuperView.MoveSubviewToStart (_findReplaceWindow); _tabView.SetFocus (); _tabView.SelectedTab = isFind ? _tabView.Tabs.ToArray () [0] : _tabView.Tabs.ToArray () [1]; - _tabView.SelectedTab.View.FocusFirst (null); + _tabView.SelectedTab.View.FocusDeepest (NavigationDirection.Forward, null); } private void CreateFindReplace () @@ -753,10 +753,10 @@ private void CreateFindReplace () _tabView.AddTab (new () { DisplayText = "Find", View = CreateFindTab () }, true); _tabView.AddTab (new () { DisplayText = "Replace", View = CreateReplaceTab () }, false); - _tabView.SelectedTabChanged += (s, e) => _tabView.SelectedTab.View.FocusFirst (null); + _tabView.SelectedTabChanged += (s, e) => _tabView.SelectedTab.View.FocusDeepest (NavigationDirection.Forward, null); _findReplaceWindow.Add (_tabView); - _tabView.SelectedTab.View.FocusLast (null); // Hack to get the first tab to be focused +// _tabView.SelectedTab.View.FocusLast (null); // Hack to get the first tab to be focused _findReplaceWindow.Visible = false; _appWindow.Add (_findReplaceWindow); } @@ -860,7 +860,7 @@ private View CreateFindTab () Width = Dim.Fill (1), Text = _textToFind }; - txtToFind.Enter += (s, e) => txtToFind.Text = _textToFind; + txtToFind.HasFocusChanging += (s, e) => txtToFind.Text = _textToFind; d.Add (txtToFind); var btnFindNext = new Button @@ -1088,7 +1088,7 @@ private View CreateReplaceTab () Width = Dim.Fill (1), Text = _textToFind }; - txtToFind.Enter += (s, e) => txtToFind.Text = _textToFind; + txtToFind.HasFocusChanging += (s, e) => txtToFind.Text = _textToFind; d.Add (txtToFind); var btnFindNext = new Button diff --git a/UICatalog/Scenarios/GraphViewExample.cs b/UICatalog/Scenarios/GraphViewExample.cs index 2ddb8bd443..8cb5285280 100644 --- a/UICatalog/Scenarios/GraphViewExample.cs +++ b/UICatalog/Scenarios/GraphViewExample.cs @@ -161,7 +161,7 @@ public override void Main () }; frameRight.Add ( - _about = new() { Width = Dim.Fill (), Height = Dim.Fill () } + _about = new() { Width = Dim.Fill (), Height = Dim.Fill (), ReadOnly = true } ); app.Add (frameRight); @@ -170,8 +170,8 @@ public override void Main () new Shortcut [] { new (Key.G.WithCtrl, "Next Graph", () => _graphs [_currentGraph++ % _graphs.Length] ()), - new (Key.CursorUp, "Zoom In", () => Zoom (0.5f)), - new (Key.CursorDown, "Zoom Out", () => Zoom (2f)) + new (Key.PageUp, "Zoom In", () => Zoom (0.5f)), + new (Key.PageDown, "Zoom Out", () => Zoom (2f)) } ); app.Add (statusBar); diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs index b941378c61..08029a0e72 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -8,7 +8,7 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Controls")] [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Text and Formatting")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] [ScenarioCategory ("Files and IO")] public class HexEditor : Scenario { diff --git a/UICatalog/Scenarios/KeyBindings.cs b/UICatalog/Scenarios/KeyBindings.cs index d624445436..2f85aca46f 100644 --- a/UICatalog/Scenarios/KeyBindings.cs +++ b/UICatalog/Scenarios/KeyBindings.cs @@ -82,8 +82,8 @@ Pressing Esc or {Application.QuitKey} will cause it to quit the app. foreach (var appBinding in Application.KeyBindings.Bindings) { - var commands = Application.KeyBindings.GetCommands (appBinding.Key); - appBindings.Add ($"{appBinding.Key} -> {appBinding.Value.BoundView?.GetType ().Name} - {commands [0]}"); + var commands = Application.KeyBindings.GetCommands (appBinding.Key); + appBindings.Add ($"{appBinding.Key} -> {appBinding.Value.BoundView?.GetType ().Name} - {commands [0]}"); } ObservableCollection hotkeyBindings = new (); @@ -125,8 +125,7 @@ Pressing Esc or {Application.QuitKey} will cause it to quit the app. }; appWindow.Add (_focusedBindingsListView); - appWindow.Leave += AppWindow_Leave; - appWindow.Enter += AppWindow_Leave; + appWindow.HasFocusChanged += AppWindow_HasFocusChanged; appWindow.DrawContent += AppWindow_DrawContent; // Run - Start the application. @@ -148,11 +147,17 @@ private void AppWindow_DrawContent (object sender, DrawEventArgs e) } } - private void AppWindow_Leave (object sender, FocusEventArgs e) + private void AppWindow_HasFocusChanged (object sender, HasFocusEventArgs e) { - foreach (var binding in Application.Top.MostFocused.KeyBindings.Bindings.Where (b => b.Value.Scope == KeyBindingScope.Focused)) + if (e.NewValue) { - _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}"); + if (Application.Top is { MostFocused: {} }) + { + foreach (var binding in Application.Top.MostFocused.KeyBindings.Bindings.Where (b => b.Value.Scope == KeyBindingScope.Focused)) + { + _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}"); + } + } } } } diff --git a/UICatalog/Scenarios/LineCanvasExperiment.cs b/UICatalog/Scenarios/LineCanvasExperiment.cs index 88ec3b63c2..e38175296b 100644 --- a/UICatalog/Scenarios/LineCanvasExperiment.cs +++ b/UICatalog/Scenarios/LineCanvasExperiment.cs @@ -4,7 +4,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("LineCanvas Experiments", "Experiments with LineCanvas")] [ScenarioCategory ("Drawing")] -[ScenarioCategory ("Borders")] +[ScenarioCategory ("Adornments")] [ScenarioCategory ("Proof of Concept")] public class LineCanvasExperiment : Scenario { diff --git a/UICatalog/Scenarios/LineViewExample.cs b/UICatalog/Scenarios/LineViewExample.cs index 7e6b18d53c..9efeb30c9f 100644 --- a/UICatalog/Scenarios/LineViewExample.cs +++ b/UICatalog/Scenarios/LineViewExample.cs @@ -6,7 +6,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Line View", "Demonstrates drawing lines using the LineView control.")] [ScenarioCategory ("Controls")] [ScenarioCategory ("LineView")] -[ScenarioCategory ("Borders")] +[ScenarioCategory ("Adornments")] public class LineViewExample : Scenario { public override void Main () diff --git a/UICatalog/Scenarios/ListColumns.cs b/UICatalog/Scenarios/ListColumns.cs index 8b02e51418..dc04330602 100644 --- a/UICatalog/Scenarios/ListColumns.cs +++ b/UICatalog/Scenarios/ListColumns.cs @@ -11,7 +11,7 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Controls")] [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Text and Formatting")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] [ScenarioCategory ("Scrolling")] public class ListColumns : Scenario { diff --git a/UICatalog/Scenarios/Navigation.cs b/UICatalog/Scenarios/Navigation.cs new file mode 100644 index 0000000000..9db18f440e --- /dev/null +++ b/UICatalog/Scenarios/Navigation.cs @@ -0,0 +1,267 @@ +using System.Timers; +using Terminal.Gui; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("Navigation", "Navigation Tester")] +[ScenarioCategory ("Mouse and Keyboard")] +[ScenarioCategory ("Layout")] +[ScenarioCategory ("Overlapped")] +public class Navigation : Scenario +{ + private int _hotkeyCount; + + public override void Main () + { + Application.Init (); + + Window app = new () + { + Title = GetQuitKeyAndName (), + TabStop = TabBehavior.TabGroup + }; + + var editor = new AdornmentsEditor + { + X = 0, + Y = 0, + AutoSelectViewToEdit = true, + TabStop = TabBehavior.NoStop + }; + app.Add (editor); + + FrameView testFrame = new () + { + Title = "_1 Test Frame", + X = Pos.Right (editor), + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + app.Add (testFrame); + + Button button = new () + { + X = 0, + Y = 0, + Title = $"TopButton _{GetNextHotKey ()}" + }; + + testFrame.Add (button); + + View tiledView1 = CreateTiledView (0, 2, 2); + View tiledView2 = CreateTiledView (1, Pos.Right (tiledView1), Pos.Top (tiledView1)); + + testFrame.Add (tiledView1); + testFrame.Add (tiledView2); + + View tiledView3 = CreateTiledView (1, Pos.Right (tiledView2), Pos.Top (tiledView2)); + tiledView3.TabStop = TabBehavior.TabGroup; + tiledView3.BorderStyle = LineStyle.Double; + testFrame.Add (tiledView3); + + View overlappedView1 = CreateOverlappedView (2, 10, Pos.Center ()); + View tiledSubView = CreateTiledView (4, 0, 2); + overlappedView1.Add (tiledSubView); + + ProgressBar progressBar = new () + { + X = Pos.AnchorEnd (), + Y = Pos.AnchorEnd (), + Width = Dim.Fill (), + Id = "progressBar" + }; + overlappedView1.Add (progressBar); + + Timer timer = new (10) + { + AutoReset = true + }; + + timer.Elapsed += (o, args) => + { + if (progressBar.Fraction == 1.0) + { + progressBar.Fraction = 0; + } + + progressBar.Fraction += 0.01f; + + Application.Wakeup (); + + progressBar.SetNeedsDisplay (); + }; + timer.Start (); + + View overlappedView2 = CreateOverlappedView (3, 8, 10); + + View overlappedInOverlapped1 = CreateOverlappedView (4, 1, 4); + overlappedView2.Add (overlappedInOverlapped1); + + View overlappedInOverlapped2 = CreateOverlappedView (5, 10, 7); + overlappedView2.Add (overlappedInOverlapped2); + + StatusBar statusBar = new (); + + statusBar.Add ( + new Shortcut + { + Title = "Hide", + Text = "Hotkey", + Key = Key.F4, + Action = () => + { + // TODO: move this logic into `View.ShowHide()` or similar + overlappedView2.Visible = false; + overlappedView2.Enabled = overlappedView2.Visible; + } + }); + + statusBar.Add ( + new Shortcut + { + Title = "Toggle Hide", + Text = "App", + KeyBindingScope = KeyBindingScope.Application, + Key = Key.F4.WithCtrl, + Action = () => + { + // TODO: move this logic into `View.ShowHide()` or similar + overlappedView2.Visible = !overlappedView2.Visible; + overlappedView2.Enabled = overlappedView2.Visible; + + if (overlappedView2.Visible) + { + overlappedView2.SetFocus (); + } + } + }); + overlappedView2.Add (statusBar); + + ColorPicker colorPicker = new () + { + Y = 12, + Width = Dim.Fill (), + Id = "colorPicker", + Style = new () + { + ShowTextFields = true, + ShowColorName = true + } + }; + colorPicker.ApplyStyleChanges (); + + colorPicker.SelectedColor = testFrame.ColorScheme.Normal.Background; + colorPicker.ColorChanged += ColorPicker_ColorChanged; + overlappedView2.Add (colorPicker); + overlappedView2.Width = 50; + + testFrame.Add (overlappedView1); + testFrame.Add (overlappedView2); + + DatePicker datePicker = new () + { + X = 1, + Y = 7, + Id = "datePicker", + ColorScheme = Colors.ColorSchemes ["Toplevel"], + ShadowStyle = ShadowStyle.Transparent, + BorderStyle = LineStyle.Double, + CanFocus = true, // Can't drag without this? BUGBUG + TabStop = TabBehavior.TabGroup, + Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped + }; + testFrame.Add (datePicker); + + button = new () + { + X = Pos.AnchorEnd (), + Y = Pos.AnchorEnd (), + Title = $"TopButton _{GetNextHotKey ()}" + }; + + testFrame.Add (button); + + editor.AutoSelectSuperView = testFrame; + testFrame.SetFocus (); + Application.Run (app); + timer.Close (); + app.Dispose (); + Application.Shutdown (); + + return; + + void ColorPicker_ColorChanged (object sender, ColorEventArgs e) + { + testFrame.ColorScheme = testFrame.ColorScheme with { Normal = new (testFrame.ColorScheme.Normal.Foreground, e.CurrentValue) }; + } + } + + private View CreateOverlappedView (int id, Pos x, Pos y) + { + var overlapped = new View + { + X = x, + Y = y, + Height = Dim.Auto (), + Width = Dim.Auto (), + Title = $"Overlapped{id} _{GetNextHotKey ()}", + ColorScheme = Colors.ColorSchemes ["Toplevel"], + Id = $"Overlapped{id}", + ShadowStyle = ShadowStyle.Transparent, + BorderStyle = LineStyle.Double, + CanFocus = true, // Can't drag without this? BUGBUG + TabStop = TabBehavior.TabGroup, + Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped + }; + + Button button = new () + { + Title = $"Button{id} _{GetNextHotKey ()}" + }; + overlapped.Add (button); + + button = new () + { + Y = Pos.Bottom (button), + Title = $"Button{id} _{GetNextHotKey ()}" + }; + overlapped.Add (button); + + return overlapped; + } + + private View CreateTiledView (int id, Pos x, Pos y) + { + var overlapped = new View + { + X = x, + Y = y, + Height = Dim.Auto (), + Width = Dim.Auto (), + Title = $"Tiled{id} _{GetNextHotKey ()}", + Id = $"Tiled{id}", + BorderStyle = LineStyle.Single, + CanFocus = true, // Can't drag without this? BUGBUG + TabStop = TabBehavior.TabStop, + Arrangement = ViewArrangement.Fixed + }; + + Button button = new () + { + Title = $"Tiled Button{id} _{GetNextHotKey ()}" + }; + overlapped.Add (button); + + button = new () + { + Y = Pos.Bottom (button), + Title = $"Tiled Button{id} _{GetNextHotKey ()}" + }; + overlapped.Add (button); + + return overlapped; + } + + private char GetNextHotKey () { return (char)('A' + _hotkeyCount++); } +} diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index 5fb27b26c4..31e6f35136 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -84,7 +84,7 @@ public override void Main () _focusedTabView = _tabView; _tabView.SelectedTabChanged += TabView_SelectedTabChanged; - _tabView.Enter += (s, e) => _focusedTabView = _tabView; + _tabView.HasFocusChanging += (s, e) => _focusedTabView = _tabView; top.Ready += (s, e) => { @@ -241,7 +241,7 @@ private TabView CreateNewTabView () tv.TabClicked += TabView_TabClicked; tv.SelectedTabChanged += TabView_SelectedTabChanged; - tv.Enter += (s, e) => _focusedTabView = tv; + tv.HasFocusChanging += (s, e) => _focusedTabView = tv; return tv; } @@ -309,7 +309,7 @@ private void Split (int offset, Orientation orientation, TabView sender, OpenedF tab.CloneTo (newTabView); newTile.ContentView.Add (newTabView); - newTabView.FocusFirst (null); + newTabView.FocusDeepest (NavigationDirection.Forward, null); newTabView.AdvanceFocus (NavigationDirection.Forward, null); } diff --git a/UICatalog/Scenarios/NumericUpDownDemo.cs b/UICatalog/Scenarios/NumericUpDownDemo.cs index b514a041aa..3c17d7fcd8 100644 --- a/UICatalog/Scenarios/NumericUpDownDemo.cs +++ b/UICatalog/Scenarios/NumericUpDownDemo.cs @@ -15,7 +15,6 @@ public override void Main () Window app = new () { Title = GetQuitKeyAndName (), - TabStop = TabBehavior.TabGroup }; var editor = new AdornmentsEditor @@ -23,7 +22,6 @@ public override void Main () X = 0, Y = 0, AutoSelectViewToEdit = true, - TabStop = TabBehavior.NoStop }; app.Add (editor); @@ -50,15 +48,15 @@ void AppInitialized (object? sender, EventArgs e) { floatEditor!.NumericUpDown!.Increment = 0.1F; floatEditor!.NumericUpDown!.Format = "{0:0.0}"; - } + editor.AutoSelectSuperView = app; + intEditor.SetFocus (); + Application.Run (app); app.Dispose (); Application.Shutdown (); - } - } internal class NumericUpDownEditor : View where T : notnull @@ -95,6 +93,7 @@ internal NumericUpDownEditor () Width = Dim.Auto (DimAutoStyle.Content); Height = Dim.Auto (DimAutoStyle.Content); TabStop = TabBehavior.TabGroup; + CanFocus = true; Initialized += NumericUpDownEditorInitialized; @@ -137,7 +136,7 @@ void ValuedOnAccept (object? sender, EventArgs e) _numericUpDown.Value = (T)Convert.ChangeType (_value.Text, typeof (T)); } - _value.ColorScheme = SuperView.ColorScheme; + _value.ColorScheme = SuperView!.ColorScheme; } catch (System.FormatException) @@ -184,8 +183,8 @@ void FormatOnAccept (object? o, EventArgs eventArgs) // Test format to ensure it's valid _ = string.Format (_format.Text, _value); _numericUpDown.Format = _format.Text; - - _format.ColorScheme = SuperView.ColorScheme; + + _format.ColorScheme = SuperView!.ColorScheme; } catch (System.FormatException) @@ -240,7 +239,7 @@ void IncrementOnAccept (object? o, EventArgs eventArgs) _numericUpDown.Increment = (T)Convert.ChangeType (_increment.Text, typeof (T)); } - _increment.ColorScheme = SuperView.ColorScheme; + _increment.ColorScheme = SuperView!.ColorScheme; } catch (System.FormatException) diff --git a/UICatalog/Scenarios/ProgressBarStyles.cs b/UICatalog/Scenarios/ProgressBarStyles.cs index d863c33c32..54855ce529 100644 --- a/UICatalog/Scenarios/ProgressBarStyles.cs +++ b/UICatalog/Scenarios/ProgressBarStyles.cs @@ -35,7 +35,7 @@ public override void Main () var editor = new AdornmentsEditor () { - AutoSelectViewToEdit = false + AutoSelectViewToEdit = true }; app.Add (editor); diff --git a/UICatalog/Scenarios/RunTExample.cs b/UICatalog/Scenarios/RunTExample.cs index 4dcb690301..b949718bef 100644 --- a/UICatalog/Scenarios/RunTExample.cs +++ b/UICatalog/Scenarios/RunTExample.cs @@ -3,7 +3,8 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Run Example", "Illustrates using Application.Run to run a custom class")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Runnable")] +[ScenarioCategory ("Overlapped")] public class RunTExample : Scenario { public override void Main () diff --git a/UICatalog/Scenarios/ShadowStyles.cs b/UICatalog/Scenarios/ShadowStyles.cs index 8491163962..b231140fa1 100644 --- a/UICatalog/Scenarios/ShadowStyles.cs +++ b/UICatalog/Scenarios/ShadowStyles.cs @@ -34,7 +34,8 @@ public override void Main () Width = Dim.Percent (30), Height = Dim.Percent (30), Title = "Shadow Window", - Arrangement = ViewArrangement.Movable, + Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped, + BorderStyle = LineStyle.Double, ShadowStyle = ShadowStyle.Transparent }; @@ -55,11 +56,14 @@ public override void Main () }; app.Add (button); + editor.AutoSelectViewToEdit = true; + editor.AutoSelectSuperView = app; + editor.AutoSelectAdornments = false; + Application.Run (app); app.Dispose (); Application.Shutdown (); - return; } } diff --git a/UICatalog/Scenarios/SingleBackgroundWorker.cs b/UICatalog/Scenarios/SingleBackgroundWorker.cs index 3e990779f6..51ee6881ed 100644 --- a/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -9,7 +9,8 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Single BackgroundWorker", "A single BackgroundWorker threading opening another Toplevel")] [ScenarioCategory ("Threading")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] +[ScenarioCategory ("Runnable")] public class SingleBackgroundWorker : Scenario { public override void Main () diff --git a/UICatalog/Scenarios/Sliders.cs b/UICatalog/Scenarios/Sliders.cs index 3deeb40fc8..f697ebdabd 100644 --- a/UICatalog/Scenarios/Sliders.cs +++ b/UICatalog/Scenarios/Sliders.cs @@ -609,7 +609,7 @@ public override void Main () }; } - app.FocusFirst (null); + app.FocusDeepest (NavigationDirection.Forward, null); Application.Run (app); app.Dispose (); diff --git a/UICatalog/Scenarios/TabViewExample.cs b/UICatalog/Scenarios/TabViewExample.cs index b551722b1a..9d48c05685 100644 --- a/UICatalog/Scenarios/TabViewExample.cs +++ b/UICatalog/Scenarios/TabViewExample.cs @@ -73,6 +73,7 @@ public override void Main () _tabView = new() { + Title = "_Tab View", X = 0, Y = 1, Width = 60, @@ -80,9 +81,9 @@ public override void Main () BorderStyle = LineStyle.Single }; - _tabView.AddTab (new() { DisplayText = "Tab1", View = new Label { Text = "hodor!" } }, false); - _tabView.AddTab (new() { DisplayText = "Tab2", View = new TextField { Text = "durdur" } }, false); - _tabView.AddTab (new() { DisplayText = "Interactive Tab", View = GetInteractiveTab () }, false); + _tabView.AddTab (new() { DisplayText = "Tab_1", View = new Label { Text = "hodor!" } }, false); + _tabView.AddTab (new() { DisplayText = "Tab_2", View = new TextField { Text = "durdur", Width = 10 } }, false); + _tabView.AddTab (new() { DisplayText = "_Interactive Tab", View = GetInteractiveTab () }, false); _tabView.AddTab (new() { DisplayText = "Big Text", View = GetBigTextFileTab () }, false); _tabView.AddTab ( @@ -138,9 +139,10 @@ public override void Main () Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1), - Title = "About", + Title = "_About", BorderStyle = LineStyle.Single, - TabStop = TabBehavior.TabStop + TabStop = TabBehavior.TabStop, + CanFocus = true }; frameRight.Add ( @@ -161,9 +163,10 @@ public override void Main () Y = Pos.Bottom (_tabView), Width = _tabView.Width, Height = Dim.Fill (1), - Title = "Bottom Frame", + Title = "B_ottom Frame", BorderStyle = LineStyle.Single, - TabStop = TabBehavior.TabStop + TabStop = TabBehavior.TabStop, + CanFocus = true }; @@ -216,7 +219,11 @@ private View GetBigTextFileTab () private View GetInteractiveTab () { - var interactiveTab = new View { Width = Dim.Fill (), Height = Dim.Fill () }; + var interactiveTab = new View + { + Width = Dim.Fill (), Height = Dim.Fill (), + CanFocus = true + }; var lblName = new Label { Text = "Name:" }; interactiveTab.Add (lblName); diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 542537ef62..f7f3d5f17e 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -14,7 +14,7 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Controls")] [ScenarioCategory ("Dialogs")] [ScenarioCategory ("Text and Formatting")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] public class TableEditor : Scenario { private readonly HashSet _checkedFileSystemInfos = new (); diff --git a/UICatalog/Scenarios/ViewExperiments.cs b/UICatalog/Scenarios/ViewExperiments.cs index 2d10a85443..c9db4478d0 100644 --- a/UICatalog/Scenarios/ViewExperiments.cs +++ b/UICatalog/Scenarios/ViewExperiments.cs @@ -1,10 +1,11 @@ -using Terminal.Gui; +using System; +using Terminal.Gui; namespace UICatalog.Scenarios; [ScenarioMetadata ("View Experiments", "v2 View Experiments")] [ScenarioCategory ("Controls")] -[ScenarioCategory ("Borders")] +[ScenarioCategory ("Adornments")] [ScenarioCategory ("Layout")] [ScenarioCategory ("Proof of Concept")] public class ViewExperiments : Scenario @@ -23,7 +24,6 @@ public override void Main () { X = 0, Y = 0, - AutoSelectViewToEdit = true, TabStop = TabBehavior.NoStop }; app.Add (editor); @@ -42,31 +42,11 @@ public override void Main () { X = 0, Y = 0, - Title = $"TopButton _{GetNextHotKey()}", + Title = $"TopButton _{GetNextHotKey ()}", }; testFrame.Add (button); - var tiledView1 = CreateTiledView (0, 2, 2); - var tiledView2 = CreateTiledView (1, Pos.Right (tiledView1), Pos.Top (tiledView1)); - - testFrame.Add (tiledView1); - testFrame.Add (tiledView2); - - var overlappedView1 = CreateOverlappedView (2, Pos.Center(), Pos.Center()); - var tiledSubView = CreateTiledView (4, 0, 2); - overlappedView1.Add (tiledSubView); - - var overlappedView2 = CreateOverlappedView (3, Pos.Center() + 5, Pos.Center() + 5); - tiledSubView = CreateTiledView (4, 0, 2); - overlappedView2.Add (tiledSubView); - - tiledSubView = CreateTiledView (5, 0, Pos.Bottom(tiledSubView)); - overlappedView2.Add (tiledSubView); - - testFrame.Add (overlappedView1); - testFrame.Add (overlappedView2); - button = new () { X = Pos.AnchorEnd (), @@ -76,9 +56,16 @@ public override void Main () testFrame.Add (button); + editor.AutoSelectViewToEdit = true; + editor.AutoSelectSuperView = testFrame; + editor.AutoSelectAdornments = true; + Application.Run (app); app.Dispose (); + Application.Shutdown (); + + return; } private int _hotkeyCount; @@ -87,71 +74,4 @@ private char GetNextHotKey () { return (char)((int)'A' + _hotkeyCount++); } - - private View CreateTiledView (int id, Pos x, Pos y) - { - View overlapped = new View - { - X = x, - Y = y, - Height = Dim.Auto (), - Width = Dim.Auto (), - Title = $"Tiled{id} _{GetNextHotKey ()}", - Id = $"Tiled{id}", - BorderStyle = LineStyle.Single, - CanFocus = true, // Can't drag without this? BUGBUG - TabStop = TabBehavior.TabGroup, - Arrangement = ViewArrangement.Fixed - }; - - Button button = new () - { - Title = $"Tiled Button{id} _{GetNextHotKey ()}" - }; - overlapped.Add (button); - - button = new () - { - Y = Pos.Bottom (button), - Title = $"Tiled Button{id} _{GetNextHotKey ()}" - }; - overlapped.Add (button); - - return overlapped; - } - - - private View CreateOverlappedView (int id, Pos x, Pos y) - { - View overlapped = new View - { - X = x, - Y = y, - Height = Dim.Auto (), - Width = Dim.Auto (), - Title = $"Overlapped{id} _{GetNextHotKey ()}", - ColorScheme = Colors.ColorSchemes ["Toplevel"], - Id = $"Overlapped{id}", - ShadowStyle = ShadowStyle.Transparent, - BorderStyle = LineStyle.Double, - CanFocus = true, // Can't drag without this? BUGBUG - TabStop = TabBehavior.TabGroup, - Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped - }; - - Button button = new () - { - Title = $"Button{id} _{GetNextHotKey ()}" - }; - overlapped.Add (button); - - button = new () - { - Y = Pos.Bottom (button), - Title = $"Button{id} _{GetNextHotKey ()}" - }; - overlapped.Add (button); - - return overlapped; - } } diff --git a/UICatalog/Scenarios/Wizards.cs b/UICatalog/Scenarios/Wizards.cs index dbd210cc20..c909ccd405 100644 --- a/UICatalog/Scenarios/Wizards.cs +++ b/UICatalog/Scenarios/Wizards.cs @@ -5,8 +5,10 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Wizards", "Demonstrates the Wizard class")] [ScenarioCategory ("Dialogs")] -[ScenarioCategory ("Top Level Windows")] +[ScenarioCategory ("Overlapped")] [ScenarioCategory ("Wizards")] +[ScenarioCategory ("Runnable")] + public class Wizards : Scenario { public override void Main () diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 14b06f6e5c..a6f90edfe1 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -502,7 +502,8 @@ public UICatalogTopLevel () CanFocus = false }, HelpText = "", - Key = Key.F6 + KeyBindingScope = KeyBindingScope.Application, + Key = Key.F7 }; ((CheckBox)ShForce16Colors.CommandView).CheckedStateChanging += (sender, args) => @@ -643,13 +644,6 @@ public UICatalogTopLevel () Add (CategoryList); Add (ScenarioList); - Add (MenuBar!); - - if (StatusBar is { }) - { - Add (StatusBar); - } - Loaded += LoadedHandler; Unloaded += UnloadedHandler; diff --git a/UnitTests/Application/Application.NavigationTests.cs b/UnitTests/Application/Application.NavigationTests.cs index 714a509883..325d9471ec 100644 --- a/UnitTests/Application/Application.NavigationTests.cs +++ b/UnitTests/Application/Application.NavigationTests.cs @@ -1,170 +1,157 @@ -using Moq; -using Xunit.Abstractions; +using Xunit.Abstractions; -namespace Terminal.Gui.ApplicationTests; +namespace Terminal.Gui.ApplicationTests.NavigationTests; public class ApplicationNavigationTests (ITestOutputHelper output) { private readonly ITestOutputHelper _output = output; - [Fact] - public void GetDeepestFocusedSubview_ShouldReturnNull_WhenViewIsNull () - { - // Act - var result = ApplicationNavigation.GetDeepestFocusedSubview (null); - - // Assert - Assert.Null (result); - } - - [Fact] - public void GetDeepestFocusedSubview_ShouldReturnSameView_WhenNoSubviewsHaveFocus () + [Theory] + [InlineData (TabBehavior.NoStop)] + [InlineData (TabBehavior.TabStop)] + [InlineData (TabBehavior.TabGroup)] + public void Begin_SetsFocus_On_Deepest_Focusable_View (TabBehavior behavior) { - // Arrange - var view = new View () { Id = "view", CanFocus = true }; ; + Application.Init (new FakeDriver ()); + + var top = new Toplevel + { + TabStop = behavior + }; + Assert.False (top.HasFocus); + + View subView = new () + { + CanFocus = true, + TabStop = behavior + }; + top.Add (subView); + + View subSubView = new () + { + CanFocus = true, + TabStop = TabBehavior.NoStop + }; + subView.Add (subSubView); + + RunState rs = Application.Begin (top); + Assert.True (top.HasFocus); + Assert.True (subView.HasFocus); + Assert.True (subSubView.HasFocus); - // Act - var result = ApplicationNavigation.GetDeepestFocusedSubview (view); + top.Dispose (); - // Assert - Assert.Equal (view, result); + Application.Shutdown (); } [Fact] - public void GetDeepestFocusedSubview_ShouldReturnFocusedSubview () + public void Begin_SetsFocus_On_Top () { - // Arrange - var parentView = new View () { Id = "parentView", CanFocus = true }; ; - var childView1 = new View () { Id = "childView1", CanFocus = true }; ; - var childView2 = new View () { Id = "childView2", CanFocus = true }; ; - var grandChildView = new View () { Id = "grandChildView", CanFocus = true }; ; - - parentView.Add (childView1, childView2); - childView2.Add (grandChildView); + Application.Init (new FakeDriver ()); - grandChildView.SetFocus (); + var top = new Toplevel (); + Assert.False (top.HasFocus); - // Act - var result = ApplicationNavigation.GetDeepestFocusedSubview (parentView); + RunState rs = Application.Begin (top); + Assert.True (top.HasFocus); - // Assert - Assert.Equal (grandChildView, result); + top.Dispose (); + Application.Shutdown (); } [Fact] - public void GetDeepestFocusedSubview_ShouldReturnDeepestFocusedSubview () + public void Focused_Change_Raises_FocusedChanged () { - // Arrange - var parentView = new View () { Id = "parentView", CanFocus = true }; ; - var childView1 = new View () { Id = "childView1", CanFocus = true }; ; - var childView2 = new View () { Id = "childView2", CanFocus = true }; ; - var grandChildView = new View () { Id = "grandChildView", CanFocus = true }; ; - var greatGrandChildView = new View () { Id = "greatGrandChildView", CanFocus = true }; ; + var raised = false; - parentView.Add (childView1, childView2); - childView2.Add (grandChildView); - grandChildView.Add (greatGrandChildView); + Application.Navigation = new (); - grandChildView.SetFocus (); + Application.Navigation.FocusedChanged += ApplicationNavigationOnFocusedChanged; - // Act - var result = ApplicationNavigation.GetDeepestFocusedSubview (parentView); + Application.Navigation.SetFocused (new ()); - // Assert - Assert.Equal (greatGrandChildView, result); + Assert.True (raised); - // Arrange - greatGrandChildView.CanFocus = false; - grandChildView.SetFocus (); + Application.Navigation.GetFocused ().Dispose (); + Application.Navigation.SetFocused (null); - // Act - result = ApplicationNavigation.GetDeepestFocusedSubview (parentView); - - // Assert - Assert.Equal (grandChildView, result); - } + Application.Navigation.FocusedChanged -= ApplicationNavigationOnFocusedChanged; - [Fact] - public void MoveNextView_ShouldMoveFocusToNextView () - { - // Arrange - var top = new Toplevel (); - var view1 = new View () { Id = "view1", CanFocus = true }; - var view2 = new View () { Id = "view2", CanFocus = true }; - top.Add (view1, view2); - Application.Top = top; - Application.Current = top; - view1.SetFocus (); + Application.Navigation = null; - // Act - ApplicationNavigation.MoveNextView (); + return; - // Assert - Assert.True (view2.HasFocus); - - top.Dispose (); + void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e) { raised = true; } } [Fact] - public void MoveNextViewOrTop_ShouldMoveFocusToNextViewOrTop () + public void GetFocused_Returns_Focused_View () { - // Arrange - var top = new Toplevel (); - var view1 = new View () { Id = "view1", CanFocus = true }; - var view2 = new View () { Id = "view2", CanFocus = true }; - top.Add (view1, view2); - Application.Top = top; - Application.Current = top; - view1.SetFocus (); - - // Act - ApplicationNavigation.MoveNextViewOrTop (); - - // Assert - Assert.True (view2.HasFocus); - - top.Dispose (); + Application.Navigation = new (); + + Application.Current = new() + { + Id = "top", + CanFocus = true + }; + + var subView1 = new View + { + Id = "subView1", + CanFocus = true + }; + + var subView2 = new View + { + Id = "subView2", + CanFocus = true + }; + Application.Current.Add (subView1, subView2); + Assert.False (Application.Current.HasFocus); + + Application.Current.SetFocus (); + Assert.True (subView1.HasFocus); + Assert.Equal (subView1, Application.Navigation.GetFocused ()); + + Application.Navigation.AdvanceFocus (NavigationDirection.Forward, null); + Assert.Equal (subView2, Application.Navigation.GetFocused ()); + + Application.ResetState (); } [Fact] - public void MovePreviousView_ShouldMoveFocusToPreviousView () + public void GetFocused_Returns_Null_If_No_Focused_View () { - // Arrange - var top = new Toplevel (); - var view1 = new View () { Id = "view1", CanFocus = true }; - var view2 = new View () { Id = "view2", CanFocus = true }; - top.Add (view1, view2); - Application.Top = top; - Application.Current = top; - view2.SetFocus (); + Application.Navigation = new (); - // Act - ApplicationNavigation.MovePreviousView (); + Application.Current = new() + { + Id = "top", + CanFocus = true + }; - // Assert - Assert.True (view1.HasFocus); + var subView1 = new View + { + Id = "subView1", + CanFocus = true + }; - top.Dispose (); - } + Application.Current.Add (subView1); + Assert.False (Application.Current.HasFocus); - [Fact] - public void MovePreviousViewOrTop_ShouldMoveFocusToPreviousViewOrTop () - { - // Arrange - var top = new Toplevel (); - var view1 = new View () { Id = "view1", CanFocus = true, TabStop = TabBehavior.TabGroup }; - var view2 = new View () { Id = "view2", CanFocus = true, TabStop = TabBehavior.TabGroup }; - top.Add (view1, view2); - Application.Top = top; - Application.Current = top; - view2.SetFocus (); + Application.Current.SetFocus (); + Assert.True (subView1.HasFocus); + Assert.Equal (subView1, Application.Navigation.GetFocused ()); - // Act - ApplicationNavigation.MovePreviousViewOrTop (); + subView1.HasFocus = false; + Assert.False (subView1.HasFocus); + Assert.True (Application.Current.HasFocus); + Assert.Equal (Application.Current, Application.Navigation.GetFocused ()); - // Assert - Assert.True (view1.HasFocus); + Application.Current.HasFocus = false; + Assert.False (Application.Current.HasFocus); + Assert.Null (Application.Navigation.GetFocused ()); - top.Dispose (); + Application.ResetState (); } } diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index b0383e9866..623d4a31fe 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -463,7 +463,7 @@ public void Init_Unbalanced_Throws () } [Fact] - public void InitWithoutTopLevelFactory_Begin_End_Cleans_Up () + public void Init_WithoutTopLevelFactory_Begin_End_Cleans_Up () { // Begin will cause Run() to be called, which will call Begin(). Thus will block the tests // if we don't stop @@ -507,6 +507,15 @@ public void InitWithoutTopLevelFactory_Begin_End_Cleans_Up () Assert.Null (Application.Driver); } + [Fact] + public void Init_NoParam_ForceDriver_Works () + { + Application.ForceDriver = "FakeDriver"; + Application.Init (); + Assert.IsType (Application.Driver); + Application.ResetState (); + } + [Fact] [AutoInitShutdown] public void Internal_Properties_Correct () @@ -955,7 +964,7 @@ public void Run_Loaded_Ready_Unlodaded_Events () } // TODO: All Toplevel layout tests should be moved to ToplevelTests.cs - [Fact] + [Fact (Skip = "#2491 - Changing focus should cause NeedsDispay = true, so bogus test?")] public void Run_Toplevel_With_Modal_View_Does_Not_Refresh_If_Not_Dirty () { Init (); diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs index 2f6d85d00f..8b6fcce5f3 100644 --- a/UnitTests/Application/KeyboardTests.cs +++ b/UnitTests/Application/KeyboardTests.cs @@ -1,15 +1,12 @@ -using UICatalog; -using Xunit.Abstractions; +using Xunit.Abstractions; namespace Terminal.Gui.ApplicationTests; /// -/// Application tests for keyboard support. +/// Application tests for keyboard support. /// public class KeyboardTests { - private readonly ITestOutputHelper _output; - public KeyboardTests (ITestOutputHelper output) { _output = output; @@ -19,247 +16,11 @@ public KeyboardTests (ITestOutputHelper output) #endif } - - [Fact] - [AutoInitShutdown] - public void QuitKey_Getter_Setter () - { - Toplevel top = new (); - var isQuiting = false; - - top.Closing += (s, e) => - { - isQuiting = true; - e.Cancel = true; - }; - - Application.Begin (top); - top.Running = true; - - Key prevKey = Application.QuitKey; - - Application.OnKeyDown (Application.QuitKey); - Assert.True (isQuiting); - - isQuiting = false; - Application.OnKeyDown (Application.QuitKey); - Assert.True (isQuiting); - - isQuiting = false; - Application.QuitKey = Key.C.WithCtrl; - Application.OnKeyDown (prevKey); // Should not quit - Assert.False (isQuiting); - Application.OnKeyDown (Key.Q.WithCtrl);// Should not quit - Assert.False (isQuiting); - - Application.OnKeyDown (Application.QuitKey); - Assert.True (isQuiting); - - // Reset the QuitKey to avoid throws errors on another tests - Application.QuitKey = prevKey; - top.Dispose (); - } - - [Fact] - public void QuitKey_Default_Is_Esc () - { - Application.ResetState (true); - // Before Init - Assert.Equal (Key.Esc, Application.QuitKey); - - Application.Init (new FakeDriver ()); - // After Init - Assert.Equal (Key.Esc, Application.QuitKey); - - Application.Shutdown (); - } + private readonly ITestOutputHelper _output; private object _timeoutLock; - [Fact] - public void QuitKey_Quits () - { - Assert.Null (_timeoutLock); - _timeoutLock = new object (); - - uint abortTime = 500; - bool initialized = false; - int iteration = 0; - bool shutdown = false; - object timeout = null; - - Application.InitializedChanged += OnApplicationOnInitializedChanged; - - Application.Init (new FakeDriver ()); - Assert.True (initialized); - Assert.False (shutdown); - - _output.WriteLine ("Application.Run ().Dispose ().."); - Application.Run ().Dispose (); - _output.WriteLine ("Back from Application.Run ().Dispose ()"); - - Assert.True (initialized); - Assert.False (shutdown); - - Assert.Equal (1, iteration); - - Application.Shutdown (); - - Application.InitializedChanged -= OnApplicationOnInitializedChanged; - - lock (_timeoutLock) - { - if (timeout is { }) - { - Application.RemoveTimeout (timeout); - timeout = null; - } - } - - Assert.True (initialized); - Assert.True (shutdown); - -#if DEBUG_IDISPOSABLE - Assert.Empty (Responder.Instances); -#endif - lock (_timeoutLock) - { - _timeoutLock = null; - } - - return; - - void OnApplicationOnInitializedChanged (object s, EventArgs a) - { - _output.WriteLine ("OnApplicationOnInitializedChanged: {0}", a.CurrentValue); - if (a.CurrentValue) - { - Application.Iteration += OnApplicationOnIteration; - initialized = true; - lock (_timeoutLock) - { - timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback); - } - } - else - { - Application.Iteration -= OnApplicationOnIteration; - shutdown = true; - } - } - - bool ForceCloseCallback () - { - lock (_timeoutLock) - { - _output.WriteLine ($"ForceCloseCallback. iteration: {iteration}"); - if (timeout is { }) - { - timeout = null; - } - } - Application.ResetState (true); - Assert.Fail ($"Failed to Quit with {Application.QuitKey} after {abortTime}ms. Force quit."); - - return false; - } - - void OnApplicationOnIteration (object s, IterationEventArgs a) - { - _output.WriteLine ("Iteration: {0}", iteration); - iteration++; - Assert.True (iteration < 2, "Too many iterations, something is wrong."); - if (Application.IsInitialized) - { - _output.WriteLine (" Pressing QuitKey"); - Application.OnKeyDown (Application.QuitKey); - } - } - } - - [Fact (Skip = "Replace when new key statics are added.")] - public void NextTabGroupKey_PrevTabGroupKey_Tests () - { - Application.Init (new FakeDriver ()); - - Toplevel top = new (); - var w1 = new Window (); - var v1 = new TextField (); - var v2 = new TextView (); - w1.Add (v1, v2); - - var w2 = new Window (); - var v3 = new CheckBox (); - var v4 = new Button (); - w2.Add (v3, v4); - - top.Add (w1, w2); - - Application.Iteration += (s, a) => - { - Assert.True (v1.HasFocus); - - // Using default keys. - Application.OnKeyDown (Key.F6); - Assert.True (v2.HasFocus); - Application.OnKeyDown (Key.F6); - Assert.True (v3.HasFocus); - Application.OnKeyDown (Key.F6); - Assert.True (v4.HasFocus); - Application.OnKeyDown (Key.F6); - Assert.True (v1.HasFocus); - - Application.OnKeyDown (Key.F6.WithShift); - Assert.True (v4.HasFocus); - Application.OnKeyDown (Key.F6.WithShift); - Assert.True (v3.HasFocus); - Application.OnKeyDown (Key.F6.WithShift); - Assert.True (v2.HasFocus); - Application.OnKeyDown (Key.F6.WithShift); - Assert.True (v1.HasFocus); - - // Using alternate keys. - Application.NextTabGroupKey = Key.F7; - Application.PrevTabGroupKey = Key.F8; - - Application.OnKeyDown (Key.F7); - Assert.True (v2.HasFocus); - Application.OnKeyDown (Key.F7); - Assert.True (v3.HasFocus); - Application.OnKeyDown (Key.F7); - Assert.True (v4.HasFocus); - Application.OnKeyDown (Key.F7); - Assert.True (v1.HasFocus); - - Application.OnKeyDown (Key.F8); - Assert.True (v4.HasFocus); - Application.OnKeyDown (Key.F8); - Assert.True (v3.HasFocus); - Application.OnKeyDown (Key.F8); - Assert.True (v2.HasFocus); - Application.OnKeyDown (Key.F8); - Assert.True (v1.HasFocus); - - Application.RequestStop (); - }; - - Application.Run (top); - - // Replacing the defaults keys to avoid errors on others unit tests that are using it. - Application.NextTabGroupKey = Key.PageDown.WithCtrl; - Application.PrevTabGroupKey = Key.PageUp.WithCtrl; - Application.QuitKey = Key.Q.WithCtrl; - - Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.NextTabGroupKey.KeyCode); - Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.PrevTabGroupKey.KeyCode); - Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode); - - top.Dispose (); - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); - } - - [Fact] + [Fact (Skip = "No longer valid test.")] [AutoInitShutdown] public void EnsuresTopOnFront_CanFocus_False_By_Keyboard () { @@ -319,7 +80,7 @@ public void EnsuresTopOnFront_CanFocus_False_By_Keyboard () top.Dispose (); } - [Fact] + [Fact (Skip = "No longer valid test.")] [AutoInitShutdown] public void EnsuresTopOnFront_CanFocus_True_By_Keyboard () { @@ -373,97 +134,29 @@ public void EnsuresTopOnFront_CanFocus_True_By_Keyboard () } [Fact] - public void KeyUp_Event () + [AutoInitShutdown] + public void KeyBinding_Application_KeyBindings_Add_Adds () { - Application.Init (new FakeDriver ()); - - // Setup some fake keypresses (This) - var input = "Tests"; + Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); + Application.KeyBindings.Add (Key.B, KeyBindingScope.Application, Command.Accept); - Key originalQuitKey = Application.QuitKey; - Application.QuitKey = Key.Q.WithCtrl; - // Put a control-q in at the end - FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo ('Q', ConsoleKey.Q, false, false, true)); + Assert.True (Application.KeyBindings.TryGet (Key.A, out KeyBinding binding)); + Assert.Null (binding.BoundView); + Assert.True (Application.KeyBindings.TryGet (Key.B, out binding)); + Assert.Null (binding.BoundView); + } - foreach (char c in input.Reverse ()) - { - if (char.IsLetter (c)) - { - FakeConsole.MockKeyPresses.Push ( - new ConsoleKeyInfo ( - c, - (ConsoleKey)char.ToUpper (c), - char.IsUpper (c), - false, - false - ) - ); - } - else - { - FakeConsole.MockKeyPresses.Push ( - new ConsoleKeyInfo ( - c, - (ConsoleKey)c, - false, - false, - false - ) - ); - } - } + [Fact] + [AutoInitShutdown] + public void KeyBinding_Application_RemoveKeyBinding_Removes () + { + Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); - int stackSize = FakeConsole.MockKeyPresses.Count; + Assert.True (Application.KeyBindings.TryGet (Key.A, out _)); - var iterations = 0; - - Application.Iteration += (s, a) => - { - iterations++; - - // Stop if we run out of control... - if (iterations > 10) - { - Application.RequestStop (); - } - }; - - var keyUps = 0; - var output = string.Empty; - var top = new Toplevel (); - - top.KeyUp += (sender, args) => - { - if (args.KeyCode != (KeyCode.CtrlMask | KeyCode.Q)) - { - output += args.AsRune; - } - - keyUps++; - }; - - Application.Run (top); - Application.QuitKey = originalQuitKey; - - // Input string should match output - Assert.Equal (input, output); - - // # of key up events should match stack size - //Assert.Equal (stackSize, keyUps); - // We can't use numbers variables on the left side of an Assert.Equal/NotEqual, - // it must be literal (Linux only). - Assert.Equal (6, keyUps); - - // # of key up events should match # of iterations - Assert.Equal (stackSize, iterations); - - top.Dispose (); - Application.Shutdown (); - Assert.Null (Application.Current); - Assert.Null (Application.Top); - Assert.Null (Application.MainLoop); - Assert.Null (Application.Driver); - } + Application.KeyBindings.Remove (Key.A); + Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); + } [Fact] [AutoInitShutdown] @@ -483,7 +176,7 @@ public void KeyBinding_OnKeyDown () invoked = false; view.ApplicationCommand = false; - Application.KeyBindings.Remove (KeyCode.A); + Application.KeyBindings.Remove (KeyCode.A); Application.OnKeyDown (Key.A); // old Assert.False (invoked); Assert.False (view.ApplicationCommand); @@ -538,19 +231,6 @@ public void KeyBinding_OnKeyDown_Negative () top.Dispose (); } - [Fact] - [AutoInitShutdown] - public void KeyBinding_Application_KeyBindings_Add_Adds () - { - Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); - Application.KeyBindings.Add (Key.B, KeyBindingScope.Application, Command.Accept); - - Assert.True (Application.KeyBindings.TryGet (Key.A, out var binding)); - Assert.Null (binding.BoundView); - Assert.True (Application.KeyBindings.TryGet (Key.B, out binding)); - Assert.Null (binding.BoundView); - } - [Fact] [AutoInitShutdown] public void KeyBinding_View_KeyBindings_Add_Adds () @@ -561,7 +241,7 @@ public void KeyBinding_View_KeyBindings_Add_Adds () View view2 = new (); Application.KeyBindings.Add (Key.B, view2, Command.Accept); - Assert.True (Application.KeyBindings.TryGet (Key.A, out var binding)); + 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); @@ -569,29 +249,493 @@ public void KeyBinding_View_KeyBindings_Add_Adds () [Fact] [AutoInitShutdown] - public void KeyBinding_Application_RemoveKeyBinding_Removes () + public void KeyBinding_View_KeyBindings_RemoveKeyBinding_Removes () { - Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); + View view1 = new (); + Application.KeyBindings.Add (Key.A, view1, Command.Accept); - Assert.True (Application.KeyBindings.TryGet (Key.A, out _)); + View view2 = new (); + Application.KeyBindings.Add (Key.B, view1, Command.Accept); - Application.KeyBindings.Remove (Key.A); + Application.KeyBindings.Remove (Key.A, view1); Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); } + [Fact] + public void KeyUp_Event () + { + Application.Init (new FakeDriver ()); + + // Setup some fake keypresses (This) + var input = "Tests"; + + Key originalQuitKey = Application.QuitKey; + Application.QuitKey = Key.Q.WithCtrl; + + // Put a control-q in at the end + FakeConsole.MockKeyPresses.Push (new ('Q', ConsoleKey.Q, false, false, true)); + + foreach (char c in input.Reverse ()) + { + if (char.IsLetter (c)) + { + FakeConsole.MockKeyPresses.Push ( + new ( + c, + (ConsoleKey)char.ToUpper (c), + char.IsUpper (c), + false, + false + ) + ); + } + else + { + FakeConsole.MockKeyPresses.Push ( + new ( + c, + (ConsoleKey)c, + false, + false, + false + ) + ); + } + } + + int stackSize = FakeConsole.MockKeyPresses.Count; + + var iterations = 0; + + Application.Iteration += (s, a) => + { + iterations++; + + // Stop if we run out of control... + if (iterations > 10) + { + Application.RequestStop (); + } + }; + + var keyUps = 0; + var output = string.Empty; + var top = new Toplevel (); + + top.KeyUp += (sender, args) => + { + if (args.KeyCode != (KeyCode.CtrlMask | KeyCode.Q)) + { + output += args.AsRune; + } + + keyUps++; + }; + + Application.Run (top); + Application.QuitKey = originalQuitKey; + + // Input string should match output + Assert.Equal (input, output); + + // # of key up events should match stack size + //Assert.Equal (stackSize, keyUps); + // We can't use numbers variables on the left side of an Assert.Equal/NotEqual, + // it must be literal (Linux only). + Assert.Equal (6, keyUps); + + // # of key up events should match # of iterations + Assert.Equal (stackSize, iterations); + + top.Dispose (); + Application.Shutdown (); + Assert.Null (Application.Current); + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void NextTabGroupKey_Moves_Focus_To_TabStop_In_Next_TabGroup () + { + // Arrange + Application.Navigation = new (); + var top = new Toplevel (); + + var view1 = new View + { + Id = "view1", + CanFocus = true, + TabStop = TabBehavior.TabGroup + }; + + var subView1 = new View + { + Id = "subView1", + CanFocus = true, + TabStop = TabBehavior.TabStop + }; + + view1.Add (subView1); + + var view2 = new View + { + Id = "view2", + CanFocus = true, + TabStop = TabBehavior.TabGroup + }; + + var subView2 = new View + { + Id = "subView2", + CanFocus = true, + TabStop = TabBehavior.TabStop + }; + view2.Add (subView2); + + top.Add (view1, view2); + Application.Top = top; + Application.Current = top; + view1.SetFocus (); + Assert.True (view1.HasFocus); + Assert.True (subView1.HasFocus); + + // Act + Application.OnKeyDown (Application.NextTabGroupKey); + + // Assert + Assert.True (view2.HasFocus); + Assert.True (subView2.HasFocus); + + top.Dispose (); + Application.Navigation = null; + } + + [Fact] + public void NextTabGroupKey_PrevTabGroupKey_Tests () + { + Application.Init (new FakeDriver ()); + + Toplevel top = new (); // TabGroup + var w1 = new Window (); // TabGroup + var v1 = new TextField (); // TabStop + var v2 = new TextView (); // TabStop + w1.Add (v1, v2); + + var w2 = new Window (); // TabGroup + var v3 = new CheckBox (); // TabStop + var v4 = new Button (); // TabStop + w2.Add (v3, v4); + + top.Add (w1, w2); + + Application.Iteration += (s, a) => + { + Assert.True (v1.HasFocus); + + // Across TabGroups + Application.OnKeyDown (Key.F6); + Assert.True (v3.HasFocus); + Application.OnKeyDown (Key.F6); + Assert.True (v1.HasFocus); + + Application.OnKeyDown (Key.F6.WithShift); + Assert.True (v3.HasFocus); + Application.OnKeyDown (Key.F6.WithShift); + Assert.True (v1.HasFocus); + + // Restore? + Application.OnKeyDown (Key.Tab); + Assert.True (v2.HasFocus); + + Application.OnKeyDown (Key.F6); + Assert.True (v3.HasFocus); + + Application.OnKeyDown (Key.F6); + Assert.True (v2.HasFocus); + + Application.RequestStop (); + }; + + Application.Run (top); + + // Replacing the defaults keys to avoid errors on others unit tests that are using it. + Application.NextTabGroupKey = Key.PageDown.WithCtrl; + Application.PrevTabGroupKey = Key.PageUp.WithCtrl; + Application.QuitKey = Key.Q.WithCtrl; + + Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.NextTabGroupKey.KeyCode); + Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.PrevTabGroupKey.KeyCode); + Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode); + + top.Dispose (); + + // Shutdown must be called to safely clean up Application if Init has been called + Application.Shutdown (); + } + + [Fact] + public void NextTabKey_Moves_Focus_To_Next_TabStop () + { + // Arrange + Application.Navigation = new (); + var top = new Toplevel (); + var view1 = new View { Id = "view1", CanFocus = true }; + var view2 = new View { Id = "view2", CanFocus = true }; + top.Add (view1, view2); + Application.Top = top; + Application.Current = top; + view1.SetFocus (); + + // Act + Application.OnKeyDown (Application.NextTabKey); + + // Assert + Assert.True (view2.HasFocus); + + top.Dispose (); + Application.Navigation = null; + } + + [Fact] + public void PrevTabGroupKey_Moves_Focus_To_TabStop_In_Prev_TabGroup () + { + // Arrange + Application.Navigation = new (); + var top = new Toplevel (); + + var view1 = new View + { + Id = "view1", + CanFocus = true, + TabStop = TabBehavior.TabGroup + }; + + var subView1 = new View + { + Id = "subView1", + CanFocus = true, + TabStop = TabBehavior.TabStop + }; + + view1.Add (subView1); + + var view2 = new View + { + Id = "view2", + CanFocus = true, + TabStop = TabBehavior.TabGroup + }; + + var subView2 = new View + { + Id = "subView2", + CanFocus = true, + TabStop = TabBehavior.TabStop + }; + view2.Add (subView2); + + top.Add (view1, view2); + Application.Top = top; + Application.Current = top; + view1.SetFocus (); + Assert.True (view1.HasFocus); + Assert.True (subView1.HasFocus); + + // Act + Application.OnKeyDown (Application.PrevTabGroupKey); + + // Assert + Assert.True (view2.HasFocus); + Assert.True (subView2.HasFocus); + + top.Dispose (); + Application.Navigation = null; + } + + [Fact] + public void PrevTabKey_Moves_Focus_To_Prev_TabStop () + { + // Arrange + Application.Navigation = new (); + var top = new Toplevel (); + var view1 = new View { Id = "view1", CanFocus = true }; + var view2 = new View { Id = "view2", CanFocus = true }; + top.Add (view1, view2); + Application.Top = top; + Application.Current = top; + view1.SetFocus (); + + // Act + Application.OnKeyDown (Application.NextTabKey); + + // Assert + Assert.True (view2.HasFocus); + + top.Dispose (); + Application.Navigation = null; + } + + [Fact] + public void QuitKey_Default_Is_Esc () + { + Application.ResetState (true); + + // Before Init + Assert.Equal (Key.Esc, Application.QuitKey); + + Application.Init (new FakeDriver ()); + + // After Init + Assert.Equal (Key.Esc, Application.QuitKey); + + Application.Shutdown (); + } + [Fact] [AutoInitShutdown] - public void KeyBinding_View_KeyBindings_RemoveKeyBinding_Removes () + public void QuitKey_Getter_Setter () { + Toplevel top = new (); + var isQuiting = false; - View view1 = new (); - Application.KeyBindings.Add (Key.A, view1, Command.Accept); + top.Closing += (s, e) => + { + isQuiting = true; + e.Cancel = true; + }; - View view2 = new (); - Application.KeyBindings.Add (Key.B, view1, Command.Accept); + Application.Begin (top); + top.Running = true; - Application.KeyBindings.Remove (Key.A, view1); - Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); + Key prevKey = Application.QuitKey; + + Application.OnKeyDown (Application.QuitKey); + Assert.True (isQuiting); + + isQuiting = false; + Application.OnKeyDown (Application.QuitKey); + Assert.True (isQuiting); + + isQuiting = false; + Application.QuitKey = Key.C.WithCtrl; + Application.OnKeyDown (prevKey); // Should not quit + Assert.False (isQuiting); + Application.OnKeyDown (Key.Q.WithCtrl); // Should not quit + Assert.False (isQuiting); + + Application.OnKeyDown (Application.QuitKey); + Assert.True (isQuiting); + + // Reset the QuitKey to avoid throws errors on another tests + Application.QuitKey = prevKey; + top.Dispose (); + } + + [Fact] + public void QuitKey_Quits () + { + Assert.Null (_timeoutLock); + _timeoutLock = new (); + + uint abortTime = 500; + var initialized = false; + var iteration = 0; + var shutdown = false; + object timeout = null; + + Application.InitializedChanged += OnApplicationOnInitializedChanged; + + Application.Init (new FakeDriver ()); + Assert.True (initialized); + Assert.False (shutdown); + + _output.WriteLine ("Application.Run ().Dispose ().."); + Application.Run ().Dispose (); + _output.WriteLine ("Back from Application.Run ().Dispose ()"); + + Assert.True (initialized); + Assert.False (shutdown); + + Assert.Equal (1, iteration); + + Application.Shutdown (); + + Application.InitializedChanged -= OnApplicationOnInitializedChanged; + + lock (_timeoutLock) + { + if (timeout is { }) + { + Application.RemoveTimeout (timeout); + timeout = null; + } + } + + Assert.True (initialized); + Assert.True (shutdown); + +#if DEBUG_IDISPOSABLE + Assert.Empty (Responder.Instances); +#endif + lock (_timeoutLock) + { + _timeoutLock = null; + } + + return; + + void OnApplicationOnInitializedChanged (object s, EventArgs a) + { + _output.WriteLine ("OnApplicationOnInitializedChanged: {0}", a.CurrentValue); + + if (a.CurrentValue) + { + Application.Iteration += OnApplicationOnIteration; + initialized = true; + + lock (_timeoutLock) + { + timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback); + } + } + else + { + Application.Iteration -= OnApplicationOnIteration; + shutdown = true; + } + } + + bool ForceCloseCallback () + { + lock (_timeoutLock) + { + _output.WriteLine ($"ForceCloseCallback. iteration: {iteration}"); + + if (timeout is { }) + { + timeout = null; + } + } + + Application.ResetState (true); + Assert.Fail ($"Failed to Quit with {Application.QuitKey} after {abortTime}ms. Force quit."); + + return false; + } + + void OnApplicationOnIteration (object s, IterationEventArgs a) + { + _output.WriteLine ("Iteration: {0}", iteration); + iteration++; + Assert.True (iteration < 2, "Too many iterations, something is wrong."); + + if (Application.IsInitialized) + { + _output.WriteLine (" Pressing QuitKey"); + Application.OnKeyDown (Application.QuitKey); + } + } } // Test View for testing Application key Bindings diff --git a/UnitTests/FileServices/FileDialogTests.cs b/UnitTests/FileServices/FileDialogTests.cs index 1c09541681..8b8b2e19ba 100644 --- a/UnitTests/FileServices/FileDialogTests.cs +++ b/UnitTests/FileServices/FileDialogTests.cs @@ -99,13 +99,9 @@ public void DoNotConfirmSelectionWhenFindFocused () string openIn = Path.Combine (Environment.CurrentDirectory, "zz"); Directory.CreateDirectory (openIn); dlg.Path = openIn + Path.DirectorySeparatorChar; -#if BROKE_IN_2927 - Send ('f', ConsoleKey.F, false, true, false); -#else Application.OnKeyDown (Key.Tab); Application.OnKeyDown (Key.Tab); Application.OnKeyDown (Key.Tab); -#endif Assert.IsType (dlg.MostFocused); var tf = (TextField)dlg.MostFocused; diff --git a/UnitTests/Input/ResponderTests.cs b/UnitTests/Input/ResponderTests.cs index 6801aa991a..85c2d87644 100644 --- a/UnitTests/Input/ResponderTests.cs +++ b/UnitTests/Input/ResponderTests.cs @@ -239,11 +239,11 @@ public void New_Methods_Return_False () Assert.False (r.NewMouseLeaveEvent (new MouseEvent { Flags = MouseFlags.AllEvents })); var v = new View (); - Assert.False (r.OnEnter (v)); + //Assert.False (r.OnEnter (v)); v.Dispose (); v = new View (); - Assert.False (r.OnLeave (v)); + //Assert.False (r.OnLeave (v)); v.Dispose (); r.Dispose (); diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index 784b21da5b..70e977a291 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -240,6 +240,7 @@ public override void After (MethodInfo methodUnderTest) public override void Before (MethodInfo methodUnderTest) { Debug.WriteLine ($"Before: {methodUnderTest.Name}"); + Application.ResetState (); Assert.Null (Application.Driver); Application.Driver = new FakeDriver { Rows = 25, Cols = 25 }; base.Before (methodUnderTest); diff --git a/UnitTests/View/Adornment/ShadowStyletests.cs b/UnitTests/View/Adornment/ShadowStyletests.cs index 88ab6df093..60924f688a 100644 --- a/UnitTests/View/Adornment/ShadowStyletests.cs +++ b/UnitTests/View/Adornment/ShadowStyletests.cs @@ -100,7 +100,7 @@ public void ShadowStyle_Margin_Thickness (ShadowStyle style, int expectedLeft, i ShadowStyle.Transparent, """ 031 - 331 + 131 111 """)] [InlineData ( diff --git a/UnitTests/View/FindDeepestViewTests.cs b/UnitTests/View/FindDeepestViewTests.cs index c6ea6fa065..c9f53d9a3a 100644 --- a/UnitTests/View/FindDeepestViewTests.cs +++ b/UnitTests/View/FindDeepestViewTests.cs @@ -1,5 +1,6 @@  #nullable enable +using Microsoft.VisualStudio.TestPlatform.Utilities; using Xunit.Abstractions; namespace Terminal.Gui.ViewTests; diff --git a/UnitTests/View/HotKeyTests.cs b/UnitTests/View/HotKeyTests.cs index d284e9b013..163b0b32df 100644 --- a/UnitTests/View/HotKeyTests.cs +++ b/UnitTests/View/HotKeyTests.cs @@ -108,7 +108,7 @@ public void NewKeyDownEvent_Ignores_Focus_KeyBindings_SuperView () public void NewKeyDownEvent_Honors_HotKey_KeyBindings_SuperView () { var view = new View (); - view.KeyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey); + view.KeyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey); bool invoked = false; view.InvokingKeyBindings += (s, e) => { invoked = true; }; @@ -123,19 +123,35 @@ public void NewKeyDownEvent_Honors_HotKey_KeyBindings_SuperView () [Fact] - public void NewKeyDownEvent_InNewKeyDownEventvokes_HotKey_Command_With_SuperView () + public void NewKeyDownEvent_InNewKeyDownEvent_Invokes_HotKey_Command_With_SuperView () { - var view = new View { HotKeySpecifier = (Rune)'^', Title = "^Test" }; + var superView = new View () + { + CanFocus = true + }; - var superView = new View (); - superView.Add (view); + var view1 = new View + { + HotKeySpecifier = (Rune)'^', + Title = "view^1", + CanFocus = true + }; - view.CanFocus = true; - Assert.False (view.HasFocus); + var view2 = new View + { + HotKeySpecifier = (Rune)'^', + Title = "view^2", + CanFocus = true + }; + + superView.Add (view1, view2); + + superView.SetFocus (); + Assert.True (view1.HasFocus); - var ke = Key.T; + var ke = Key.D2; superView.NewKeyDownEvent (ke); - Assert.True (view.HasFocus); + Assert.True (view2.HasFocus); } [Fact] diff --git a/UnitTests/View/Layout/ToScreenTests.cs b/UnitTests/View/Layout/ToScreenTests.cs index 2524b07273..96cd3a326d 100644 --- a/UnitTests/View/Layout/ToScreenTests.cs +++ b/UnitTests/View/Layout/ToScreenTests.cs @@ -940,4 +940,294 @@ public void ViewportToScreen_Positive_NestedSuperView_WithAdornments (int frameX // Assert Assert.Equal (expectedX, screen.X); } + + + [Fact] + [AutoInitShutdown] + public void ScreenToView_ViewToScreen_FindDeepestView_Full_Top () + { + Toplevel top = new (); + top.BorderStyle = LineStyle.Single; + + var view = new View + { + X = 3, + Y = 2, + Width = 10, + Height = 1, + Text = "0123456789" + }; + top.Add (view); + + Application.Begin (top); + + Assert.Equal (Application.Current, top); + Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); + Assert.Equal (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); + Assert.Equal (new (0, 0, 80, 25), top.Frame); + + ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); + Assert.Equal (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); + Assert.Equal (new (0, 0, 20, 10), top.Frame); + + _ = TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────┐ +│ │ +│ │ +│ 0123456789 │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────┘" + , + _output + ); + + // top + Assert.Equal (Point.Empty, top.ScreenToFrame (new (0, 0))); + Point screen = top.Margin.ViewportToScreen (new Point (0, 0)); + Assert.Equal (0, screen.X); + Assert.Equal (0, screen.Y); + screen = top.Border.ViewportToScreen (new Point (0, 0)); + Assert.Equal (0, screen.X); + Assert.Equal (0, screen.Y); + screen = top.Padding.ViewportToScreen (new Point (0, 0)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = top.ViewportToScreen (new Point (0, 0)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = top.ViewportToScreen (new Point (-1, -1)); + Assert.Equal (0, screen.X); + Assert.Equal (0, screen.Y); + var found = View.FindDeepestView (top, new (0, 0)); + Assert.Equal (top.Border, found); + + Assert.Equal (0, found.Frame.X); + Assert.Equal (0, found.Frame.Y); + Assert.Equal (new (3, 2), top.ScreenToFrame (new (3, 2))); + screen = top.ViewportToScreen (new Point (3, 2)); + Assert.Equal (4, screen.X); + Assert.Equal (3, screen.Y); + found = View.FindDeepestView (top, new (screen.X, screen.Y)); + Assert.Equal (view, found); + + //Assert.Equal (0, found.FrameToScreen ().X); + //Assert.Equal (0, found.FrameToScreen ().Y); + found = View.FindDeepestView (top, new (3, 2)); + Assert.Equal (top, found); + + //Assert.Equal (3, found.FrameToScreen ().X); + //Assert.Equal (2, found.FrameToScreen ().Y); + Assert.Equal (new (13, 2), top.ScreenToFrame (new (13, 2))); + screen = top.ViewportToScreen (new Point (12, 2)); + Assert.Equal (13, screen.X); + Assert.Equal (3, screen.Y); + found = View.FindDeepestView (top, new (screen.X, screen.Y)); + Assert.Equal (view, found); + + //Assert.Equal (9, found.FrameToScreen ().X); + //Assert.Equal (0, found.FrameToScreen ().Y); + screen = top.ViewportToScreen (new Point (13, 2)); + Assert.Equal (14, screen.X); + Assert.Equal (3, screen.Y); + found = View.FindDeepestView (top, new (13, 2)); + Assert.Equal (top, found); + + //Assert.Equal (13, found.FrameToScreen ().X); + //Assert.Equal (2, found.FrameToScreen ().Y); + Assert.Equal (new (14, 3), top.ScreenToFrame (new (14, 3))); + screen = top.ViewportToScreen (new Point (14, 3)); + Assert.Equal (15, screen.X); + Assert.Equal (4, screen.Y); + found = View.FindDeepestView (top, new (14, 3)); + Assert.Equal (top, found); + + //Assert.Equal (14, found.FrameToScreen ().X); + //Assert.Equal (3, found.FrameToScreen ().Y); + + // view + Assert.Equal (new (-4, -3), view.ScreenToFrame (new (0, 0))); + screen = view.Margin.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = view.Border.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = view.Padding.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = view.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = view.ViewportToScreen (new Point (-4, -3)); + Assert.Equal (0, screen.X); + Assert.Equal (0, screen.Y); + found = View.FindDeepestView (top, new (0, 0)); + Assert.Equal (top.Border, found); + + Assert.Equal (new (-1, -1), view.ScreenToFrame (new (3, 2))); + screen = view.ViewportToScreen (new Point (0, 0)); + Assert.Equal (4, screen.X); + Assert.Equal (3, screen.Y); + found = View.FindDeepestView (top, new (4, 3)); + Assert.Equal (view, found); + + Assert.Equal (new (9, -1), view.ScreenToFrame (new (13, 2))); + screen = view.ViewportToScreen (new Point (10, 0)); + Assert.Equal (14, screen.X); + Assert.Equal (3, screen.Y); + found = View.FindDeepestView (top, new (14, 3)); + Assert.Equal (top, found); + + Assert.Equal (new (10, 0), view.ScreenToFrame (new (14, 3))); + screen = view.ViewportToScreen (new Point (11, 1)); + Assert.Equal (15, screen.X); + Assert.Equal (4, screen.Y); + found = View.FindDeepestView (top, new (15, 4)); + Assert.Equal (top, found); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void ScreenToView_ViewToScreen_FindDeepestView_Smaller_Top () + { + var top = new Toplevel + { + X = 3, + Y = 2, + Width = 20, + Height = 10, + BorderStyle = LineStyle.Single + }; + + var view = new View + { + X = 3, + Y = 2, + Width = 10, + Height = 1, + Text = "0123456789" + }; + top.Add (view); + + Application.Begin (top); + + Assert.Equal (Application.Current, top); + Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); + Assert.NotEqual (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); + Assert.Equal (new (3, 2, 20, 10), top.Frame); + + ((FakeDriver)Application.Driver!).SetBufferSize (30, 20); + Assert.Equal (new (0, 0, 30, 20), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); + Assert.NotEqual (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); + Assert.Equal (new (3, 2, 20, 10), top.Frame); + + Rectangle frame = TestHelpers.AssertDriverContentsWithFrameAre ( + @" + ┌──────────────────┐ + │ │ + │ │ + │ 0123456789 │ + │ │ + │ │ + │ │ + │ │ + │ │ + └──────────────────┘" + , + _output + ); + + // mean the output started at col 3 and line 2 + // which result with a width of 23 and a height of 10 on the output + Assert.Equal (new (3, 2, 23, 10), frame); + + // top + Assert.Equal (new (-3, -2), top.ScreenToFrame (new (0, 0))); + Point screen = top.Margin.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (0, screen.X); + Assert.Equal (0, screen.Y); + screen = top.Border.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (0, screen.X); + Assert.Equal (0, screen.Y); + screen = top.Padding.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = top.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = top.ViewportToScreen (new Point (-4, -3)); + Assert.Equal (0, screen.X); + Assert.Equal (0, screen.Y); + var found = View.FindDeepestView (top, new (-4, -3)); + Assert.Null (found); + Assert.Equal (Point.Empty, top.ScreenToFrame (new (3, 2))); + screen = top.ViewportToScreen (new Point (0, 0)); + Assert.Equal (4, screen.X); + Assert.Equal (3, screen.Y); + Assert.Equal (top.Border, View.FindDeepestView (top, new (3, 2))); + + //Assert.Equal (0, found.FrameToScreen ().X); + //Assert.Equal (0, found.FrameToScreen ().Y); + Assert.Equal (new (10, 0), top.ScreenToFrame (new (13, 2))); + screen = top.ViewportToScreen (new Point (10, 0)); + Assert.Equal (14, screen.X); + Assert.Equal (3, screen.Y); + Assert.Equal (top.Border, View.FindDeepestView (top, new (13, 2))); + + //Assert.Equal (10, found.FrameToScreen ().X); + //Assert.Equal (0, found.FrameToScreen ().Y); + Assert.Equal (new (11, 1), top.ScreenToFrame (new (14, 3))); + screen = top.ViewportToScreen (new Point (11, 1)); + Assert.Equal (15, screen.X); + Assert.Equal (4, screen.Y); + Assert.Equal (top, View.FindDeepestView (top, new (14, 3))); + + // view + Assert.Equal (new (-7, -5), view.ScreenToFrame (new (0, 0))); + screen = view.Margin.ViewportToScreen (new Point (-6, -4)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = view.Border.ViewportToScreen (new Point (-6, -4)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = view.Padding.ViewportToScreen (new Point (-6, -4)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + screen = view.ViewportToScreen (new Point (-6, -4)); + Assert.Equal (1, screen.X); + Assert.Equal (1, screen.Y); + Assert.Null (View.FindDeepestView (top, new (1, 1))); + Assert.Equal (new (-4, -3), view.ScreenToFrame (new (3, 2))); + screen = view.ViewportToScreen (new Point (-3, -2)); + Assert.Equal (4, screen.X); + Assert.Equal (3, screen.Y); + Assert.Equal (top, View.FindDeepestView (top, new (4, 3))); + Assert.Equal (new (-1, -1), view.ScreenToFrame (new (6, 4))); + screen = view.ViewportToScreen (new Point (0, 0)); + Assert.Equal (7, screen.X); + Assert.Equal (5, screen.Y); + Assert.Equal (view, View.FindDeepestView (top, new (7, 5))); + Assert.Equal (new (6, -1), view.ScreenToFrame (new (13, 4))); + screen = view.ViewportToScreen (new Point (7, 0)); + Assert.Equal (14, screen.X); + Assert.Equal (5, screen.Y); + Assert.Equal (view, View.FindDeepestView (top, new (14, 5))); + Assert.Equal (new (7, -2), view.ScreenToFrame (new (14, 3))); + screen = view.ViewportToScreen (new Point (8, -1)); + Assert.Equal (15, screen.X); + Assert.Equal (4, screen.Y); + Assert.Equal (top, View.FindDeepestView (top, new (15, 4))); + Assert.Equal (new (16, -2), view.ScreenToFrame (new (23, 3))); + screen = view.ViewportToScreen (new Point (17, -1)); + Assert.Equal (24, screen.X); + Assert.Equal (4, screen.Y); + Assert.Null (View.FindDeepestView (top, new (24, 4))); + top.Dispose (); + } } diff --git a/UnitTests/View/Navigation/AddRemoveTests.cs b/UnitTests/View/Navigation/AddRemoveTests.cs new file mode 100644 index 0000000000..ae84ba64fe --- /dev/null +++ b/UnitTests/View/Navigation/AddRemoveTests.cs @@ -0,0 +1,241 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class AddRemoveNavigationTests () : TestsAllViews +{ + [Fact] + public void Add_First_Subview_Gets_Focus () + { + View top = new View () + { + Id = "top", + CanFocus = true + }; + + top.SetFocus (); + Assert.True (top.HasFocus); + + View subView = new View () + { + Id = "subView", + CanFocus = true + }; + + top.Add (subView); + + Assert.True (top.HasFocus); + Assert.Equal (subView, top.Focused); + Assert.True (subView.HasFocus); + } + + [Fact] + public void Add_Subsequent_Subview_Gets_Focus () + { + View top = new View () + { + Id = "top", + CanFocus = true + }; + + top.SetFocus (); + Assert.True (top.HasFocus); + + View subView = new View () + { + Id = "subView", + CanFocus = true + }; + + top.Add (subView); + + Assert.True (subView.HasFocus); + + View subView2 = new View () + { + Id = "subView2", + CanFocus = true + }; + + top.Add (subView2); + + Assert.True (subView2.HasFocus); + + + } + + [Fact] + public void Add_Nested_Subviews_Deepest_Gets_Focus () + { + View top = new View () + { + Id = "top", + CanFocus = true + }; + + top.SetFocus (); + Assert.True (top.HasFocus); + + View subView = new View () + { + Id = "subView", + CanFocus = true + }; + + View subSubView = new View () + { + Id = "subSubView", + CanFocus = true + }; + + subView.Add (subSubView); + + top.Add (subView); + + Assert.True (top.HasFocus); + Assert.Equal (subView, top.Focused); + Assert.True (subView.HasFocus); + Assert.True (subSubView.HasFocus); + } + + + [Fact] + public void Remove_Subview_Raises_HasFocusChanged () + { + var top = new View + { + Id = "top", + CanFocus = true + }; + + var subView1 = new View + { + Id = "subView1", + CanFocus = true + }; + + var subView2 = new View + { + Id = "subView2", + CanFocus = true + }; + top.Add (subView1, subView2); + + var subView1HasFocusChangedTrueCount = 0; + var subView1HasFocusChangedFalseCount = 0; + + subView1.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subView1HasFocusChangedTrueCount++; + } + else + { + subView1HasFocusChangedFalseCount++; + } + }; + + var subView2HasFocusChangedTrueCount = 0; + var subView2HasFocusChangedFalseCount = 0; + + subView2.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subView2HasFocusChangedTrueCount++; + } + else + { + subView2HasFocusChangedFalseCount++; + } + }; + + top.SetFocus (); + Assert.True (top.HasFocus); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + Assert.Equal (1, subView1HasFocusChangedTrueCount); + Assert.Equal (0, subView1HasFocusChangedFalseCount); + + Assert.Equal (0, subView2HasFocusChangedTrueCount); + Assert.Equal (0, subView2HasFocusChangedFalseCount); + + top.Remove (subView1); // this should have the same resuilt as top.AdvanceFocus (NavigationDirection.Forward, null); + + Assert.False (subView1.HasFocus); + Assert.True (subView2.HasFocus); + + Assert.Equal (1, subView1HasFocusChangedTrueCount); + Assert.Equal (1, subView1HasFocusChangedFalseCount); + + Assert.Equal (1, subView2HasFocusChangedTrueCount); + Assert.Equal (0, subView2HasFocusChangedFalseCount); + } + + + [Fact] + public void Remove_Focused_Subview_Keeps_Focus_And_SubView_Looses_Focus () + { + View top = new View () + { + Id = "top", + CanFocus = true + }; + + View subView = new View () + { + Id = "subView", + CanFocus = true + }; + + top.Add (subView); + + top.SetFocus (); + Assert.True (top.HasFocus); + Assert.Equal (subView, top.Focused); + Assert.True (subView.HasFocus); + + top.Remove (subView); + Assert.True (top.HasFocus); + Assert.Null (top.Focused); + Assert.False (subView.HasFocus); + } + + [Fact] + public void Remove_Focused_Subview_Keeps_Focus_And_SubView_Looses_Focus_And_Next_Gets_Focus () + { + View top = new View () + { + Id = "top", + CanFocus = true + }; + + View subView1 = new View () + { + Id = "subView1", + CanFocus = true + }; + + View subView2 = new View () + { + Id = "subView2", + CanFocus = true + }; + top.Add (subView1, subView2); + + top.SetFocus (); + Assert.True (top.HasFocus); + Assert.Equal (subView1, top.Focused); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + top.Remove (subView1); + + Assert.True (top.HasFocus); + Assert.True (subView2.HasFocus); + Assert.Equal (subView2, top.Focused); + Assert.False (subView1.HasFocus); + } +} diff --git a/UnitTests/View/Navigation/AdvanceFocusTests.cs b/UnitTests/View/Navigation/AdvanceFocusTests.cs new file mode 100644 index 0000000000..63e7843cce --- /dev/null +++ b/UnitTests/View/Navigation/AdvanceFocusTests.cs @@ -0,0 +1,518 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class AdvanceFocusTests () +{ + [Fact] + public void AdvanceFocus_CanFocus_Mixed () + { + var r = new View (); + var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v2 = new View { CanFocus = false, TabStop = TabBehavior.TabStop }; + var v3 = new View { CanFocus = false, TabStop = TabBehavior.NoStop }; + + r.Add (v1, v2, v3); + + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.Dispose (); + } + + [Theory] + [CombinatorialData] + public void AdvanceFocus_Change_CanFocus_Works ([CombinatorialValues (TabBehavior.NoStop, TabBehavior.TabStop, TabBehavior.TabGroup)] TabBehavior behavior) + { + var r = new View { CanFocus = true }; + var v1 = new View (); + var v2 = new View (); + var v3 = new View (); + Assert.True (r.CanFocus); + Assert.False (v1.CanFocus); + Assert.False (v2.CanFocus); + Assert.False (v3.CanFocus); + + r.Add (v1, v2, v3); + + r.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + + v1.CanFocus = true; + v1.TabStop = behavior; + r.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.True (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + + v2.CanFocus = true; + v2.TabStop = behavior; + r.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.False (v1.HasFocus); + Assert.True (v2.HasFocus); + Assert.False (v3.HasFocus); + + v3.CanFocus = true; + v3.TabStop = behavior; + r.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.True (v3.HasFocus); + r.Dispose (); + } + + [Fact] + public void AdvanceFocus_Compound_Subview_TabStop () + { + TabBehavior behavior = TabBehavior.TabStop; + var top = new View { Id = "top", CanFocus = true }; + + var compoundSubview = new View + { + CanFocus = true, + Id = "compoundSubview", + TabStop = behavior + }; + var v1 = new View { Id = "v1", CanFocus = true, TabStop = behavior }; + var v2 = new View { Id = "v2", CanFocus = true, TabStop = behavior }; + var v3 = new View { Id = "v3", CanFocus = false, TabStop = behavior }; + + compoundSubview.Add (v1, v2, v3); + + top.Add (compoundSubview); + + // Cycle through v1 & v2 + top.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.True (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + top.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.False (v1.HasFocus); + Assert.True (v2.HasFocus); + Assert.False (v3.HasFocus); + top.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.True (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + + // Add another subview + View otherSubview = new () + { + CanFocus = true, + TabStop = behavior, + Id = "otherSubview" + }; + + top.Add (otherSubview); + + // Adding a focusable subview causes advancefocus + Assert.True (otherSubview.HasFocus); + Assert.False (v1.HasFocus); + + // Cycle through v1 & v2 + top.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.True (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + top.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.False (v1.HasFocus); + Assert.True (v2.HasFocus); + Assert.False (v3.HasFocus); + top.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + + Assert.True (otherSubview.HasFocus); + + // v2 was previously focused down the compoundSubView focus chain + top.AdvanceFocus (NavigationDirection.Forward, behavior); + Assert.False (v1.HasFocus); + Assert.True (v2.HasFocus); + Assert.False (v3.HasFocus); + + top.Dispose (); + } + + [Fact] + public void AdvanceFocus_Compound_Subview_TabGroup () + { + var top = new View { Id = "top", CanFocus = true, TabStop = TabBehavior.TabGroup }; + + var compoundSubview = new View + { + CanFocus = true, + Id = "compoundSubview", + TabStop = TabBehavior.TabGroup + }; + var tabStopView = new View { Id = "tabStop", CanFocus = true, TabStop = TabBehavior.TabStop }; + var tabGroupView1 = new View { Id = "tabGroup1", CanFocus = true, TabStop = TabBehavior.TabGroup }; + var tabGroupView2 = new View { Id = "tabGroup2", CanFocus = true, TabStop = TabBehavior.TabGroup }; + + compoundSubview.Add (tabStopView, tabGroupView1, tabGroupView2); + + top.Add (compoundSubview); + top.SetFocus (); + Assert.True (tabStopView.HasFocus); + + // TabGroup should cycle to tabGroup1 then tabGroup2 + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); + Assert.False (tabStopView.HasFocus); + Assert.True (tabGroupView1.HasFocus); + Assert.False (tabGroupView2.HasFocus); + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); + Assert.False (tabStopView.HasFocus); + Assert.False (tabGroupView1.HasFocus); + Assert.True (tabGroupView2.HasFocus); + + // Add another TabGroup subview + View otherTabGroupSubview = new () + { + CanFocus = true, + TabStop = TabBehavior.TabGroup, + Id = "otherTabGroupSubview" + }; + + top.Add (otherTabGroupSubview); + + // Adding a focusable subview causes advancefocus + Assert.True (otherTabGroupSubview.HasFocus); + Assert.False (tabStopView.HasFocus); + + // TagBroup navs to the other subview + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); + Assert.Equal (compoundSubview, top.Focused); + Assert.True (tabStopView.HasFocus); + + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); + Assert.Equal (compoundSubview, top.Focused); + Assert.True (tabGroupView1.HasFocus); + + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); + Assert.Equal (compoundSubview, top.Focused); + Assert.True (tabGroupView2.HasFocus); + + // Now go backwards + top.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup); + Assert.Equal (compoundSubview, top.Focused); + Assert.True (tabGroupView1.HasFocus); + + top.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup); + Assert.Equal (otherTabGroupSubview, top.Focused); + Assert.True (otherTabGroupSubview.HasFocus); + + top.Dispose (); + } + + [Fact] + public void AdvanceFocus_NoStop_And_CanFocus_True_No_Focus () + { + var r = new View (); + var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + + r.Add (v1, v2, v3); + + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.Dispose (); + } + + [Fact] + public void AdvanceFocus_NoStop_Change_Enables_Stop () + { + var r = new View { CanFocus = true }; + var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + + r.Add (v1, v2, v3); + + v1.TabStop = TabBehavior.TabStop; + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.True (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + + v2.TabStop = TabBehavior.TabStop; + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.True (v2.HasFocus); + Assert.False (v3.HasFocus); + + v3.TabStop = TabBehavior.TabStop; + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.True (v3.HasFocus); + r.Dispose (); + } + + [Fact] + public void AdvanceFocus_NoStop_Prevents_Stop () + { + var r = new View (); + var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; + + r.Add (v1, v2, v3); + + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + } + + [Fact] + public void AdvanceFocus_Null_And_CanFocus_False_No_Advance () + { + var r = new View (); + var v1 = new View (); + var v2 = new View (); + var v3 = new View (); + Assert.False (v1.CanFocus); + Assert.Null (v1.TabStop); + + r.Add (v1, v2, v3); + + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + r.Dispose (); + } + + [Fact] + public void AdvanceFocus_Subviews_Raises_HasFocusChanged () + { + var top = new View + { + Id = "top", + CanFocus = true + }; + + var subView1 = new View + { + Id = "subView1", + CanFocus = true + }; + + var subView2 = new View + { + Id = "subView2", + CanFocus = true + }; + top.Add (subView1, subView2); + + var subView1HasFocusChangedTrueCount = 0; + var subView1HasFocusChangedFalseCount = 0; + + subView1.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subView1HasFocusChangedTrueCount++; + } + else + { + subView1HasFocusChangedFalseCount++; + } + }; + + var subView2HasFocusChangedTrueCount = 0; + var subView2HasFocusChangedFalseCount = 0; + + subView2.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subView2HasFocusChangedTrueCount++; + } + else + { + subView2HasFocusChangedFalseCount++; + } + }; + + top.SetFocus (); + Assert.True (top.HasFocus); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + Assert.Equal (1, subView1HasFocusChangedTrueCount); + Assert.Equal (0, subView1HasFocusChangedFalseCount); + + Assert.Equal (0, subView2HasFocusChangedTrueCount); + Assert.Equal (0, subView2HasFocusChangedFalseCount); + + top.AdvanceFocus (NavigationDirection.Forward, null); + Assert.False (subView1.HasFocus); + Assert.True (subView2.HasFocus); + + Assert.Equal (1, subView1HasFocusChangedTrueCount); + Assert.Equal (1, subView1HasFocusChangedFalseCount); + + Assert.Equal (1, subView2HasFocusChangedTrueCount); + Assert.Equal (0, subView2HasFocusChangedFalseCount); + + top.AdvanceFocus (NavigationDirection.Forward, null); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + Assert.Equal (2, subView1HasFocusChangedTrueCount); + Assert.Equal (1, subView1HasFocusChangedFalseCount); + + Assert.Equal (1, subView2HasFocusChangedTrueCount); + Assert.Equal (1, subView2HasFocusChangedFalseCount); + } + + [Fact] + public void AdvanceFocus_Subviews_Raises_HasFocusChanging () + { + var top = new View + { + Id = "top", + CanFocus = true + }; + + var subView1 = new View + { + Id = "subView1", + CanFocus = true + }; + + var subView2 = new View + { + Id = "subView2", + CanFocus = true + }; + top.Add (subView1, subView2); + + var subView1HasFocusChangingTrueCount = 0; + var subView1HasFocusChangingFalseCount = 0; + + subView1.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subView1HasFocusChangingTrueCount++; + } + else + { + subView1HasFocusChangingFalseCount++; + } + }; + + var subView2HasFocusChangingTrueCount = 0; + var subView2HasFocusChangingFalseCount = 0; + + subView2.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subView2HasFocusChangingTrueCount++; + } + else + { + subView2HasFocusChangingFalseCount++; + } + }; + + top.SetFocus (); + Assert.True (top.HasFocus); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + Assert.Equal (1, subView1HasFocusChangingTrueCount); + Assert.Equal (0, subView1HasFocusChangingFalseCount); + + Assert.Equal (0, subView2HasFocusChangingTrueCount); + Assert.Equal (0, subView2HasFocusChangingFalseCount); + + top.AdvanceFocus (NavigationDirection.Forward, null); + Assert.False (subView1.HasFocus); + Assert.True (subView2.HasFocus); + + Assert.Equal (1, subView1HasFocusChangingTrueCount); + Assert.Equal (1, subView1HasFocusChangingFalseCount); + + Assert.Equal (1, subView2HasFocusChangingTrueCount); + Assert.Equal (0, subView2HasFocusChangingFalseCount); + + top.AdvanceFocus (NavigationDirection.Forward, null); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + Assert.Equal (2, subView1HasFocusChangingTrueCount); + Assert.Equal (1, subView1HasFocusChangingFalseCount); + + Assert.Equal (1, subView2HasFocusChangingTrueCount); + Assert.Equal (1, subView2HasFocusChangingFalseCount); + } + + [Fact] + public void AdvanceFocus_With_CanFocus_Are_All_True () + { + var top = new View { Id = "top", CanFocus = true }; + var v1 = new View { Id = "v1", CanFocus = true }; + var v2 = new View { Id = "v2", CanFocus = true }; + var v3 = new View { Id = "v3", CanFocus = true }; + + top.Add (v1, v2, v3); + + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.True (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.False (v3.HasFocus); + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.True (v2.HasFocus); + Assert.False (v3.HasFocus); + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.False (v1.HasFocus); + Assert.False (v2.HasFocus); + Assert.True (v3.HasFocus); + top.Dispose (); + } + + [Theory] + [CombinatorialData] + public void TabStop_And_CanFocus_Are_Decoupled (bool canFocus, TabBehavior tabStop) + { + var view = new View { CanFocus = canFocus, TabStop = tabStop }; + + Assert.Equal (canFocus, view.CanFocus); + Assert.Equal (tabStop, view.TabStop); + } +} diff --git a/UnitTests/View/Navigation/CanFocusTests.cs b/UnitTests/View/Navigation/CanFocusTests.cs new file mode 100644 index 0000000000..474039a335 --- /dev/null +++ b/UnitTests/View/Navigation/CanFocusTests.cs @@ -0,0 +1,653 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class CanFocusTests () : TestsAllViews +{ + [Fact] + public void CanFocus_False_Prevents_SubSubView_HasFocus () + { + var view = new View { }; + var subView = new View { }; + var subSubView = new View { CanFocus = true }; + + subView.Add (subSubView); + view.Add (subView); + + Assert.False (view.CanFocus); + Assert.False (subView.CanFocus); + Assert.True (subSubView.CanFocus); + + view.SetFocus (); + Assert.False (view.HasFocus); + + subView.SetFocus (); + Assert.False (subView.HasFocus); + + subSubView.SetFocus (); + Assert.False (subSubView.HasFocus); + } + + [Fact] + public void CanFocus_False_Prevents_SubView_HasFocus () + { + var view = new View { }; + var subView = new View { CanFocus = true }; + var subSubView = new View { }; + + subView.Add (subSubView); + view.Add (subView); + + Assert.False (view.CanFocus); + Assert.True (subView.CanFocus); + Assert.False (subSubView.CanFocus); + + view.SetFocus (); + Assert.False (view.HasFocus); + + subView.SetFocus (); + Assert.False (subView.HasFocus); + + subSubView.SetFocus (); + Assert.False (subSubView.HasFocus); + } + + [Fact] + public void CanFocus_Set_True_No_SuperView_Doesnt_Set_HasFocus () + { + var view = new View { }; + + // Act + view.CanFocus = true; + Assert.False (view.HasFocus); + } + + [Fact] + public void CanFocus_Set_True_Sets_HasFocus_To_True () + { + var view = new View { }; + var subView = new View { }; + view.Add (subView); + + Assert.False (view.CanFocus); + Assert.False (subView.CanFocus); + + view.SetFocus (); + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + + view.CanFocus = true; + view.SetFocus (); + Assert.True (view.HasFocus); + + // Act + subView.CanFocus = true; + Assert.True (subView.HasFocus); + } + + [Fact] + public void CanFocus_Set_SubView_True_Sets_HasFocus_To_True () + { + var view = new View + { + CanFocus = true + }; + var subView = new View + { + CanFocus = false + }; + var subSubView = new View + { + CanFocus = true + }; + + subView.Add (subSubView); + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.False (subView.HasFocus); + Assert.False (subSubView.HasFocus); + + // Act + subView.CanFocus = true; + Assert.True (subView.HasFocus); + Assert.True (subSubView.HasFocus); + } + + + [Fact] + public void CanFocus_Set_SubView_True_Does_Not_Change_Focus_If_SuperView_Focused_Is_True () + { + var top = new View + { + Id = "top", + CanFocus = true + }; + var subView = new View + { + Id = "subView", + CanFocus = true + }; + var subSubView = new View + { + Id = "subSubView", + CanFocus = true + }; + + subView.Add (subSubView); + + var subView2 = new View + { + Id = "subView2", + CanFocus = false + }; + + top.Add (subView, subView2); + + top.SetFocus (); + Assert.True (top.HasFocus); + Assert.Equal (subView, top.Focused); + Assert.True (subView.HasFocus); + Assert.True (subSubView.HasFocus); + + // Act + subView2.CanFocus = true; + Assert.False (subView2.HasFocus); + Assert.True (subView.HasFocus); + Assert.True (subSubView.HasFocus); + } + + [Fact] + public void CanFocus_Set_False_Sets_HasFocus_To_False () + { + var view = new View { CanFocus = true }; + var view2 = new View { CanFocus = true }; + view2.Add (view); + + Assert.True (view.CanFocus); + + view.SetFocus (); + Assert.True (view.HasFocus); + + view.CanFocus = false; + Assert.False (view.CanFocus); + Assert.False (view.HasFocus); + } + + // TODO: Figure out what this test is supposed to be testing + [Fact] + public void CanFocus_Faced_With_Container () + { + var t = new Toplevel (); + var w = new Window (); + var f = new FrameView (); + var v = new View { CanFocus = true }; + f.Add (v); + w.Add (f); + t.Add (w); + + Assert.True (t.CanFocus); + Assert.True (w.CanFocus); + Assert.True (f.CanFocus); + Assert.True (v.CanFocus); + + f.CanFocus = false; + Assert.False (f.CanFocus); + Assert.True (v.CanFocus); + + v.CanFocus = false; + Assert.False (f.CanFocus); + Assert.False (v.CanFocus); + + v.CanFocus = true; + Assert.False (f.CanFocus); + Assert.True (v.CanFocus); + } + + // TODO: Figure out what this test is supposed to be testing + [Fact] + public void CanFocus_Faced_With_Container_Before_Run () + { + Application.Init (new FakeDriver ()); + + Toplevel t = new (); + + var w = new Window (); + var f = new FrameView (); + var v = new View { CanFocus = true }; + f.Add (v); + w.Add (f); + t.Add (w); + + Assert.True (t.CanFocus); + Assert.True (w.CanFocus); + Assert.True (f.CanFocus); + Assert.True (v.CanFocus); + + f.CanFocus = false; + Assert.False (f.CanFocus); + Assert.True (v.CanFocus); + + v.CanFocus = false; + Assert.False (f.CanFocus); + Assert.False (v.CanFocus); + + v.CanFocus = true; + Assert.False (f.CanFocus); + Assert.True (v.CanFocus); + + Application.Iteration += (s, a) => Application.RequestStop (); + + Application.Run (t); + t.Dispose (); + Application.Shutdown (); + } + + //[Fact] + //public void CanFocus_Set_Changes_TabIndex_And_TabStop () + //{ + // var r = new View (); + // var v1 = new View { Text = "1" }; + // var v2 = new View { Text = "2" }; + // var v3 = new View { Text = "3" }; + + // r.Add (v1, v2, v3); + + // v2.CanFocus = true; + // Assert.Equal (r.TabIndexes.IndexOf (v2), v2.TabIndex); + // Assert.Equal (0, v2.TabIndex); + // Assert.Equal (TabBehavior.TabStop, v2.TabStop); + + // v1.CanFocus = true; + // Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); + // Assert.Equal (1, v1.TabIndex); + // Assert.Equal (TabBehavior.TabStop, v1.TabStop); + + // v1.TabIndex = 2; + // Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); + // Assert.Equal (1, v1.TabIndex); + // v3.CanFocus = true; + // Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); + // Assert.Equal (1, v1.TabIndex); + // Assert.Equal (r.TabIndexes.IndexOf (v3), v3.TabIndex); + // Assert.Equal (2, v3.TabIndex); + // Assert.Equal (TabBehavior.TabStop, v3.TabStop); + + // v2.CanFocus = false; + // Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); + // Assert.Equal (1, v1.TabIndex); + // Assert.Equal (TabBehavior.TabStop, v1.TabStop); + // Assert.Equal (r.TabIndexes.IndexOf (v2), v2.TabIndex); // TabIndex is not changed + // Assert.NotEqual (-1, v2.TabIndex); + // Assert.Equal (TabBehavior.TabStop, v2.TabStop); // TabStop is not changed + // Assert.Equal (r.TabIndexes.IndexOf (v3), v3.TabIndex); + // Assert.Equal (2, v3.TabIndex); + // Assert.Equal (TabBehavior.TabStop, v3.TabStop); + // r.Dispose (); + //} + + [Fact] + public void CanFocus_True_Focuses () + { + View view = new () + { + Id = "view" + }; + + View superView = new () + { + Id = "superView", + CanFocus = true + }; + + superView.Add (view); + + superView.SetFocus (); + Assert.True (superView.HasFocus); + Assert.NotEqual (view, superView.Focused); + + view.CanFocus = true; + Assert.True (superView.HasFocus); + Assert.Equal (view, superView.Focused); + Assert.True (view.HasFocus); + + view.CanFocus = false; + Assert.True (superView.HasFocus); + Assert.NotEqual (view, superView.Focused); + Assert.False (view.HasFocus); + } + + + [Fact] + public void CanFocus_Set_True_Get_AdvanceFocus_Works () + { + Label label = new () { Text = "label" }; + View view = new () { Text = "view", CanFocus = true }; + Application.Navigation = new (); + Application.Current = new (); + Application.Current.Add (label, view); + + Application.Current.SetFocus (); + Assert.Equal (view, Application.Navigation.GetFocused()); + Assert.False (label.CanFocus); + Assert.False (label.HasFocus); + Assert.True (view.CanFocus); + Assert.True (view.HasFocus); + + Assert.False (Application.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); + Assert.False (label.HasFocus); + Assert.True (view.HasFocus); + + // Set label CanFocus to true + label.CanFocus = true; + Assert.False (label.HasFocus); + Assert.True (view.HasFocus); + + // label can now be focused, so AdvanceFocus should move to it. + Assert.True (Application.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); + Assert.True (label.HasFocus); + Assert.False (view.HasFocus); + + // Move back to view + view.SetFocus (); + Assert.False (label.HasFocus); + Assert.True (view.HasFocus); + + Assert.True (Application.OnKeyDown (Key.Tab)); + Assert.True (label.HasFocus); + Assert.False (view.HasFocus); + + Application.Current.Dispose (); + Application.ResetState (); + } + +#if V2_NEW_FOCUS_IMPL // Bogus test - depends on auto CanFocus behavior + + [Fact] + public void CanFocus_Container_ToFalse_Turns_All_Subviews_ToFalse_Too () + { + Application.Init (new FakeDriver ()); + + Toplevel t = new (); + + var w = new Window (); + var f = new FrameView (); + var v1 = new View { CanFocus = true }; + var v2 = new View { CanFocus = true }; + f.Add (v1, v2); + w.Add (f); + t.Add (w); + + t.Ready += (s, e) => + { + Assert.True (t.CanFocus); + Assert.True (w.CanFocus); + Assert.True (f.CanFocus); + Assert.True (v1.CanFocus); + Assert.True (v2.CanFocus); + + w.CanFocus = false; + Assert.False (w.CanFocus); + Assert.False (f.CanFocus); + Assert.False (v1.CanFocus); + Assert.False (v2.CanFocus); + }; + + Application.Iteration += (s, a) => Application.RequestStop (); + + Application.Run (t); + t.Dispose (); + Application.Shutdown (); + } +#endif + +#if V2_NEW_FOCUS_IMPL // Bogus test - depends on auto CanFocus behavior + + [Fact] + public void CanFocus_Container_Toggling_All_Subviews_To_Old_Value_When_Is_True () + { + Application.Init (new FakeDriver ()); + + Toplevel t = new (); + + var w = new Window (); + var f = new FrameView (); + var v1 = new View (); + var v2 = new View { CanFocus = true }; + f.Add (v1, v2); + w.Add (f); + t.Add (w); + + t.Ready += (s, e) => + { + Assert.True (t.CanFocus); + Assert.True (w.CanFocus); + Assert.True (f.CanFocus); + Assert.False (v1.CanFocus); + Assert.True (v2.CanFocus); + + w.CanFocus = false; + Assert.False (w.CanFocus); + Assert.False (f.CanFocus); + Assert.False (v1.CanFocus); + Assert.False (v2.CanFocus); + + w.CanFocus = true; + Assert.True (w.CanFocus); + Assert.True (f.CanFocus); + Assert.False (v1.CanFocus); + Assert.True (v2.CanFocus); + }; + + Application.Iteration += (s, a) => Application.RequestStop (); + + Application.Run (t); + t.Dispose (); + Application.Shutdown (); + } +#endif +#if V2_NEW_FOCUS_IMPL // Bogus test - depends on auto CanFocus behavior + [Fact] + public void CanFocus_Faced_With_Container_After_Run () + { + Application.Init (new FakeDriver ()); + + Toplevel t = new (); + + var w = new Window (); + var f = new FrameView (); + var v = new View { CanFocus = true }; + f.Add (v); + w.Add (f); + t.Add (w); + + t.Ready += (s, e) => + { + Assert.True (t.CanFocus); + Assert.True (w.CanFocus); + Assert.True (f.CanFocus); + Assert.True (v.CanFocus); + + f.CanFocus = false; + Assert.False (f.CanFocus); + Assert.False (v.CanFocus); + + v.CanFocus = false; + Assert.False (f.CanFocus); + Assert.False (v.CanFocus); + + Assert.Throws (() => v.CanFocus = true); + Assert.False (f.CanFocus); + Assert.False (v.CanFocus); + + f.CanFocus = true; + Assert.True (f.CanFocus); + Assert.True (v.CanFocus); + }; + + Application.Iteration += (s, a) => Application.RequestStop (); + + Application.Run (t); + t.Dispose (); + Application.Shutdown (); + } +#endif +#if V2_NEW_FOCUS_IMPL + + [Fact] + [AutoInitShutdown] + public void CanFocus_Sets_To_False_On_Single_View_Focus_View_On_Another_Toplevel () + { + var view1 = new View { Id = "view1", Width = 10, Height = 1, CanFocus = true }; + var win1 = new Window { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () }; + win1.Add (view1); + var view2 = new View { Id = "view2", Width = 20, Height = 2, CanFocus = true }; + var win2 = new Window { Id = "win2", X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () }; + win2.Add (view2); + var top = new Toplevel (); + top.Add (win1, win2); + Application.Begin (top); + + Assert.True (view1.CanFocus); + Assert.True (view1.HasFocus); + Assert.True (view2.CanFocus); + Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus + + Assert.True (Application.OnKeyDown (Key.F6)); + Assert.True (view1.CanFocus); + Assert.False (view1.HasFocus); // Only one of the most focused toplevels view can have focus + Assert.True (view2.CanFocus); + Assert.True (view2.HasFocus); + + Assert.True (Application.OnKeyDown (Key.F6)); + Assert.True (view1.CanFocus); + Assert.True (view1.HasFocus); + Assert.True (view2.CanFocus); + Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus + + view1.CanFocus = false; + Assert.False (view1.CanFocus); + Assert.False (view1.HasFocus); + Assert.True (view2.CanFocus); + Assert.True (view2.HasFocus); + Assert.Equal (win2, Application.Current.GetFocused ()); + Assert.Equal (view2, Application.Current.GetMostFocused ()); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void CanFocus_Sets_To_False_On_Toplevel_Focus_View_On_Another_Toplevel () + { + var view1 = new View { Id = "view1", Width = 10, Height = 1, CanFocus = true }; + var win1 = new Window { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () }; + win1.Add (view1); + var view2 = new View { Id = "view2", Width = 20, Height = 2, CanFocus = true }; + var win2 = new Window { Id = "win2", X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () }; + win2.Add (view2); + var top = new Toplevel (); + top.Add (win1, win2); + Application.Begin (top); + + Assert.True (view1.CanFocus); + Assert.True (view1.HasFocus); + Assert.True (view2.CanFocus); + Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus + + Assert.True (Application.OnKeyDown (Key.F6)); + Assert.True (view1.CanFocus); + Assert.False (view1.HasFocus); // Only one of the most focused toplevels view can have focus + Assert.True (view2.CanFocus); + Assert.True (view2.HasFocus); + + Assert.True (Application.OnKeyDown (Key.F6)); + Assert.True (view1.CanFocus); + Assert.True (view1.HasFocus); + Assert.True (view2.CanFocus); + Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus + + win1.CanFocus = false; + Assert.False (view1.CanFocus); + Assert.False (view1.HasFocus); + Assert.False (win1.CanFocus); + Assert.False (win1.HasFocus); + Assert.True (view2.CanFocus); + Assert.True (view2.HasFocus); + Assert.Equal (win2, Application.Current.GetFocused ()); + Assert.Equal (view2, Application.Current.GetMostFocused ()); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void CanFocus_Sets_To_False_With_Two_Views_Focus_Another_View_On_The_Same_Toplevel () + { + var view1 = new View { Id = "view1", Width = 10, Height = 1, CanFocus = true }; + + var view12 = new View + { + Id = "view12", + Y = 5, + Width = 10, + Height = 1, + CanFocus = true + }; + var win1 = new Window { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () }; + win1.Add (view1, view12); + var view2 = new View { Id = "view2", Width = 20, Height = 2, CanFocus = true }; + var win2 = new Window { Id = "win2", X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () }; + win2.Add (view2); + var top = new Toplevel (); + top.Add (win1, win2); + Application.Begin (top); + + Assert.True (view1.CanFocus); + Assert.True (view1.HasFocus); + Assert.True (view2.CanFocus); + Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus + + Assert.True (Application.OnKeyDown (Key.F6)); // move to win2 + Assert.True (view1.CanFocus); + Assert.False (view1.HasFocus); // Only one of the most focused toplevels view can have focus + Assert.True (view2.CanFocus); + Assert.True (view2.HasFocus); + + Assert.True (Application.OnKeyDown (Key.F6)); + Assert.True (view1.CanFocus); + Assert.True (view1.HasFocus); + Assert.True (view2.CanFocus); + Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus + + view1.CanFocus = false; + Assert.False (view1.CanFocus); + Assert.False (view1.HasFocus); + Assert.True (view2.CanFocus); + Assert.False (view2.HasFocus); + Assert.Equal (win1, Application.Current.GetFocused ()); + Assert.Equal (view12, Application.Current.GetMostFocused ()); + top.Dispose (); + } +#endif + + [Fact (Skip = "Causes crash on Ubuntu in Github Action. Bogus test anyway.")] + public void WindowDispose_CanFocusProblem () + { + // Arrange + Application.Init (); + using var top = new Toplevel (); + using var view = new View { X = 0, Y = 1, Text = nameof (WindowDispose_CanFocusProblem) }; + using var window = new Window (); + top.Add (window); + window.Add (view); + + // Act + RunState rs = Application.Begin (top); + Application.End (rs); + top.Dispose (); + Application.Shutdown (); + + // Assert does Not throw NullReferenceException + top.SetFocus (); + } +} diff --git a/UnitTests/View/Navigation/EnabledTests.cs b/UnitTests/View/Navigation/EnabledTests.cs new file mode 100644 index 0000000000..24bb0c2062 --- /dev/null +++ b/UnitTests/View/Navigation/EnabledTests.cs @@ -0,0 +1,317 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class EnabledTests () : TestsAllViews +{ + [Fact] + public void Enabled_False_Leaves () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.SetFocus (); + Assert.True (view.HasFocus); + + view.Enabled = false; + Assert.False (view.HasFocus); + } + + [Fact] + public void Enabled_False_Leaves_Subview () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + + view.Enabled = false; + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + } + + [Fact] + public void Enabled_False_Leaves_Subview2 () + { + var view = new Window + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + + view.Enabled = false; + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + } + + [Fact] + public void Enabled_False_On_Subview_Leaves_Just_Subview () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + + subView.Enabled = false; + Assert.True (view.HasFocus); + Assert.False (subView.HasFocus); + } + + [Fact] + public void Enabled_False_Focuses_Deepest_Focusable_Subview () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + var subViewSubView1 = new View + { + Id = "subViewSubView1", + CanFocus = false + }; + + var subViewSubView2 = new View + { + Id = "subViewSubView2", + CanFocus = true + }; + + var subViewSubView3 = new View + { + Id = "subViewSubView3", + CanFocus = true // This is the one that will be focused + }; + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.Equal (subViewSubView2, subView.Focused); + + subViewSubView2.Enabled = false; + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.Equal (subViewSubView3, subView.Focused); + Assert.True (subViewSubView3.HasFocus); + } + + [Fact] + public void Enabled_True_Subview_Focuses_SubView () + { + var view = new View + { + Id = "view", + CanFocus = true, + Enabled = false + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + view.Add (subView); + + view.SetFocus (); + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + + view.Enabled = true; + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + } + + [Fact] + public void Enabled_True_On_Subview_Focuses () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true, + Enabled = false + }; + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.False (subView.HasFocus); + + subView.Enabled = true; + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + } + + [Fact] + public void Enabled_True_Focuses_Deepest_Focusable_Subview () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true, + Enabled = false + }; + + var subViewSubView1 = new View + { + Id = "subViewSubView1", + CanFocus = false + }; + + var subViewSubView2 = new View + { + Id = "subViewSubView2", + CanFocus = true // This is the one that will be focused + }; + + var subViewSubView3 = new View + { + Id = "subViewSubView3", + CanFocus = true + }; + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.False (subView.HasFocus); + Assert.False (subViewSubView2.HasFocus); + + subView.Enabled = true; + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.Equal (subViewSubView2, subView.Focused); + Assert.True (subViewSubView2.HasFocus); + } + + [Fact] + [AutoInitShutdown] + public void _Enabled_Sets_Also_Sets_Subviews () + { + var wasClicked = false; + var button = new Button { Text = "Click Me" }; + button.IsDefault = true; + button.Accept += (s, e) => wasClicked = !wasClicked; + var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; + win.Add (button); + var top = new Toplevel (); + top.Add (win); + + var iterations = 0; + + Application.Iteration += (s, a) => + { + iterations++; + + win.NewKeyDownEvent (Key.Enter); + Assert.True (wasClicked); + button.NewMouseEvent (new () { Flags = MouseFlags.Button1Clicked }); + Assert.False (wasClicked); + Assert.True (button.Enabled); + Assert.True (button.CanFocus); + Assert.True (button.HasFocus); + Assert.True (win.Enabled); + Assert.True (win.CanFocus); + Assert.True (win.HasFocus); + + Assert.True (button.HasFocus); + win.Enabled = false; + Assert.False (button.HasFocus); + button.NewKeyDownEvent (Key.Enter); + Assert.False (wasClicked); + button.NewMouseEvent (new () { Flags = MouseFlags.Button1Clicked }); + Assert.False (wasClicked); + Assert.False (button.Enabled); + Assert.True (button.CanFocus); + Assert.False (button.HasFocus); + Assert.False (win.Enabled); + Assert.True (win.CanFocus); + Assert.False (win.HasFocus); + button.SetFocus (); + Assert.False (button.HasFocus); + Assert.False (win.HasFocus); + win.SetFocus (); + Assert.False (button.HasFocus); + Assert.False (win.HasFocus); + + win.Enabled = true; + win.FocusDeepest (NavigationDirection.Forward, null); + Assert.True (button.HasFocus); + Assert.True (win.HasFocus); + + Application.RequestStop (); + }; + + Application.Run (top); + + Assert.Equal (1, iterations); + top.Dispose (); + } +} diff --git a/UnitTests/View/Navigation/HasFocusChangeEventTests.cs b/UnitTests/View/Navigation/HasFocusChangeEventTests.cs new file mode 100644 index 0000000000..24e4d44785 --- /dev/null +++ b/UnitTests/View/Navigation/HasFocusChangeEventTests.cs @@ -0,0 +1,1045 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class HasFocusChangeEventTests () : TestsAllViews +{ + #region HasFocusChanging_NewValue_True + + [Fact] + public void HasFocusChanging_SetFocus_Raises () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + } + else + { + hasFocusFalseCount++; + } + }; + + Assert.True (view.CanFocus); + Assert.False (view.HasFocus); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + } + + + [Fact] + public void HasFocusChanging_SetFocus_SubView_SetFocus_Raises () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + } + else + { + hasFocusFalseCount++; + } + }; + + var subviewHasFocusTrueCount = 0; + var subviewHasFocusFalseCount = 0; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + subview.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewHasFocusTrueCount++; + } + else + { + subviewHasFocusFalseCount++; + } + }; + + view.Add (subview); + + view.SetFocus (); + + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (1, subviewHasFocusTrueCount); + Assert.Equal (0, subviewHasFocusFalseCount); + } + + + [Fact] + public void HasFocusChanging_SetFocus_On_SubView_SubView_SetFocus_Raises () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + } + else + { + hasFocusFalseCount++; + } + }; + + var subviewHasFocusTrueCount = 0; + var subviewHasFocusFalseCount = 0; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + subview.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewHasFocusTrueCount++; + } + else + { + subviewHasFocusFalseCount++; + } + }; + + view.Add (subview); + + subview.SetFocus (); + + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (1, subviewHasFocusTrueCount); + Assert.Equal (0, subviewHasFocusFalseCount); + } + + [Fact] + public void HasFocusChanging_SetFocus_CompoundSubView_Raises () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + } + else + { + hasFocusFalseCount++; + } + }; + + var subViewEnterCount = 0; + var subViewLeaveCount = 0; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + subView.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subViewEnterCount++; + } + else + { + subViewLeaveCount++; + } + }; + + var subviewSubView1EnterCount = 0; + var subviewSubView1LeaveCount = 0; + + var subViewSubView1 = new View + { + Id = "subViewSubView1", + CanFocus = false + }; + + subViewSubView1.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewSubView1EnterCount++; + } + else + { + subviewSubView1LeaveCount++; + } + }; + + var subviewSubView2EnterCount = 0; + var subviewSubView2LeaveCount = 0; + + var subViewSubView2 = new View + { + Id = "subViewSubView2", + CanFocus = true + }; + + subViewSubView2.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewSubView2EnterCount++; + } + else + { + subviewSubView2EnterCount++; + } + }; + + var subviewSubView3EnterCount = 0; + var subviewSubView3LeaveCount = 0; + + var subViewSubView3 = new View + { + Id = "subViewSubView3", + CanFocus = false + }; + + subViewSubView3.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewSubView3EnterCount++; + } + else + { + subviewSubView3LeaveCount++; + } + }; + + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.True (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (1, subViewEnterCount); + Assert.Equal (0, subViewLeaveCount); + + Assert.Equal (0, subviewSubView1EnterCount); + Assert.Equal (0, subviewSubView1LeaveCount); + + Assert.Equal (1, subviewSubView2EnterCount); + Assert.Equal (0, subviewSubView2LeaveCount); + + Assert.Equal (0, subviewSubView3EnterCount); + Assert.Equal (0, subviewSubView3LeaveCount); + } + + [Fact] + public void HasFocusChanging_Can_Cancel () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + e.Cancel = true; + } + else + { + hasFocusFalseCount++; + } + }; + + var subviewHasFocusTrueCount = 0; + var subviewHasFocusFalseCount = 0; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + subview.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewHasFocusTrueCount++; + } + else + { + subviewHasFocusFalseCount++; + } + }; + + view.Add (subview); + + view.SetFocus (); + + Assert.False (view.HasFocus); + Assert.False (subview.HasFocus); + + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (0, subviewHasFocusTrueCount); + Assert.Equal (0, subviewHasFocusFalseCount); + } + + [Fact] + public void HasFocusChanging_SetFocus_On_SubView_Can_Cancel () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + e.Cancel = true; + } + else + { + hasFocusFalseCount++; + } + }; + + var subviewHasFocusTrueCount = 0; + var subviewHasFocusFalseCount = 0; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + subview.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewHasFocusTrueCount++; + } + else + { + subviewHasFocusFalseCount++; + } + }; + + view.Add (subview); + + subview.SetFocus (); + + Assert.False (view.HasFocus); + Assert.False (subview.HasFocus); + + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (1, subviewHasFocusTrueCount); + Assert.Equal (0, subviewHasFocusFalseCount); + } + + [Fact] + public void HasFocusChanging_SubView_Can_Cancel () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + } + else + { + hasFocusFalseCount++; + } + }; + + var subviewHasFocusTrueCount = 0; + var subviewHasFocusFalseCount = 0; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + subview.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewHasFocusTrueCount++; + e.Cancel = true; + } + else + { + subviewHasFocusFalseCount++; + } + }; + + view.Add (subview); + + view.SetFocus (); + + Assert.True (view.HasFocus); + Assert.False (subview.HasFocus); + + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (1, subviewHasFocusTrueCount); + Assert.Equal (0, subviewHasFocusFalseCount); + } + + + [Fact] + public void HasFocusChanging_SetFocus_On_Subview_If_SubView_Cancels () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + } + else + { + hasFocusFalseCount++; + } + }; + + var subviewHasFocusTrueCount = 0; + var subviewHasFocusFalseCount = 0; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + subview.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewHasFocusTrueCount++; + e.Cancel = true; + } + else + { + subviewHasFocusFalseCount++; + } + }; + + view.Add (subview); + + subview.SetFocus (); + + Assert.False (view.HasFocus); // Never had focus + Assert.False (subview.HasFocus); // Cancelled + + Assert.Equal (0, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (1, subviewHasFocusTrueCount); + Assert.Equal (0, subviewHasFocusFalseCount); + + // Now set focus on the view + view.SetFocus (); + + Assert.True (view.HasFocus); + Assert.False (subview.HasFocus); // Cancelled + + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (2, subviewHasFocusTrueCount); + Assert.Equal (0, subviewHasFocusFalseCount); + } + + #endregion HasFocusChanging_NewValue_True + + #region HasFocusChanging_NewValue_False + + [Fact] + public void HasFocusChanging_RemoveFocus_Raises () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + } + else + { + hasFocusFalseCount++; + } + }; + + Assert.True (view.CanFocus); + Assert.False (view.HasFocus); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + view.HasFocus = false; + Assert.False (view.HasFocus); + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (1, hasFocusFalseCount); + } + + + [Fact] + public void HasFocusChanging_RemoveFocus_SubView_SetFocus_Raises () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + } + else + { + hasFocusFalseCount++; + } + }; + + var subviewHasFocusTrueCount = 0; + var subviewHasFocusFalseCount = 0; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + subview.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewHasFocusTrueCount++; + } + else + { + subviewHasFocusFalseCount++; + } + }; + + view.Add (subview); + + view.SetFocus (); + + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (1, subviewHasFocusTrueCount); + Assert.Equal (0, subviewHasFocusFalseCount); + + view.HasFocus = false; + Assert.False (view.HasFocus); + Assert.False (subview.HasFocus); + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (1, hasFocusFalseCount); + + Assert.Equal (1, subviewHasFocusTrueCount); + Assert.Equal (1, subviewHasFocusFalseCount); + } + + + + [Fact] + public void HasFocusChanging_RemoveFocus_On_SubView_SubView_SetFocus_Raises () + { + var hasFocusTrueCount = 0; + var hasFocusFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + hasFocusTrueCount++; + } + else + { + hasFocusFalseCount++; + } + }; + + var subviewHasFocusTrueCount = 0; + var subviewHasFocusFalseCount = 0; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + subview.HasFocusChanging += (s, e) => + { + if (e.NewValue) + { + subviewHasFocusTrueCount++; + } + else + { + subviewHasFocusFalseCount++; + } + }; + + view.Add (subview); + + subview.SetFocus (); + + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (1, subviewHasFocusTrueCount); + Assert.Equal (0, subviewHasFocusFalseCount); + + subview.HasFocus = false; + Assert.False (subview.HasFocus); + Assert.Equal (1, hasFocusTrueCount); + Assert.Equal (0, hasFocusFalseCount); + + Assert.Equal (1, subviewHasFocusTrueCount); + Assert.Equal (1, subviewHasFocusFalseCount); + + } + + #endregion HasFocusChanging_NewValue_False + + #region HasFocusChanged + + [Fact] + public void HasFocusChanged_RemoveFocus_Raises () + { + var newValueTrueCount = 0; + var newValueFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + newValueTrueCount++; + } + else + { + newValueFalseCount++; + } + }; + + Assert.True (view.CanFocus); + Assert.False (view.HasFocus); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.Equal (1, newValueTrueCount); + Assert.Equal (0, newValueFalseCount); + + view.HasFocus = false; + Assert.Equal (1, newValueTrueCount); + Assert.Equal (1, newValueFalseCount); + } + + + [Fact] + public void HasFocusChanged_With_SubView_Raises () + { + var newValueTrueCount = 0; + var newValueFalseCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + newValueTrueCount++; + } + else + { + newValueFalseCount++; + } + }; + + var subviewNewValueTrueCount = 0; + var subviewNewValueFalseCount = 0; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + subview.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subviewNewValueTrueCount++; + } + else + { + subviewNewValueFalseCount++; + } + }; + + view.Add (subview); + + view.SetFocus (); + Assert.Equal (1, newValueTrueCount); + Assert.Equal (0, newValueFalseCount); + + Assert.Equal (1, subviewNewValueTrueCount); + Assert.Equal (0, subviewNewValueFalseCount); + + view.HasFocus = false; + + Assert.Equal (1, newValueTrueCount); + Assert.Equal (1, newValueFalseCount); + + Assert.Equal (1, subviewNewValueTrueCount); + Assert.Equal (1, subviewNewValueFalseCount); + + view.SetFocus (); + + Assert.Equal (2, newValueTrueCount); + Assert.Equal (1, newValueFalseCount); + + Assert.Equal (2, subviewNewValueTrueCount); + Assert.Equal (1, subviewNewValueFalseCount); + + subview.HasFocus = false; + + Assert.Equal (2, newValueTrueCount); + Assert.Equal (1, newValueFalseCount); + + Assert.Equal (2, subviewNewValueTrueCount); + Assert.Equal (2, subviewNewValueFalseCount); + } + + + [Fact] + public void HasFocusChanged_CompoundSubView_Raises () + { + var viewEnterCount = 0; + var viewLeaveCount = 0; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + viewEnterCount++; + } + else + { + viewLeaveCount++; + } + }; + + var subViewEnterCount = 0; + var subViewLeaveCount = 0; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + subView.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subViewEnterCount++; + } + else + { + subViewLeaveCount++; + } + }; + + var subviewSubView1EnterCount = 0; + var subviewSubView1LeaveCount = 0; + + var subViewSubView1 = new View + { + Id = "subViewSubView1", + CanFocus = false + }; + + subViewSubView1.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subviewSubView1EnterCount++; + } + else + { + subviewSubView1LeaveCount++; + } + }; + + var subviewSubView2EnterCount = 0; + var subviewSubView2LeaveCount = 0; + + var subViewSubView2 = new View + { + Id = "subViewSubView2", + CanFocus = true + }; + + subViewSubView2.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subviewSubView2EnterCount++; + } + else + { + subviewSubView2LeaveCount++; + } + }; + var subviewSubView3EnterCount = 0; + var subviewSubView3LeaveCount = 0; + + var subViewSubView3 = new View + { + Id = "subViewSubView3", + CanFocus = false + }; + + subViewSubView3.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subviewSubView3EnterCount++; + } + else + { + subviewSubView3LeaveCount++; + } + }; + + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.True (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + + view.HasFocus = false; + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.False (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + + Assert.Equal (1, viewEnterCount); + Assert.Equal (1, viewLeaveCount); + + Assert.Equal (1, subViewEnterCount); + Assert.Equal (1, subViewLeaveCount); + + Assert.Equal (0, subviewSubView1EnterCount); + Assert.Equal (0, subviewSubView1LeaveCount); + + Assert.Equal (1, subviewSubView2EnterCount); + Assert.Equal (1, subviewSubView2LeaveCount); + + Assert.Equal (0, subviewSubView3EnterCount); + Assert.Equal (0, subviewSubView3LeaveCount); + } + + + [Fact] + public void HasFocusChanged_NewValue_False_Hide_Subview () + { + var subView1 = new View + { + Id = $"subView1", + CanFocus = true + }; + + var subView2 = new View + { + Id = $"subView2", + CanFocus = true + }; + + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + Assert.True (view.HasFocus); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + subView1.Visible = true; + subView2.Visible = false; + + Assert.True (view.HasFocus); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + } + else + { + Assert.False (view.HasFocus); + Assert.False (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + subView1.Visible = false; + subView2.Visible = true; + } + }; + + view.Add (subView1, subView2); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + view.HasFocus = false; + Assert.False (view.HasFocus); + Assert.False (subView1.HasFocus); + Assert.False (subView2.HasFocus); + } + + #endregion HasFocusChanged +} diff --git a/UnitTests/View/Navigation/HasFocusTests.cs b/UnitTests/View/Navigation/HasFocusTests.cs new file mode 100644 index 0000000000..258c408e9d --- /dev/null +++ b/UnitTests/View/Navigation/HasFocusTests.cs @@ -0,0 +1,233 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class HasFocusTests () : TestsAllViews +{ + + [Fact] + public void HasFocus_False () + { + var view = new View () + { + Id = "view", + CanFocus = true + }; + + view.SetFocus (); + Assert.True (view.HasFocus); + + view.HasFocus = false; + Assert.False (view.HasFocus); + } + + [Fact] + public void HasFocus_False_WithSuperView_Does_Not_Change_SuperView () + { + var view = new View () + { + Id = "view", + CanFocus = true + }; + + var subview = new View () + { + Id = "subview", + CanFocus = true + }; + view.Add (subview); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subview.HasFocus); + + subview.HasFocus = false; + Assert.False (subview.HasFocus); + Assert.True (view.HasFocus); + } + + [Fact] + public void HasFocus_False_WithSubview_Leaves_All () + { + var view = new View () + { + Id = "view", + CanFocus = true + }; + + var subview = new View () + { + Id = "subview", + CanFocus = true + }; + view.Add (subview); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subview.HasFocus); + Assert.Equal (subview, view.Focused); + + view.HasFocus = false; + Assert.Null (view.Focused); + Assert.False (view.HasFocus); + Assert.False (subview.HasFocus); + } + + + + [Fact] + public void Enabled_False_Sets_HasFocus_To_False () + { + var wasClicked = false; + var view = new Button { Text = "Click Me" }; + view.Accept += (s, e) => wasClicked = !wasClicked; + + view.NewKeyDownEvent (Key.Space); + Assert.True (wasClicked); + view.NewMouseEvent (new () { Flags = MouseFlags.Button1Clicked }); + Assert.False (wasClicked); + Assert.True (view.Enabled); + Assert.True (view.CanFocus); + Assert.True (view.HasFocus); + + view.Enabled = false; + view.NewKeyDownEvent (Key.Space); + Assert.False (wasClicked); + view.NewMouseEvent (new () { Flags = MouseFlags.Button1Clicked }); + Assert.False (wasClicked); + Assert.False (view.Enabled); + Assert.True (view.CanFocus); + Assert.False (view.HasFocus); + view.SetFocus (); + Assert.False (view.HasFocus); + } + + + + [Fact] + public void HasFocus_False_CompoundSubView_Leaves_All () + { + var view = new View () + { + Id = "view", + CanFocus = true + }; + + var subView = new View () + { + Id = "subView", + CanFocus = true + }; + + var subViewSubView1 = new View () + { + Id = "subViewSubView1", + CanFocus = false + }; + + var subViewSubView2 = new View () + { + Id = "subViewSubView2", + CanFocus = true + }; + + var subViewSubView3 = new View () + { + Id = "subViewSubView3", + CanFocus = false + }; + + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.True (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + + view.HasFocus = false; + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.False (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + } + + [Fact] + public void HasFocus_False_Subview_Raises_HasFocusChanged () + { + var top = new View + { + Id = "top", + CanFocus = true + }; + + var subView1 = new View + { + Id = "subView1", + CanFocus = true + }; + + var subView2 = new View + { + Id = "subView2", + CanFocus = true + }; + top.Add (subView1, subView2); + + var subView1HasFocusChangedTrueCount = 0; + var subView1HasFocusChangedFalseCount = 0; + + subView1.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subView1HasFocusChangedTrueCount++; + } + else + { + subView1HasFocusChangedFalseCount++; + } + }; + + var subView2HasFocusChangedTrueCount = 0; + var subView2HasFocusChangedFalseCount = 0; + + subView2.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + subView2HasFocusChangedTrueCount++; + } + else + { + subView2HasFocusChangedFalseCount++; + } + }; + + top.SetFocus (); + Assert.True (top.HasFocus); + Assert.True (subView1.HasFocus); + Assert.False (subView2.HasFocus); + + Assert.Equal (1, subView1HasFocusChangedTrueCount); + Assert.Equal (0, subView1HasFocusChangedFalseCount); + + Assert.Equal (0, subView2HasFocusChangedTrueCount); + Assert.Equal (0, subView2HasFocusChangedFalseCount); + + subView1.HasFocus = false; // this should have the same resuilt as top.AdvanceFocus (NavigationDirection.Forward, null); + + Assert.False (subView1.HasFocus); + Assert.True (subView2.HasFocus); + + Assert.Equal (1, subView1HasFocusChangedTrueCount); + Assert.Equal (1, subView1HasFocusChangedFalseCount); + + Assert.Equal (1, subView2HasFocusChangedTrueCount); + Assert.Equal (0, subView2HasFocusChangedFalseCount); + } +} diff --git a/UnitTests/View/Navigation/NavigationTests.cs b/UnitTests/View/Navigation/NavigationTests.cs new file mode 100644 index 0000000000..306e7739e4 --- /dev/null +++ b/UnitTests/View/Navigation/NavigationTests.cs @@ -0,0 +1,590 @@ +using JetBrains.Annotations; +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class NavigationTests (ITestOutputHelper _output) : TestsAllViews +{ + [Theory] + [MemberData (nameof (AllViewTypes))] + [SetupFakeDriver] // SetupFakeDriver resets app state; helps to avoid test pollution + public void AllViews_AtLeastOneNavKey_Advances (Type viewType) + { + View view = CreateInstanceIfNotGeneric (viewType); + + if (view == null) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); + + return; + } + + if (!view.CanFocus) + { + _output.WriteLine ($"Ignoring {viewType} - It can't focus."); + + return; + } + + + Toplevel top = new (); + Application.Current = top; + Application.Navigation = new ApplicationNavigation (); + + View otherView = new () + { + Id = "otherView", + CanFocus = true, + TabStop = view.TabStop == TabBehavior.NoStop ? TabBehavior.TabStop : view.TabStop + }; + + top.Add (view, otherView); + + // Start with the focus on our test view + view.SetFocus (); + + Key [] navKeys = [Key.Tab, Key.Tab.WithShift, Key.CursorUp, Key.CursorDown, Key.CursorLeft, Key.CursorRight]; + + if (view.TabStop == TabBehavior.TabGroup) + { + navKeys = new [] { Key.F6, Key.F6.WithShift }; + } + + var left = false; + + foreach (Key key in navKeys) + { + switch (view.TabStop) + { + case TabBehavior.TabStop: + case TabBehavior.NoStop: + case TabBehavior.TabGroup: + Application.OnKeyDown (key); + + break; + default: + Application.OnKeyDown (Key.Tab); + + break; + } + + if (!view.HasFocus) + { + left = true; + _output.WriteLine ($"{view.GetType ().Name} - {key} Left."); + view.SetFocus (); + } + else + { + _output.WriteLine ($"{view.GetType ().Name} - {key} did not Leave."); + } + } + + top.Dispose (); + Application.ResetState (); + + Assert.True (left); + } + + [Theory] + [MemberData (nameof (AllViewTypes))] + [SetupFakeDriver] // SetupFakeDriver resets app state; helps to avoid test pollution + public void AllViews_HasFocus_Changed_Event (Type viewType) + { + View view = CreateInstanceIfNotGeneric (viewType); + + if (view == null) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); + + return; + } + + if (!view.CanFocus) + { + _output.WriteLine ($"Ignoring {viewType} - It can't focus."); + + return; + } + + if (view is Toplevel && ((Toplevel)view).Modal) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); + + return; + } + + Toplevel top = new (); + Application.Current = top; + Application.Navigation = new ApplicationNavigation (); + + View otherView = new () + { + Id = "otherView", + CanFocus = true, + TabStop = view.TabStop == TabBehavior.NoStop ? TabBehavior.TabStop : view.TabStop + }; + + var hasFocusTrue = 0; + var hasFocusFalse = 0; + + view.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + hasFocusTrue++; + } + else + { + hasFocusFalse++; + } + }; + + top.Add (view, otherView); + Assert.False (view.HasFocus); + Assert.False (otherView.HasFocus); + + Application.Current.SetFocus (); + Assert.True (Application.Current!.HasFocus); + Assert.True (top.HasFocus); + + // Start with the focus on our test view + Assert.True (view.HasFocus); + + Assert.Equal (1, hasFocusTrue); + Assert.Equal (0, hasFocusFalse); + + // Use keyboard to navigate to next view (otherView). + var tries = 0; + + while (view.HasFocus) + { + if (++tries > 10) + { + Assert.Fail ($"{view} is not leaving."); + } + + switch (view.TabStop) + { + case null: + case TabBehavior.NoStop: + case TabBehavior.TabStop: + if (Application.OnKeyDown (Key.Tab)) + { + if (view.HasFocus) + { + // Try another nav key (e.g. for TextView that eats Tab) + Application.OnKeyDown (Key.CursorDown); + } + }; + break; + + case TabBehavior.TabGroup: + Application.OnKeyDown (Key.F6); + + break; + default: + throw new ArgumentOutOfRangeException (); + } + } + + Assert.Equal (1, hasFocusTrue); + Assert.Equal (1, hasFocusFalse); + + Assert.False (view.HasFocus); + Assert.True (otherView.HasFocus); + + // Now navigate back to our test view + switch (view.TabStop) + { + case TabBehavior.NoStop: + view.SetFocus (); + + break; + case TabBehavior.TabStop: + Application.OnKeyDown (Key.Tab); + + break; + case TabBehavior.TabGroup: + if (!Application.OnKeyDown (Key.F6)) + { + view.SetFocus (); + } + + break; + case null: + Application.OnKeyDown (Key.Tab); + + break; + default: + throw new ArgumentOutOfRangeException (); + } + + Assert.Equal (2, hasFocusTrue); + Assert.Equal (1, hasFocusFalse); + + Assert.True (view.HasFocus); + Assert.False (otherView.HasFocus); + + // Cache state because Shutdown has side effects. + // Also ensures other tests can continue running if there's a fail + bool otherViewHasFocus = otherView.HasFocus; + bool viewHasFocus = view.HasFocus; + + int enterCount = hasFocusTrue; + int leaveCount = hasFocusFalse; + + top.Dispose (); + + Assert.False (otherViewHasFocus); + Assert.True (viewHasFocus); + + Assert.Equal (2, enterCount); + Assert.Equal (1, leaveCount); + + Application.ResetState (); + } + + [Theory] + [MemberData (nameof (AllViewTypes))] + [SetupFakeDriver] // SetupFakeDriver resets app state; helps to avoid test pollution + public void AllViews_Visible_False_No_HasFocus_Events (Type viewType) + { + View view = CreateInstanceIfNotGeneric (viewType); + + if (view == null) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); + + return; + } + + if (!view.CanFocus) + { + _output.WriteLine ($"Ignoring {viewType} - It can't focus."); + + return; + } + + if (view is Toplevel && ((Toplevel)view).Modal) + { + _output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); + + return; + } + + Toplevel top = new (); + + Application.Current = top; + Application.Navigation = new ApplicationNavigation (); + + View otherView = new () + { + CanFocus = true + }; + + view.Visible = false; + + var hasFocusChangingCount = 0; + var hasFocusChangedCount = 0; + + view.HasFocusChanging += (s, e) => hasFocusChangingCount++; + view.HasFocusChanged += (s, e) => hasFocusChangedCount++; + + top.Add (view, otherView); + + // Start with the focus on our test view + view.SetFocus (); + + Assert.Equal (0, hasFocusChangingCount); + Assert.Equal (0, hasFocusChangedCount); + + Application.OnKeyDown (Key.Tab); + + Assert.Equal (0, hasFocusChangingCount); + Assert.Equal (0, hasFocusChangedCount); + + Application.OnKeyDown (Key.F6); + + Assert.Equal (0, hasFocusChangingCount); + Assert.Equal (0, hasFocusChangedCount); + + top.Dispose (); + + Application.ResetState (); + + } + + // View.Focused & View.MostFocused tests + + // View.Focused - No subviews + [Fact] + public void Focused_NoSubviews () + { + var view = new View (); + Assert.Null (view.Focused); + + view.CanFocus = true; + view.SetFocus (); + } + + [Fact] + public void GetMostFocused_NoSubviews_Returns_Null () + { + var view = new View (); + Assert.Null (view.Focused); + + view.CanFocus = true; + Assert.False (view.HasFocus); + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.Null (view.MostFocused); + } + + [Fact] + public void GetMostFocused_Returns_Most () + { + var view = new View () + { + Id = "view", + CanFocus = true + }; + + var subview = new View () + { + Id = "subview", + CanFocus = true + }; + + view.Add (subview); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subview.HasFocus); + Assert.Equal (subview, view.MostFocused); + + var subview2 = new View () + { + Id = "subview2", + CanFocus = true + }; + + view.Add (subview2); + Assert.Equal (subview2, view.MostFocused); + } + + // [Fact] + // [AutoInitShutdown] + // public void HotKey_Will_Invoke_KeyPressed_Only_For_The_MostFocused_With_Top_KeyPress_Event () + // { + // var sbQuiting = false; + // var tfQuiting = false; + // var topQuiting = false; + + // var sb = new StatusBar ( + // new Shortcut [] + // { + // new ( + // KeyCode.CtrlMask | KeyCode.Q, + // "Quit", + // () => sbQuiting = true + // ) + // } + // ); + // var tf = new TextField (); + // tf.KeyDown += Tf_KeyPressed; + + // void Tf_KeyPressed (object sender, Key obj) + // { + // if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) + // { + // obj.Handled = tfQuiting = true; + // } + // } + + // var win = new Window (); + // win.Add (sb, tf); + // Toplevel top = new (); + // top.KeyDown += Top_KeyPress; + + // void Top_KeyPress (object sender, Key obj) + // { + // if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) + // { + // obj.Handled = topQuiting = true; + // } + // } + + // top.Add (win); + // Application.Begin (top); + + // Assert.False (sbQuiting); + // Assert.False (tfQuiting); + // Assert.False (topQuiting); + + // Application.Driver?.SendKeys ('Q', ConsoleKey.Q, false, false, true); + // Assert.False (sbQuiting); + // Assert.True (tfQuiting); + // Assert.False (topQuiting); + + //#if BROKE_WITH_2927 + // tf.KeyPressed -= Tf_KeyPress; + // tfQuiting = false; + // Application.Driver?.SendKeys ('q', ConsoleKey.Q, false, false, true); + // Application.MainLoop.RunIteration (); + // Assert.True (sbQuiting); + // Assert.False (tfQuiting); + // Assert.False (topQuiting); + + // sb.RemoveItem (0); + // sbQuiting = false; + // Application.Driver?.SendKeys ('q', ConsoleKey.Q, false, false, true); + // Application.MainLoop.RunIteration (); + // Assert.False (sbQuiting); + // Assert.False (tfQuiting); + + //// This test is now invalid because `win` is focused, so it will receive the keypress + // Assert.True (topQuiting); + //#endif + // top.Dispose (); + // } + + // [Fact] + // [AutoInitShutdown] + // public void HotKey_Will_Invoke_KeyPressed_Only_For_The_MostFocused_Without_Top_KeyPress_Event () + // { + // var sbQuiting = false; + // var tfQuiting = false; + + // var sb = new StatusBar ( + // new Shortcut [] + // { + // new ( + // KeyCode.CtrlMask | KeyCode.Q, + // "~^Q~ Quit", + // () => sbQuiting = true + // ) + // } + // ); + // var tf = new TextField (); + // tf.KeyDown += Tf_KeyPressed; + + // void Tf_KeyPressed (object sender, Key obj) + // { + // if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) + // { + // obj.Handled = tfQuiting = true; + // } + // } + + // var win = new Window (); + // win.Add (sb, tf); + // Toplevel top = new (); + // top.Add (win); + // Application.Begin (top); + + // Assert.False (sbQuiting); + // Assert.False (tfQuiting); + + // Application.Driver?.SendKeys ('Q', ConsoleKey.Q, false, false, true); + // Assert.False (sbQuiting); + // Assert.True (tfQuiting); + + // tf.KeyDown -= Tf_KeyPressed; + // tfQuiting = false; + // Application.Driver?.SendKeys ('Q', ConsoleKey.Q, false, false, true); + // Application.MainLoop.RunIteration (); + //#if BROKE_WITH_2927 + // Assert.True (sbQuiting); + // Assert.False (tfQuiting); + //#endif + // top.Dispose (); + // } + + [Fact] + [SetupFakeDriver] + public void Navigation_With_Null_Focused_View () + { + // Non-regression test for #882 (NullReferenceException during keyboard navigation when Focused is null) + + Application.Init (new FakeDriver ()); + + var top = new Toplevel (); + top.Ready += (s, e) => { Assert.Null (top.Focused); }; + + // Keyboard navigation with tab + FakeConsole.MockKeyPresses.Push (new ('\t', ConsoleKey.Tab, false, false, false)); + + Application.Iteration += (s, a) => Application.RequestStop (); + + Application.Run (top); + top.Dispose (); + Application.Shutdown (); + } + + + [Fact] + [AutoInitShutdown] + public void Application_Begin_FocusesDeepest () + { + var win1 = new Window { Id = "win1", Width = 10, Height = 1 }; + var view1 = new View { Id = "view1", Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true }; + var win2 = new Window { Id = "win2", Y = 6, Width = 10, Height = 1 }; + var view2 = new View { Id = "view2", Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true }; + win2.Add (view2); + win1.Add (view1, win2); + + Application.Begin (win1); + + Assert.True (win1.HasFocus); + Assert.True (view1.HasFocus); + Assert.False (win2.HasFocus); + Assert.False (view2.HasFocus); + win1.Dispose (); + } + + +#if V2_NEW_FOCUS_IMPL // bogus test - Depends on auto setting of CanFocus + [Fact] + [AutoInitShutdown] + public void Remove_Does_Not_Change_Focus () + { + var top = new Toplevel (); + Assert.True (top.CanFocus); + Assert.False (top.HasFocus); + + var container = new View { Width = 10, Height = 10 }; + var leave = false; + container.Leave += (s, e) => leave = true; + Assert.False (container.CanFocus); + var child = new View { Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true }; + container.Add (child); + + Assert.True (container.CanFocus); + Assert.False (container.HasFocus); + Assert.True (child.CanFocus); + Assert.False (child.HasFocus); + + top.Add (container); + Application.Begin (top); + + Assert.True (top.CanFocus); + Assert.True (top.HasFocus); + Assert.True (container.CanFocus); + Assert.True (container.HasFocus); + Assert.True (child.CanFocus); + Assert.True (child.HasFocus); + + container.Remove (child); + child.Dispose (); + child = null; + Assert.True (top.HasFocus); + Assert.True (container.CanFocus); + Assert.True (container.HasFocus); + Assert.Null (child); + Assert.False (leave); + top.Dispose (); + } +#endif + +} diff --git a/UnitTests/View/Navigation/RestoreFocusTests.cs b/UnitTests/View/Navigation/RestoreFocusTests.cs new file mode 100644 index 0000000000..e03c25fc80 --- /dev/null +++ b/UnitTests/View/Navigation/RestoreFocusTests.cs @@ -0,0 +1,173 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class RestoreFocusTests () : TestsAllViews +{ + [Fact] + public void RestoreFocus_Restores () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + var subViewSubView1 = new View + { + Id = "subViewSubView1", + CanFocus = true + }; + + var subViewSubView2 = new View + { + Id = "subViewSubView2", + CanFocus = true + }; + + var subViewSubView3 = new View + { + Id = "subViewSubView3", + CanFocus = true + }; + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.True (subViewSubView1.HasFocus); + Assert.Equal (subViewSubView1, subView.Focused); + + view.HasFocus = false; + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.False (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + + view.RestoreFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.Equal (subViewSubView1, subView.Focused); + Assert.True (subViewSubView1.HasFocus); + Assert.False (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + + subViewSubView2.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.True (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + + view.HasFocus = false; + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.False (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + + view.RestoreFocus (); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.True (subViewSubView2.HasFocus); + Assert.Equal (subViewSubView2, subView.Focused); + Assert.False (subViewSubView1.HasFocus); + Assert.False (subViewSubView3.HasFocus); + } + + [Fact] + public void RestoreFocus_Across_TabGroup () + { + var top = new View + { + Id = "top", + CanFocus = true, + TabStop = TabBehavior.TabGroup + }; + + var tabGroup1 = new View + { + Id = "tabGroup1", + CanFocus = true, + TabStop = TabBehavior.TabGroup + }; + + var tabGroup1SubView1 = new View + { + Id = "tabGroup1SubView1", + CanFocus = true, + TabStop = TabBehavior.TabStop + }; + + var tabGroup1SubView2 = new View + { + Id = "tabGroup1SubView2", + CanFocus = true, + TabStop = TabBehavior.TabStop + }; + tabGroup1.Add (tabGroup1SubView1, tabGroup1SubView2); + + var tabGroup2 = new View + { + Id = "tabGroup2", + CanFocus = true, + TabStop = TabBehavior.TabGroup + }; + + var tabGroup2SubView1 = new View + { + Id = "tabGroup2SubView1", + CanFocus = true, + TabStop = TabBehavior.TabStop + }; + + var tabGroup2SubView2 = new View + { + Id = "tabGroup2SubView2", + CanFocus = true, + TabStop = TabBehavior.TabStop + }; + tabGroup2.Add (tabGroup2SubView1, tabGroup2SubView2); + + top.Add (tabGroup1, tabGroup2); + + top.SetFocus (); + Assert.True (top.HasFocus); + Assert.Equal (tabGroup1, top.Focused); + Assert.Equal (tabGroup1SubView1, tabGroup1.Focused); + + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup); + Assert.True (top.HasFocus); + Assert.Equal (tabGroup2, top.Focused); + Assert.Equal (tabGroup2SubView1, tabGroup2.Focused); + + top.HasFocus = false; + Assert.False (top.HasFocus); + + top.RestoreFocus (); + Assert.True (top.HasFocus); + Assert.Equal (tabGroup2, top.Focused); + Assert.Equal (tabGroup2SubView1, tabGroup2.Focused); + + top.HasFocus = false; + Assert.False (top.HasFocus); + + top.RestoreFocus (); + Assert.True (top.HasFocus); + Assert.Equal (tabGroup2, top.Focused); + Assert.Equal (tabGroup2SubView1, tabGroup2.Focused); + + + } +} diff --git a/UnitTests/View/Navigation/SetFocusTests.cs b/UnitTests/View/Navigation/SetFocusTests.cs new file mode 100644 index 0000000000..3896e51cb5 --- /dev/null +++ b/UnitTests/View/Navigation/SetFocusTests.cs @@ -0,0 +1,278 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class SetFocusTests () : TestsAllViews +{ + [Fact] + public void SetFocus_With_Null_Superview_Does_Not_Throw_Exception () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + Assert.True (view.CanFocus); + Assert.False (view.HasFocus); + + Exception exception = Record.Exception (() => view.SetFocus ()); + Assert.Null (exception); + + Assert.True (view.CanFocus); + Assert.True (view.HasFocus); + } + + [Fact] + public void SetFocus_SetsFocus () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + Assert.True (view.CanFocus); + Assert.False (view.HasFocus); + + view.SetFocus (); + Assert.True (view.HasFocus); + } + + [Fact] + public void SetFocus_NoSubView_Focused_Is_Null () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + Assert.True (view.CanFocus); + Assert.False (view.HasFocus); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.Null (view.Focused); + } + + [Fact] + public void SetFocus_SubView_Focused_Is_Set () + { + var view = new Window + { + Id = "view", + CanFocus = true + }; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + view.Add (subview); + Assert.True (view.CanFocus); + Assert.False (view.HasFocus); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.Equal (subview, view.Focused); + } + + [Fact] + public void SetFocus_SetsFocus_DeepestSubView () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + view.Add (subview); + + view.SetFocus (); + Assert.True (subview.HasFocus); + Assert.Equal (subview, view.Focused); + } + + [Fact] + public void SetFocus_SetsFocus_DeepestSubView_CompoundSubView () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + var subViewSubView1 = new View + { + Id = "subViewSubView1", + CanFocus = false + }; + + var subViewSubView2 = new View + { + Id = "subViewSubView2", + CanFocus = true + }; + + var subViewSubView3 = new View + { + Id = "subViewSubView3", + CanFocus = false + }; + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.Equal (subViewSubView2, subView.Focused); + } + + [Fact] + public void SetFocus_CompoundSubView_SetFocus_Sets () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + var subViewSubView1 = new View + { + Id = "subViewSubView1", + CanFocus = true + }; + + var subViewSubView2 = new View + { + Id = "subViewSubView2", + CanFocus = true + }; + + var subViewSubView3 = new View + { + Id = "subViewSubView3", + CanFocus = true + }; + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.True (subViewSubView1.HasFocus); + Assert.Equal (subViewSubView1, subView.Focused); + + subViewSubView2.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.True (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + } + + [Fact] + public void SetFocus_Peer_LeavesOther () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subview1 = new View + { + Id = "subview1", + CanFocus = true + }; + + var subview2 = new View + { + Id = "subview2", + CanFocus = true + }; + view.Add (subview1, subview2); + + view.SetFocus (); + Assert.Equal (subview1, view.Focused); + Assert.True (subview1.HasFocus); + Assert.False (subview2.HasFocus); + + subview2.SetFocus (); + Assert.Equal (subview2, view.Focused); + Assert.True (subview2.HasFocus); + Assert.False (subview1.HasFocus); + } + + [Fact] + public void SetFocus_On_Peer_Moves_Focus_To_Peer () + { + var top = new View + { + Id = "top", + CanFocus = true + }; + + var view1 = new View + { + Id = "view1", + CanFocus = true + }; + + var subView1 = new View + { + Id = "subView1", + CanFocus = true + }; + + view1.Add (subView1); + + var subView1SubView1 = new View + { + Id = "subView1subView1", + CanFocus = true + }; + + subView1.Add (subView1SubView1); + + var view2 = new View + { + Id = "view2", + CanFocus = true + }; + + top.Add (view1, view2); + Assert.False (view1.HasFocus); + Assert.False (view2.HasFocus); + + view1.SetFocus (); + Assert.True (view1.HasFocus); + Assert.True (subView1.HasFocus); + Assert.True (subView1SubView1.HasFocus); + Assert.Equal (subView1, view1.Focused); + Assert.Equal (subView1SubView1, subView1.Focused); + + view2.SetFocus (); + Assert.False (view1.HasFocus); + Assert.True (view2.HasFocus); + } +} diff --git a/UnitTests/View/Navigation/VisibleTests.cs b/UnitTests/View/Navigation/VisibleTests.cs new file mode 100644 index 0000000000..e4a1e102b4 --- /dev/null +++ b/UnitTests/View/Navigation/VisibleTests.cs @@ -0,0 +1,253 @@ +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewTests; + +public class VisibleTests () : TestsAllViews +{ + [Fact] + public void Visible_False_Leaves () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + view.SetFocus (); + Assert.True (view.HasFocus); + + view.Visible = false; + Assert.False (view.HasFocus); + } + + [Fact] + public void Visible_False_Leaves_Subview () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + + view.Visible = false; + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + } + + [Fact] + public void Visible_False_Leaves_Subview2 () + { + var view = new Window + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + + view.Visible = false; + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + } + + [Fact] + public void Visible_False_On_Subview_Leaves_Just_Subview () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + + subView.Visible = false; + Assert.True (view.HasFocus); + Assert.False (subView.HasFocus); + } + + [Fact] + public void Visible_False_Focuses_Deepest_Focusable_Subview () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + var subViewSubView1 = new View + { + Id = "subViewSubView1", + CanFocus = false + }; + + var subViewSubView2 = new View + { + Id = "subViewSubView2", + CanFocus = true + }; + + var subViewSubView3 = new View + { + Id = "subViewSubView3", + CanFocus = true // This is the one that will be focused + }; + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.Equal (subViewSubView2, subView.Focused); + + subViewSubView2.Visible = false; + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.Equal (subViewSubView3, subView.Focused); + Assert.True (subViewSubView3.HasFocus); + } + + [Fact] + public void Visible_True_Subview_Focuses_SubView () + { + var view = new View + { + Id = "view", + CanFocus = true, + Visible = false + }; + + var subView = new View + { + Id = "subView", + CanFocus = true + }; + + view.Add (subView); + + view.SetFocus (); + Assert.False (view.HasFocus); + Assert.False (subView.HasFocus); + + view.Visible = true; + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + } + + [Fact] + public void Visible_True_On_Subview_Focuses () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true, + Visible = false + }; + + view.Add (subView); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.False (subView.HasFocus); + + subView.Visible = true; + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + } + + [Fact] + public void Visible_True_Focuses_Deepest_Focusable_Subview () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subView = new View + { + Id = "subView", + CanFocus = true, + Visible = false + }; + + var subViewSubView1 = new View + { + Id = "subViewSubView1", + CanFocus = false + }; + + var subViewSubView2 = new View + { + Id = "subViewSubView2", + CanFocus = true // This is the one that will be focused + }; + + var subViewSubView3 = new View + { + Id = "subViewSubView3", + CanFocus = true + }; + subView.Add (subViewSubView1, subViewSubView2, subViewSubView3); + + view.Add (subView); + + view.SetFocus (); + Assert.False (subView.HasFocus); + Assert.False (subViewSubView2.HasFocus); + + subView.Visible = true; + Assert.True (subView.HasFocus); + Assert.Equal (subView, view.Focused); + Assert.Equal (subViewSubView2, subView.Focused); + Assert.True (subViewSubView2.HasFocus); + } +} diff --git a/UnitTests/View/NavigationTests.cs b/UnitTests/View/NavigationTests.cs deleted file mode 100644 index 488bed57fa..0000000000 --- a/UnitTests/View/NavigationTests.cs +++ /dev/null @@ -1,1957 +0,0 @@ -using Xunit.Abstractions; - -namespace Terminal.Gui.ViewTests; - -public class NavigationTests (ITestOutputHelper _output) : TestsAllViews -{ - [Theory] - [MemberData (nameof (AllViewTypes))] - public void AllViews_AtLeastOneNavKey_Leaves (Type viewType) - { - View view = CreateInstanceIfNotGeneric (viewType); - - if (view == null) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); - - return; - } - - if (!view.CanFocus) - { - _output.WriteLine ($"Ignoring {viewType} - It can't focus."); - - return; - } - - Application.Init (new FakeDriver ()); - - Toplevel top = new (); - - View otherView = new () - { - Id = "otherView", - CanFocus = true, - TabStop = view.TabStop - }; - - top.Add (view, otherView); - Application.Begin (top); - - // Start with the focus on our test view - view.SetFocus (); - - Key [] navKeys = { Key.Tab, Key.Tab.WithShift, Key.CursorUp, Key.CursorDown, Key.CursorLeft, Key.CursorRight }; - - if (view.TabStop == TabBehavior.TabGroup) - { - navKeys = new [] { Key.F6, Key.F6.WithShift }; - } - - var left = false; - - foreach (Key key in navKeys) - { - switch (view.TabStop) - { - case TabBehavior.TabStop: - case TabBehavior.NoStop: - case TabBehavior.TabGroup: - Application.OnKeyDown (key); - - break; - default: - Application.OnKeyDown (Key.Tab); - - break; - } - - if (!view.HasFocus) - { - left = true; - _output.WriteLine ($"{view.GetType ().Name} - {key} Left."); - view.SetFocus (); - } - else - { - _output.WriteLine ($"{view.GetType ().Name} - {key} did not Leave."); - } - } - - top.Dispose (); - Application.Shutdown (); - - Assert.True (left); - } - - [Theory] - [MemberData (nameof (AllViewTypes))] - public void AllViews_Enter_Leave_Events (Type viewType) - { - View view = CreateInstanceIfNotGeneric (viewType); - - if (view == null) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); - - return; - } - - if (!view.CanFocus) - { - _output.WriteLine ($"Ignoring {viewType} - It can't focus."); - - return; - } - - if (view is Toplevel && ((Toplevel)view).Modal) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); - - return; - } - - Application.Init (new FakeDriver ()); - - Toplevel top = new () - { - Height = 10, - Width = 10 - }; - - View otherView = new () - { - Id = "otherView", - X = 0, Y = 0, - Height = 1, - Width = 1, - CanFocus = true, - TabStop = view.TabStop - }; - - view.X = Pos.Right (otherView); - view.Y = 0; - view.Width = 10; - view.Height = 1; - - var nEnter = 0; - var nLeave = 0; - - view.Enter += (s, e) => nEnter++; - view.Leave += (s, e) => nLeave++; - - top.Add (view, otherView); - Application.Begin (top); - - // Start with the focus on our test view - view.SetFocus (); - - //Assert.Equal (1, nEnter); - //Assert.Equal (0, nLeave); - - // Use keyboard to navigate to next view (otherView). - if (view is TextView) - { - Application.OnKeyDown (Key.F6); - } - else - { - var tries = 0; - - while (view.HasFocus) - { - if (++tries > 10) - { - Assert.Fail ($"{view} is not leaving."); - } - - switch (view.TabStop) - { - case TabBehavior.NoStop: - Application.OnKeyDown (Key.Tab); - - break; - case TabBehavior.TabStop: - Application.OnKeyDown (Key.Tab); - - break; - case TabBehavior.TabGroup: - Application.OnKeyDown (Key.F6); - - break; - case null: - Application.OnKeyDown (Key.Tab); - - break; - default: - throw new ArgumentOutOfRangeException (); - } - } - } - - //Assert.Equal (1, nEnter); - //Assert.Equal (1, nLeave); - - //Assert.False (view.HasFocus); - //Assert.True (otherView.HasFocus); - - // Now navigate back to our test view - switch (view.TabStop) - { - case TabBehavior.NoStop: - view.SetFocus (); - - break; - case TabBehavior.TabStop: - Application.OnKeyDown (Key.Tab); - - break; - case TabBehavior.TabGroup: - Application.OnKeyDown (Key.F6); - - break; - case null: - Application.OnKeyDown (Key.Tab); - - break; - default: - throw new ArgumentOutOfRangeException (); - } - - // Cache state because Shutdown has side effects. - // Also ensures other tests can continue running if there's a fail - bool otherViewHasFocus = otherView.HasFocus; - bool viewHasFocus = view.HasFocus; - - int enterCount = nEnter; - int leaveCount = nLeave; - - top.Dispose (); - Application.Shutdown (); - - Assert.False (otherViewHasFocus); - Assert.True (viewHasFocus); - - Assert.Equal (2, enterCount); - Assert.Equal (1, leaveCount); - } - - [Theory] - [MemberData (nameof (AllViewTypes))] - public void AllViews_Enter_Leave_Events_Visible_False (Type viewType) - { - View view = CreateInstanceIfNotGeneric (viewType); - - if (view == null) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Generic"); - - return; - } - - if (!view.CanFocus) - { - _output.WriteLine ($"Ignoring {viewType} - It can't focus."); - - return; - } - - if (view is Toplevel && ((Toplevel)view).Modal) - { - _output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); - - return; - } - - Application.Init (new FakeDriver ()); - - Toplevel top = new () - { - Height = 10, - Width = 10 - }; - - View otherView = new () - { - X = 0, Y = 0, - Height = 1, - Width = 1, - CanFocus = true - }; - - view.Visible = false; - view.X = Pos.Right (otherView); - view.Y = 0; - view.Width = 10; - view.Height = 1; - - var nEnter = 0; - var nLeave = 0; - - view.Enter += (s, e) => nEnter++; - view.Leave += (s, e) => nLeave++; - - top.Add (view, otherView); - Application.Begin (top); - - // Start with the focus on our test view - view.SetFocus (); - - Assert.Equal (0, nEnter); - Assert.Equal (0, nLeave); - - // Use keyboard to navigate to next view (otherView). - if (view is TextView) - { - Application.OnKeyDown (Key.F6); - } - else if (view is DatePicker) - { - for (var i = 0; i < 4; i++) - { - Application.OnKeyDown (Key.F6); - } - } - else - { - Application.OnKeyDown (Key.Tab); - } - - Assert.Equal (0, nEnter); - Assert.Equal (0, nLeave); - - top.NewKeyDownEvent (Key.Tab); - - Assert.Equal (0, nEnter); - Assert.Equal (0, nLeave); - - top.Dispose (); - Application.Shutdown (); - } - - [Fact] - public void BringSubviewForward_Subviews_vs_TabIndexes () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - r.BringSubviewForward (v1); - Assert.True (r.Subviews.IndexOf (v1) == 1); - Assert.True (r.Subviews.IndexOf (v2) == 0); - Assert.True (r.Subviews.IndexOf (v3) == 2); - - Assert.True (r.TabIndexes.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v2) == 1); - Assert.True (r.TabIndexes.IndexOf (v3) == 2); - r.Dispose (); - } - - [Fact] - public void BringSubviewToFront_Subviews_vs_TabIndexes () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - r.BringSubviewToFront (v1); - Assert.True (r.Subviews.IndexOf (v1) == 2); - Assert.True (r.Subviews.IndexOf (v2) == 0); - Assert.True (r.Subviews.IndexOf (v3) == 1); - - Assert.True (r.TabIndexes.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v2) == 1); - Assert.True (r.TabIndexes.IndexOf (v3) == 2); - r.Dispose (); - } - - [Fact] - public void CanFocus_Container_ToFalse_Turns_All_Subviews_ToFalse_Too () - { - Application.Init (new FakeDriver ()); - - Toplevel t = new (); - - var w = new Window (); - var f = new FrameView (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - f.Add (v1, v2); - w.Add (f); - t.Add (w); - - t.Ready += (s, e) => - { - Assert.True (t.CanFocus); - Assert.True (w.CanFocus); - Assert.True (f.CanFocus); - Assert.True (v1.CanFocus); - Assert.True (v2.CanFocus); - - w.CanFocus = false; - Assert.False (w.CanFocus); - Assert.False (f.CanFocus); - Assert.False (v1.CanFocus); - Assert.False (v2.CanFocus); - }; - - Application.Iteration += (s, a) => Application.RequestStop (); - - Application.Run (t); - t.Dispose (); - Application.Shutdown (); - } - - [Fact] - public void CanFocus_Container_Toggling_All_Subviews_To_Old_Value_When_Is_True () - { - Application.Init (new FakeDriver ()); - - Toplevel t = new (); - - var w = new Window (); - var f = new FrameView (); - var v1 = new View (); - var v2 = new View { CanFocus = true }; - f.Add (v1, v2); - w.Add (f); - t.Add (w); - - t.Ready += (s, e) => - { - Assert.True (t.CanFocus); - Assert.True (w.CanFocus); - Assert.True (f.CanFocus); - Assert.False (v1.CanFocus); - Assert.True (v2.CanFocus); - - w.CanFocus = false; - Assert.False (w.CanFocus); - Assert.False (f.CanFocus); - Assert.False (v1.CanFocus); - Assert.False (v2.CanFocus); - - w.CanFocus = true; - Assert.True (w.CanFocus); - Assert.True (f.CanFocus); - Assert.False (v1.CanFocus); - Assert.True (v2.CanFocus); - }; - - Application.Iteration += (s, a) => Application.RequestStop (); - - Application.Run (t); - t.Dispose (); - Application.Shutdown (); - } - - [Fact] - [AutoInitShutdown] - public void CanFocus_Faced_With_Container () - { - var t = new Toplevel (); - var w = new Window (); - var f = new FrameView (); - var v = new View { CanFocus = true }; - f.Add (v); - w.Add (f); - t.Add (w); - - Assert.True (t.CanFocus); - Assert.True (w.CanFocus); - Assert.True (f.CanFocus); - Assert.True (v.CanFocus); - - f.CanFocus = false; - Assert.False (f.CanFocus); - Assert.True (v.CanFocus); - - v.CanFocus = false; - Assert.False (f.CanFocus); - Assert.False (v.CanFocus); - - v.CanFocus = true; - Assert.False (f.CanFocus); - Assert.True (v.CanFocus); - t.Dispose (); - } - - [Fact] - public void CanFocus_Faced_With_Container_After_Run () - { - Application.Init (new FakeDriver ()); - - Toplevel t = new (); - - var w = new Window (); - var f = new FrameView (); - var v = new View { CanFocus = true }; - f.Add (v); - w.Add (f); - t.Add (w); - - t.Ready += (s, e) => - { - Assert.True (t.CanFocus); - Assert.True (w.CanFocus); - Assert.True (f.CanFocus); - Assert.True (v.CanFocus); - - f.CanFocus = false; - Assert.False (f.CanFocus); - Assert.False (v.CanFocus); - - v.CanFocus = false; - Assert.False (f.CanFocus); - Assert.False (v.CanFocus); - - Assert.Throws (() => v.CanFocus = true); - Assert.False (f.CanFocus); - Assert.False (v.CanFocus); - - f.CanFocus = true; - Assert.True (f.CanFocus); - Assert.True (v.CanFocus); - }; - - Application.Iteration += (s, a) => Application.RequestStop (); - - Application.Run (t); - t.Dispose (); - Application.Shutdown (); - } - - [Fact] - public void CanFocus_Faced_With_Container_Before_Run () - { - Application.Init (new FakeDriver ()); - - Toplevel t = new (); - - var w = new Window (); - var f = new FrameView (); - var v = new View { CanFocus = true }; - f.Add (v); - w.Add (f); - t.Add (w); - - Assert.True (t.CanFocus); - Assert.True (w.CanFocus); - Assert.True (f.CanFocus); - Assert.True (v.CanFocus); - - f.CanFocus = false; - Assert.False (f.CanFocus); - Assert.True (v.CanFocus); - - v.CanFocus = false; - Assert.False (f.CanFocus); - Assert.False (v.CanFocus); - - v.CanFocus = true; - Assert.False (f.CanFocus); - Assert.True (v.CanFocus); - - Application.Iteration += (s, a) => Application.RequestStop (); - - Application.Run (t); - t.Dispose (); - Application.Shutdown (); - } - - [Fact] - public void CanFocus_False_Set_HasFocus_To_False () - { - var view = new View { CanFocus = true }; - var view2 = new View { CanFocus = true }; - view2.Add (view); - - Assert.True (view.CanFocus); - - view.SetFocus (); - Assert.True (view.HasFocus); - - view.CanFocus = false; - Assert.False (view.CanFocus); - Assert.False (view.HasFocus); - } - - [Fact] - public void CanFocus_Set_Changes_TabIndex_And_TabStop () - { - var r = new View (); - var v1 = new View { Text = "1" }; - var v2 = new View { Text = "2" }; - var v3 = new View { Text = "3" }; - - r.Add (v1, v2, v3); - - v2.CanFocus = true; - Assert.Equal (r.TabIndexes.IndexOf (v2), v2.TabIndex); - Assert.Equal (0, v2.TabIndex); - Assert.Equal (TabBehavior.TabStop, v2.TabStop); - - v1.CanFocus = true; - Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); - Assert.Equal (1, v1.TabIndex); - Assert.Equal (TabBehavior.TabStop, v1.TabStop); - - v1.TabIndex = 2; - Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); - Assert.Equal (1, v1.TabIndex); - v3.CanFocus = true; - Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); - Assert.Equal (1, v1.TabIndex); - Assert.Equal (r.TabIndexes.IndexOf (v3), v3.TabIndex); - Assert.Equal (2, v3.TabIndex); - Assert.Equal (TabBehavior.TabStop, v3.TabStop); - - v2.CanFocus = false; - Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); - Assert.Equal (1, v1.TabIndex); - Assert.Equal (TabBehavior.TabStop, v1.TabStop); - Assert.Equal (r.TabIndexes.IndexOf (v2), v2.TabIndex); // TabIndex is not changed - Assert.NotEqual (-1, v2.TabIndex); - Assert.Equal (TabBehavior.TabStop, v2.TabStop); // TabStop is not changed - Assert.Equal (r.TabIndexes.IndexOf (v3), v3.TabIndex); - Assert.Equal (2, v3.TabIndex); - Assert.Equal (TabBehavior.TabStop, v3.TabStop); - r.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void CanFocus_Sets_To_False_On_Single_View_Focus_View_On_Another_Toplevel () - { - var view1 = new View { Id = "view1", Width = 10, Height = 1, CanFocus = true }; - var win1 = new Window { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () }; - win1.Add (view1); - var view2 = new View { Id = "view2", Width = 20, Height = 2, CanFocus = true }; - var win2 = new Window { Id = "win2", X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () }; - win2.Add (view2); - var top = new Toplevel (); - top.Add (win1, win2); - Application.Begin (top); - - Assert.True (view1.CanFocus); - Assert.True (view1.HasFocus); - Assert.True (view2.CanFocus); - Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus - - Assert.True (Application.OnKeyDown (Key.F6)); - Assert.True (view1.CanFocus); - Assert.False (view1.HasFocus); // Only one of the most focused toplevels view can have focus - Assert.True (view2.CanFocus); - Assert.True (view2.HasFocus); - - Assert.True (Application.OnKeyDown (Key.F6)); - Assert.True (view1.CanFocus); - Assert.True (view1.HasFocus); - Assert.True (view2.CanFocus); - Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus - - view1.CanFocus = false; - Assert.False (view1.CanFocus); - Assert.False (view1.HasFocus); - Assert.True (view2.CanFocus); - Assert.True (view2.HasFocus); - Assert.Equal (win2, Application.Current.Focused); - Assert.Equal (view2, Application.Current.MostFocused); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void CanFocus_Sets_To_False_On_Toplevel_Focus_View_On_Another_Toplevel () - { - var view1 = new View { Id = "view1", Width = 10, Height = 1, CanFocus = true }; - var win1 = new Window { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () }; - win1.Add (view1); - var view2 = new View { Id = "view2", Width = 20, Height = 2, CanFocus = true }; - var win2 = new Window { Id = "win2", X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () }; - win2.Add (view2); - var top = new Toplevel (); - top.Add (win1, win2); - Application.Begin (top); - - Assert.True (view1.CanFocus); - Assert.True (view1.HasFocus); - Assert.True (view2.CanFocus); - Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus - - Assert.True (Application.OnKeyDown (Key.F6)); - Assert.True (view1.CanFocus); - Assert.False (view1.HasFocus); // Only one of the most focused toplevels view can have focus - Assert.True (view2.CanFocus); - Assert.True (view2.HasFocus); - - Assert.True (Application.OnKeyDown (Key.F6)); - Assert.True (view1.CanFocus); - Assert.True (view1.HasFocus); - Assert.True (view2.CanFocus); - Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus - - win1.CanFocus = false; - Assert.False (view1.CanFocus); - Assert.False (view1.HasFocus); - Assert.False (win1.CanFocus); - Assert.False (win1.HasFocus); - Assert.True (view2.CanFocus); - Assert.True (view2.HasFocus); - Assert.Equal (win2, Application.Current.Focused); - Assert.Equal (view2, Application.Current.MostFocused); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void CanFocus_Sets_To_False_With_Two_Views_Focus_Another_View_On_The_Same_Toplevel () - { - var view1 = new View { Id = "view1", Width = 10, Height = 1, CanFocus = true }; - - var view12 = new View - { - Id = "view12", - Y = 5, - Width = 10, - Height = 1, - CanFocus = true - }; - var win1 = new Window { Id = "win1", Width = Dim.Percent (50), Height = Dim.Fill () }; - win1.Add (view1, view12); - var view2 = new View { Id = "view2", Width = 20, Height = 2, CanFocus = true }; - var win2 = new Window { Id = "win2", X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () }; - win2.Add (view2); - var top = new Toplevel (); - top.Add (win1, win2); - Application.Begin (top); - - Assert.True (view1.CanFocus); - Assert.True (view1.HasFocus); - Assert.True (view2.CanFocus); - Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus - - Assert.True (Application.OnKeyDown (Key.F6)); // move to win2 - Assert.True (view1.CanFocus); - Assert.False (view1.HasFocus); // Only one of the most focused toplevels view can have focus - Assert.True (view2.CanFocus); - Assert.True (view2.HasFocus); - - Assert.True (Application.OnKeyDown (Key.F6)); - Assert.True (view1.CanFocus); - Assert.True (view1.HasFocus); - Assert.True (view2.CanFocus); - Assert.False (view2.HasFocus); // Only one of the most focused toplevels view can have focus - - view1.CanFocus = false; - Assert.False (view1.CanFocus); - Assert.False (view1.HasFocus); - Assert.True (view2.CanFocus); - Assert.False (view2.HasFocus); - Assert.Equal (win1, Application.Current.Focused); - Assert.Equal (view12, Application.Current.MostFocused); - top.Dispose (); - } - - [Fact] - public void Enabled_False_Sets_HasFocus_To_False () - { - var wasClicked = false; - var view = new Button { Text = "Click Me" }; - view.Accept += (s, e) => wasClicked = !wasClicked; - - view.NewKeyDownEvent (Key.Space); - Assert.True (wasClicked); - view.NewMouseEvent (new() { Flags = MouseFlags.Button1Clicked }); - Assert.False (wasClicked); - Assert.True (view.Enabled); - Assert.True (view.CanFocus); - Assert.True (view.HasFocus); - - view.Enabled = false; - view.NewKeyDownEvent (Key.Space); - Assert.False (wasClicked); - view.NewMouseEvent (new() { Flags = MouseFlags.Button1Clicked }); - Assert.False (wasClicked); - Assert.False (view.Enabled); - Assert.True (view.CanFocus); - Assert.False (view.HasFocus); - view.SetFocus (); - Assert.False (view.HasFocus); - } - - [Fact] - [AutoInitShutdown] - public void Enabled_Sets_Also_Sets_Subviews () - { - var wasClicked = false; - var button = new Button { Text = "Click Me" }; - button.IsDefault = true; - button.Accept += (s, e) => wasClicked = !wasClicked; - var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; - win.Add (button); - var top = new Toplevel (); - top.Add (win); - - var iterations = 0; - - Application.Iteration += (s, a) => - { - iterations++; - - win.NewKeyDownEvent (Key.Enter); - Assert.True (wasClicked); - button.NewMouseEvent (new() { Flags = MouseFlags.Button1Clicked }); - Assert.False (wasClicked); - Assert.True (button.Enabled); - Assert.True (button.CanFocus); - Assert.True (button.HasFocus); - Assert.True (win.Enabled); - Assert.True (win.CanFocus); - Assert.True (win.HasFocus); - - win.Enabled = false; - button.NewKeyDownEvent (Key.Enter); - Assert.False (wasClicked); - button.NewMouseEvent (new() { Flags = MouseFlags.Button1Clicked }); - Assert.False (wasClicked); - Assert.False (button.Enabled); - Assert.True (button.CanFocus); - Assert.False (button.HasFocus); - Assert.False (win.Enabled); - Assert.True (win.CanFocus); - Assert.False (win.HasFocus); - button.SetFocus (); - Assert.False (button.HasFocus); - Assert.False (win.HasFocus); - win.SetFocus (); - Assert.False (button.HasFocus); - Assert.False (win.HasFocus); - - win.Enabled = true; - win.FocusFirst (null); - Assert.True (button.HasFocus); - Assert.True (win.HasFocus); - - Application.RequestStop (); - }; - - Application.Run (top); - - Assert.Equal (1, iterations); - top.Dispose (); - } - - // View.Focused & View.MostFocused tests - - // View.Focused - No subviews - [Fact] - [Trait ("BUGBUG", "Fix in Issue #3444")] - public void Focused_NoSubviews () - { - var view = new View (); - Assert.Null (view.Focused); - - view.CanFocus = true; - view.SetFocus (); - Assert.True (view.HasFocus); - Assert.Null (view.Focused); // BUGBUG: Should be view - } - - [Fact] - [AutoInitShutdown] - public void FocusNearestView_Ensure_Focus_Ordered () - { - var top = new Toplevel (); - - var win = new Window (); - var winSubview = new View { CanFocus = true, Text = "WindowSubview" }; - win.Add (winSubview); - top.Add (win); - - var frm = new FrameView (); - var frmSubview = new View { CanFocus = true, Text = "FrameSubview" }; - frm.Add (frmSubview); - top.Add (frm); - - Application.Begin (top); - Assert.Equal ("WindowSubview", top.MostFocused.Text); - - Application.OnKeyDown (Key.Tab); - Assert.Equal ("WindowSubview", top.MostFocused.Text); - - Application.OnKeyDown (Key.F6); - Assert.Equal ("FrameSubview", top.MostFocused.Text); - - Application.OnKeyDown (Key.Tab); - Assert.Equal ("FrameSubview", top.MostFocused.Text); - - Application.OnKeyDown (Key.F6); - Assert.Equal ("WindowSubview", top.MostFocused.Text); - - Application.OnKeyDown (Key.F6.WithShift); - Assert.Equal ("FrameSubview", top.MostFocused.Text); - - Application.OnKeyDown (Key.F6.WithShift); - Assert.Equal ("WindowSubview", top.MostFocused.Text); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void FocusNext_Does_Not_Throws_If_A_View_Was_Removed_From_The_Collection () - { - Toplevel top1 = new (); - var view1 = new View { Id = "view1", Width = 10, Height = 5, CanFocus = true }; - var top2 = new Toplevel { Id = "top2", Y = 1, Width = 10, Height = 5 }; - - var view2 = new View - { - Id = "view2", - Y = 1, - Width = 10, - Height = 5, - CanFocus = true - }; - View view3 = null; - var removed = false; - - view2.Enter += (s, e) => - { - if (!removed) - { - removed = true; - view3 = new() { Id = "view3", Y = 1, Width = 10, Height = 5 }; - Application.Current.Add (view3); - Application.Current.BringSubviewToFront (view3); - Assert.False (view3.HasFocus); - } - }; - - view2.Leave += (s, e) => - { - Application.Current.Remove (view3); - view3.Dispose (); - view3 = null; - }; - top2.Add (view2); - top1.Add (view1, top2); - Application.Begin (top1); - - Assert.True (top1.HasFocus); - Assert.True (view1.HasFocus); - Assert.False (view2.HasFocus); - Assert.False (removed); - Assert.Null (view3); - - Assert.True (Application.OnKeyDown (Key.F6)); - Assert.True (top1.HasFocus); - Assert.False (view1.HasFocus); - Assert.True (view2.HasFocus); - Assert.True (removed); - Assert.NotNull (view3); - - Exception exception = Record.Exception (() => Application.OnKeyDown (Key.F6)); - Assert.Null (exception); - Assert.True (removed); - Assert.Null (view3); - top1.Dispose (); - } - - // View.MostFocused - No subviews - [Fact] - [Trait ("BUGBUG", "Fix in Issue #3444")] - public void Most_Focused_NoSubviews () - { - var view = new View (); - Assert.Null (view.Focused); - - view.CanFocus = true; - view.SetFocus (); - Assert.True (view.HasFocus); - Assert.Null (view.MostFocused); // BUGBUG: Should be view - } - - // [Fact] - // [AutoInitShutdown] - // public void HotKey_Will_Invoke_KeyPressed_Only_For_The_MostFocused_With_Top_KeyPress_Event () - // { - // var sbQuiting = false; - // var tfQuiting = false; - // var topQuiting = false; - - // var sb = new StatusBar ( - // new Shortcut [] - // { - // new ( - // KeyCode.CtrlMask | KeyCode.Q, - // "Quit", - // () => sbQuiting = true - // ) - // } - // ); - // var tf = new TextField (); - // tf.KeyDown += Tf_KeyPressed; - - // void Tf_KeyPressed (object sender, Key obj) - // { - // if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) - // { - // obj.Handled = tfQuiting = true; - // } - // } - - // var win = new Window (); - // win.Add (sb, tf); - // Toplevel top = new (); - // top.KeyDown += Top_KeyPress; - - // void Top_KeyPress (object sender, Key obj) - // { - // if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) - // { - // obj.Handled = topQuiting = true; - // } - // } - - // top.Add (win); - // Application.Begin (top); - - // Assert.False (sbQuiting); - // Assert.False (tfQuiting); - // Assert.False (topQuiting); - - // Application.Driver?.SendKeys ('Q', ConsoleKey.Q, false, false, true); - // Assert.False (sbQuiting); - // Assert.True (tfQuiting); - // Assert.False (topQuiting); - - //#if BROKE_WITH_2927 - // tf.KeyPressed -= Tf_KeyPress; - // tfQuiting = false; - // Application.Driver?.SendKeys ('q', ConsoleKey.Q, false, false, true); - // Application.MainLoop.RunIteration (); - // Assert.True (sbQuiting); - // Assert.False (tfQuiting); - // Assert.False (topQuiting); - - // sb.RemoveItem (0); - // sbQuiting = false; - // Application.Driver?.SendKeys ('q', ConsoleKey.Q, false, false, true); - // Application.MainLoop.RunIteration (); - // Assert.False (sbQuiting); - // Assert.False (tfQuiting); - - //// This test is now invalid because `win` is focused, so it will receive the keypress - // Assert.True (topQuiting); - //#endif - // top.Dispose (); - // } - - // [Fact] - // [AutoInitShutdown] - // public void HotKey_Will_Invoke_KeyPressed_Only_For_The_MostFocused_Without_Top_KeyPress_Event () - // { - // var sbQuiting = false; - // var tfQuiting = false; - - // var sb = new StatusBar ( - // new Shortcut [] - // { - // new ( - // KeyCode.CtrlMask | KeyCode.Q, - // "~^Q~ Quit", - // () => sbQuiting = true - // ) - // } - // ); - // var tf = new TextField (); - // tf.KeyDown += Tf_KeyPressed; - - // void Tf_KeyPressed (object sender, Key obj) - // { - // if (obj.KeyCode == (KeyCode.Q | KeyCode.CtrlMask)) - // { - // obj.Handled = tfQuiting = true; - // } - // } - - // var win = new Window (); - // win.Add (sb, tf); - // Toplevel top = new (); - // top.Add (win); - // Application.Begin (top); - - // Assert.False (sbQuiting); - // Assert.False (tfQuiting); - - // Application.Driver?.SendKeys ('Q', ConsoleKey.Q, false, false, true); - // Assert.False (sbQuiting); - // Assert.True (tfQuiting); - - // tf.KeyDown -= Tf_KeyPressed; - // tfQuiting = false; - // Application.Driver?.SendKeys ('Q', ConsoleKey.Q, false, false, true); - // Application.MainLoop.RunIteration (); - //#if BROKE_WITH_2927 - // Assert.True (sbQuiting); - // Assert.False (tfQuiting); - //#endif - // top.Dispose (); - // } - - [Fact] - [SetupFakeDriver] - public void Navigation_With_Null_Focused_View () - { - // Non-regression test for #882 (NullReferenceException during keyboard navigation when Focused is null) - - Application.Init (new FakeDriver ()); - - var top = new Toplevel (); - top.Ready += (s, e) => { Assert.Null (top.Focused); }; - - // Keyboard navigation with tab - FakeConsole.MockKeyPresses.Push (new ('\t', ConsoleKey.Tab, false, false, false)); - - Application.Iteration += (s, a) => Application.RequestStop (); - - Application.Run (top); - top.Dispose (); - Application.Shutdown (); - } - - [Fact] - [AutoInitShutdown] - public void Remove_Does_Not_Change_Focus () - { - var top = new Toplevel (); - Assert.True (top.CanFocus); - Assert.False (top.HasFocus); - - var container = new View { Width = 10, Height = 10 }; - var leave = false; - container.Leave += (s, e) => leave = true; - Assert.False (container.CanFocus); - var child = new View { Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true }; - container.Add (child); - - Assert.True (container.CanFocus); - Assert.False (container.HasFocus); - Assert.True (child.CanFocus); - Assert.False (child.HasFocus); - - top.Add (container); - Application.Begin (top); - - Assert.True (top.CanFocus); - Assert.True (top.HasFocus); - Assert.True (container.CanFocus); - Assert.True (container.HasFocus); - Assert.True (child.CanFocus); - Assert.True (child.HasFocus); - - container.Remove (child); - child.Dispose (); - child = null; - Assert.True (top.HasFocus); - Assert.True (container.CanFocus); - Assert.True (container.HasFocus); - Assert.Null (child); - Assert.False (leave); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void ScreenToView_ViewToScreen_FindDeepestView_Full_Top () - { - Toplevel top = new (); - top.BorderStyle = LineStyle.Single; - - var view = new View - { - X = 3, - Y = 2, - Width = 10, - Height = 1, - Text = "0123456789" - }; - top.Add (view); - - Application.Begin (top); - - Assert.Equal (Application.Current, top); - Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); - Assert.Equal (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); - Assert.Equal (new (0, 0, 80, 25), top.Frame); - - ((FakeDriver)Application.Driver!).SetBufferSize (20, 10); - Assert.Equal (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); - Assert.Equal (new (0, 0, 20, 10), top.Frame); - - _ = TestHelpers.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────┐ -│ │ -│ │ -│ 0123456789 │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────┘", - _output - ); - - // top - Assert.Equal (Point.Empty, top.ScreenToFrame (new (0, 0))); - Point screen = top.Margin.ViewportToScreen (new Point (0, 0)); - Assert.Equal (0, screen.X); - Assert.Equal (0, screen.Y); - screen = top.Border.ViewportToScreen (new Point (0, 0)); - Assert.Equal (0, screen.X); - Assert.Equal (0, screen.Y); - screen = top.Padding.ViewportToScreen (new Point (0, 0)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = top.ViewportToScreen (new Point (0, 0)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = top.ViewportToScreen (new Point (-1, -1)); - Assert.Equal (0, screen.X); - Assert.Equal (0, screen.Y); - var found = View.FindDeepestView (top, new (0, 0)); - Assert.Equal (top.Border, found); - - Assert.Equal (0, found.Frame.X); - Assert.Equal (0, found.Frame.Y); - Assert.Equal (new (3, 2), top.ScreenToFrame (new (3, 2))); - screen = top.ViewportToScreen (new Point (3, 2)); - Assert.Equal (4, screen.X); - Assert.Equal (3, screen.Y); - found = View.FindDeepestView (top, new (screen.X, screen.Y)); - Assert.Equal (view, found); - - //Assert.Equal (0, found.FrameToScreen ().X); - //Assert.Equal (0, found.FrameToScreen ().Y); - found = View.FindDeepestView (top, new (3, 2)); - Assert.Equal (top, found); - - //Assert.Equal (3, found.FrameToScreen ().X); - //Assert.Equal (2, found.FrameToScreen ().Y); - Assert.Equal (new (13, 2), top.ScreenToFrame (new (13, 2))); - screen = top.ViewportToScreen (new Point (12, 2)); - Assert.Equal (13, screen.X); - Assert.Equal (3, screen.Y); - found = View.FindDeepestView (top, new (screen.X, screen.Y)); - Assert.Equal (view, found); - - //Assert.Equal (9, found.FrameToScreen ().X); - //Assert.Equal (0, found.FrameToScreen ().Y); - screen = top.ViewportToScreen (new Point (13, 2)); - Assert.Equal (14, screen.X); - Assert.Equal (3, screen.Y); - found = View.FindDeepestView (top, new (13, 2)); - Assert.Equal (top, found); - - //Assert.Equal (13, found.FrameToScreen ().X); - //Assert.Equal (2, found.FrameToScreen ().Y); - Assert.Equal (new (14, 3), top.ScreenToFrame (new (14, 3))); - screen = top.ViewportToScreen (new Point (14, 3)); - Assert.Equal (15, screen.X); - Assert.Equal (4, screen.Y); - found = View.FindDeepestView (top, new (14, 3)); - Assert.Equal (top, found); - - //Assert.Equal (14, found.FrameToScreen ().X); - //Assert.Equal (3, found.FrameToScreen ().Y); - - // view - Assert.Equal (new (-4, -3), view.ScreenToFrame (new (0, 0))); - screen = view.Margin.ViewportToScreen (new Point (-3, -2)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = view.Border.ViewportToScreen (new Point (-3, -2)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = view.Padding.ViewportToScreen (new Point (-3, -2)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = view.ViewportToScreen (new Point (-3, -2)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = view.ViewportToScreen (new Point (-4, -3)); - Assert.Equal (0, screen.X); - Assert.Equal (0, screen.Y); - found = View.FindDeepestView (top, new (0, 0)); - Assert.Equal (top.Border, found); - - Assert.Equal (new (-1, -1), view.ScreenToFrame (new (3, 2))); - screen = view.ViewportToScreen (new Point (0, 0)); - Assert.Equal (4, screen.X); - Assert.Equal (3, screen.Y); - found = View.FindDeepestView (top, new (4, 3)); - Assert.Equal (view, found); - - Assert.Equal (new (9, -1), view.ScreenToFrame (new (13, 2))); - screen = view.ViewportToScreen (new Point (10, 0)); - Assert.Equal (14, screen.X); - Assert.Equal (3, screen.Y); - found = View.FindDeepestView (top, new (14, 3)); - Assert.Equal (top, found); - - Assert.Equal (new (10, 0), view.ScreenToFrame (new (14, 3))); - screen = view.ViewportToScreen (new Point (11, 1)); - Assert.Equal (15, screen.X); - Assert.Equal (4, screen.Y); - found = View.FindDeepestView (top, new (15, 4)); - Assert.Equal (top, found); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void ScreenToView_ViewToScreen_FindDeepestView_Smaller_Top () - { - var top = new Toplevel - { - X = 3, - Y = 2, - Width = 20, - Height = 10, - BorderStyle = LineStyle.Single - }; - - var view = new View - { - X = 3, - Y = 2, - Width = 10, - Height = 1, - Text = "0123456789" - }; - top.Add (view); - - Application.Begin (top); - - Assert.Equal (Application.Current, top); - Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); - Assert.NotEqual (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); - Assert.Equal (new (3, 2, 20, 10), top.Frame); - - ((FakeDriver)Application.Driver!).SetBufferSize (30, 20); - Assert.Equal (new (0, 0, 30, 20), new Rectangle (0, 0, View.Driver.Cols, View.Driver.Rows)); - Assert.NotEqual (new (0, 0, View.Driver.Cols, View.Driver.Rows), top.Frame); - Assert.Equal (new (3, 2, 20, 10), top.Frame); - - Rectangle frame = TestHelpers.AssertDriverContentsWithFrameAre ( - @" - ┌──────────────────┐ - │ │ - │ │ - │ 0123456789 │ - │ │ - │ │ - │ │ - │ │ - │ │ - └──────────────────┘", - _output - ); - - // mean the output started at col 3 and line 2 - // which result with a width of 23 and a height of 10 on the output - Assert.Equal (new (3, 2, 23, 10), frame); - - // top - Assert.Equal (new (-3, -2), top.ScreenToFrame (new (0, 0))); - Point screen = top.Margin.ViewportToScreen (new Point (-3, -2)); - Assert.Equal (0, screen.X); - Assert.Equal (0, screen.Y); - screen = top.Border.ViewportToScreen (new Point (-3, -2)); - Assert.Equal (0, screen.X); - Assert.Equal (0, screen.Y); - screen = top.Padding.ViewportToScreen (new Point (-3, -2)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = top.ViewportToScreen (new Point (-3, -2)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = top.ViewportToScreen (new Point (-4, -3)); - Assert.Equal (0, screen.X); - Assert.Equal (0, screen.Y); - var found = View.FindDeepestView (top, new (-4, -3)); - Assert.Null (found); - Assert.Equal (Point.Empty, top.ScreenToFrame (new (3, 2))); - screen = top.ViewportToScreen (new Point (0, 0)); - Assert.Equal (4, screen.X); - Assert.Equal (3, screen.Y); - Assert.Equal (top.Border, View.FindDeepestView (top, new (3, 2))); - - //Assert.Equal (0, found.FrameToScreen ().X); - //Assert.Equal (0, found.FrameToScreen ().Y); - Assert.Equal (new (10, 0), top.ScreenToFrame (new (13, 2))); - screen = top.ViewportToScreen (new Point (10, 0)); - Assert.Equal (14, screen.X); - Assert.Equal (3, screen.Y); - Assert.Equal (top.Border, View.FindDeepestView (top, new (13, 2))); - - //Assert.Equal (10, found.FrameToScreen ().X); - //Assert.Equal (0, found.FrameToScreen ().Y); - Assert.Equal (new (11, 1), top.ScreenToFrame (new (14, 3))); - screen = top.ViewportToScreen (new Point (11, 1)); - Assert.Equal (15, screen.X); - Assert.Equal (4, screen.Y); - Assert.Equal (top, View.FindDeepestView (top, new (14, 3))); - - // view - Assert.Equal (new (-7, -5), view.ScreenToFrame (new (0, 0))); - screen = view.Margin.ViewportToScreen (new Point (-6, -4)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = view.Border.ViewportToScreen (new Point (-6, -4)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = view.Padding.ViewportToScreen (new Point (-6, -4)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - screen = view.ViewportToScreen (new Point (-6, -4)); - Assert.Equal (1, screen.X); - Assert.Equal (1, screen.Y); - Assert.Null (View.FindDeepestView (top, new (1, 1))); - Assert.Equal (new (-4, -3), view.ScreenToFrame (new (3, 2))); - screen = view.ViewportToScreen (new Point (-3, -2)); - Assert.Equal (4, screen.X); - Assert.Equal (3, screen.Y); - Assert.Equal (top, View.FindDeepestView (top, new (4, 3))); - Assert.Equal (new (-1, -1), view.ScreenToFrame (new (6, 4))); - screen = view.ViewportToScreen (new Point (0, 0)); - Assert.Equal (7, screen.X); - Assert.Equal (5, screen.Y); - Assert.Equal (view, View.FindDeepestView (top, new (7, 5))); - Assert.Equal (new (6, -1), view.ScreenToFrame (new (13, 4))); - screen = view.ViewportToScreen (new Point (7, 0)); - Assert.Equal (14, screen.X); - Assert.Equal (5, screen.Y); - Assert.Equal (view, View.FindDeepestView (top, new (14, 5))); - Assert.Equal (new (7, -2), view.ScreenToFrame (new (14, 3))); - screen = view.ViewportToScreen (new Point (8, -1)); - Assert.Equal (15, screen.X); - Assert.Equal (4, screen.Y); - Assert.Equal (top, View.FindDeepestView (top, new (15, 4))); - Assert.Equal (new (16, -2), view.ScreenToFrame (new (23, 3))); - screen = view.ViewportToScreen (new Point (17, -1)); - Assert.Equal (24, screen.X); - Assert.Equal (4, screen.Y); - Assert.Null (View.FindDeepestView (top, new (24, 4))); - top.Dispose (); - } - - [Fact] - public void SendSubviewBackwards_Subviews_vs_TabIndexes () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - r.SendSubviewBackwards (v3); - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.Subviews.IndexOf (v2) == 2); - Assert.True (r.Subviews.IndexOf (v3) == 1); - - Assert.True (r.TabIndexes.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v2) == 1); - Assert.True (r.TabIndexes.IndexOf (v3) == 2); - r.Dispose (); - } - - [Fact] - public void SendSubviewToBack_Subviews_vs_TabIndexes () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - r.SendSubviewToBack (v3); - Assert.True (r.Subviews.IndexOf (v1) == 1); - Assert.True (r.Subviews.IndexOf (v2) == 2); - Assert.True (r.Subviews.IndexOf (v3) == 0); - - Assert.True (r.TabIndexes.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v2) == 1); - Assert.True (r.TabIndexes.IndexOf (v3) == 2); - r.Dispose (); - } - - [Fact] - public void SetFocus_View_With_Null_Superview_Does_Not_Throw_Exception () - { - var top = new Toplevel (); - Assert.True (top.CanFocus); - Assert.False (top.HasFocus); - - Exception exception = Record.Exception (top.SetFocus); - Assert.Null (exception); - Assert.True (top.CanFocus); - Assert.True (top.HasFocus); - } - - [Fact] - [AutoInitShutdown] - public void SetHasFocus_Do_Not_Throws_If_OnLeave_Remove_Focused_Changing_To_Null () - { - var view1Leave = false; - var subView1Leave = false; - var subView1subView1Leave = false; - Toplevel top = new (); - var view1 = new View { CanFocus = true }; - var subView1 = new View { CanFocus = true }; - var subView1subView1 = new View { CanFocus = true }; - view1.Leave += (s, e) => { view1Leave = true; }; - - subView1.Leave += (s, e) => - { - subView1.Remove (subView1subView1); - subView1Leave = true; - }; - view1.Add (subView1); - - subView1subView1.Leave += (s, e) => - { - // This is never invoked - subView1subView1Leave = true; - }; - subView1.Add (subView1subView1); - var view2 = new View { CanFocus = true }; - top.Add (view1, view2); - RunState rs = Application.Begin (top); - - view2.SetFocus (); - Assert.True (view1Leave); - Assert.True (subView1Leave); - Assert.False (subView1subView1Leave); - Application.End (rs); - subView1subView1.Dispose (); - top.Dispose (); - } - - [Fact] - public void Subviews_TabIndexes_AreEqual () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.Subviews.IndexOf (v2) == 1); - Assert.True (r.Subviews.IndexOf (v3) == 2); - - Assert.True (r.TabIndexes.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v2) == 1); - Assert.True (r.TabIndexes.IndexOf (v3) == 2); - - Assert.Equal (r.Subviews.IndexOf (v1), r.TabIndexes.IndexOf (v1)); - Assert.Equal (r.Subviews.IndexOf (v2), r.TabIndexes.IndexOf (v2)); - Assert.Equal (r.Subviews.IndexOf (v3), r.TabIndexes.IndexOf (v3)); - r.Dispose (); - } - - [Fact] - public void TabIndex_Invert_Order () - { - var r = new View (); - var v1 = new View { Id = "1", CanFocus = true }; - var v2 = new View { Id = "2", CanFocus = true }; - var v3 = new View { Id = "3", CanFocus = true }; - - r.Add (v1, v2, v3); - - v1.TabIndex = 2; - v2.TabIndex = 1; - v3.TabIndex = 0; - Assert.True (r.TabIndexes.IndexOf (v1) == 2); - Assert.True (r.TabIndexes.IndexOf (v2) == 1); - Assert.True (r.TabIndexes.IndexOf (v3) == 0); - - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.Subviews.IndexOf (v2) == 1); - Assert.True (r.Subviews.IndexOf (v3) == 2); - } - - [Fact] - public void TabIndex_Invert_Order_Added_One_By_One_Does_Not_Do_What_Is_Expected () - { - var r = new View (); - var v1 = new View { Id = "1", CanFocus = true }; - r.Add (v1); - v1.TabIndex = 2; - var v2 = new View { Id = "2", CanFocus = true }; - r.Add (v2); - v2.TabIndex = 1; - var v3 = new View { Id = "3", CanFocus = true }; - r.Add (v3); - v3.TabIndex = 0; - - Assert.False (r.TabIndexes.IndexOf (v1) == 2); - Assert.True (r.TabIndexes.IndexOf (v1) == 1); - Assert.False (r.TabIndexes.IndexOf (v2) == 1); - Assert.True (r.TabIndexes.IndexOf (v2) == 2); - - // Only the last is in the expected index - Assert.True (r.TabIndexes.IndexOf (v3) == 0); - - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.Subviews.IndexOf (v2) == 1); - Assert.True (r.Subviews.IndexOf (v3) == 2); - } - - [Fact] - public void TabIndex_Invert_Order_Mixed () - { - var r = new View (); - var vl1 = new View { Id = "vl1" }; - var v1 = new View { Id = "v1", CanFocus = true }; - var vl2 = new View { Id = "vl2" }; - var v2 = new View { Id = "v2", CanFocus = true }; - var vl3 = new View { Id = "vl3" }; - var v3 = new View { Id = "v3", CanFocus = true }; - - r.Add (vl1, v1, vl2, v2, vl3, v3); - - v1.TabIndex = 2; - v2.TabIndex = 1; - v3.TabIndex = 0; - Assert.True (r.TabIndexes.IndexOf (v1) == 4); - Assert.True (r.TabIndexes.IndexOf (v2) == 2); - Assert.True (r.TabIndexes.IndexOf (v3) == 0); - - Assert.True (r.Subviews.IndexOf (v1) == 1); - Assert.True (r.Subviews.IndexOf (v2) == 3); - Assert.True (r.Subviews.IndexOf (v3) == 5); - } - - [Fact] - public void TabIndex_Set_CanFocus_False () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - v1.CanFocus = false; - v1.TabIndex = 0; - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v1) == 0); - Assert.NotEqual (-1, v1.TabIndex); - r.Dispose (); - } - - [Fact] - public void TabIndex_Set_CanFocus_False_To_True () - { - var r = new View (); - var v1 = new View (); - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - v1.CanFocus = true; - v1.TabIndex = 1; - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v1) == 1); - r.Dispose (); - } - - [Fact] - public void TabIndex_Set_CanFocus_HigherValues () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - v1.TabIndex = 3; - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v1) == 2); - r.Dispose (); - } - - [Fact] - public void TabIndex_Set_CanFocus_LowerValues () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - //v1.TabIndex = -1; - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v1) == 0); - r.Dispose (); - } - - [Fact] - public void TabIndex_Set_CanFocus_ValidValues () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - v1.TabIndex = 1; - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v1) == 1); - - v1.TabIndex = 2; - Assert.True (r.Subviews.IndexOf (v1) == 0); - Assert.True (r.TabIndexes.IndexOf (v1) == 2); - r.Dispose (); - } - - [Fact] - public void TabStop_And_CanFocus_Are_All_True () - { - var r = new View (); - var v1 = new View { CanFocus = true }; - var v2 = new View { CanFocus = true }; - var v3 = new View { CanFocus = true }; - - r.Add (v1, v2, v3); - - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.True (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.True (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.True (v3.HasFocus); - r.Dispose (); - } - - [Theory] - [CombinatorialData] - public void TabStop_And_CanFocus_Are_Decoupled (bool canFocus, TabBehavior tabStop) - { - var view = new View { CanFocus = canFocus, TabStop = tabStop }; - - Assert.Equal (canFocus, view.CanFocus); - Assert.Equal (tabStop, view.TabStop); - } - - [Fact] - public void TabStop_And_CanFocus_Mixed () - { - var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v2 = new View { CanFocus = false, TabStop = TabBehavior.TabStop }; - var v3 = new View { CanFocus = false, TabStop = TabBehavior.NoStop }; - - r.Add (v1, v2, v3); - - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.Dispose (); - } - - [Theory] - [CombinatorialData] - public void TabStop_Change_CanFocus_Works ([CombinatorialValues (TabBehavior.NoStop, TabBehavior.TabStop, TabBehavior.TabGroup)] TabBehavior behavior) - { - var r = new View (); - var v1 = new View (); - var v2 = new View (); - var v3 = new View (); - Assert.False (v1.CanFocus); - Assert.False (v2.CanFocus); - Assert.False (v3.CanFocus); - - r.Add (v1, v2, v3); - - r.AdvanceFocus (NavigationDirection.Forward, behavior); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - - v1.CanFocus = true; - v1.TabStop = behavior; - r.AdvanceFocus (NavigationDirection.Forward, behavior); - Assert.True (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - - v2.CanFocus = true; - v2.TabStop = behavior; - r.AdvanceFocus (NavigationDirection.Forward, behavior); - Assert.False (v1.HasFocus); - Assert.True (v2.HasFocus); - Assert.False (v3.HasFocus); - - v3.CanFocus = true; - v3.TabStop = behavior; - r.AdvanceFocus (NavigationDirection.Forward, behavior); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.True (v3.HasFocus); - r.Dispose (); - } - - [Fact] - public void TabStop_NoStop_And_CanFocus_True_No_Focus () - { - var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - - r.Add (v1, v2, v3); - - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.Dispose (); - } - - [Fact] - public void TabStop_NoStop_Change_Enables_Stop () - { - var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - - r.Add (v1, v2, v3); - - v1.TabStop = TabBehavior.TabStop; - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.True (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - - v2.TabStop = TabBehavior.TabStop; - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.True (v2.HasFocus); - Assert.False (v3.HasFocus); - - v3.TabStop = TabBehavior.TabStop; - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.True (v3.HasFocus); - r.Dispose (); - } - - [Fact] - public void TabStop_NoStop_Prevents_Stop () - { - var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v2 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - var v3 = new View { CanFocus = true, TabStop = TabBehavior.NoStop }; - - r.Add (v1, v2, v3); - - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - } - - [Fact] - public void TabStop_Null_And_CanFocus_False_No_Advance () - { - var r = new View (); - var v1 = new View (); - var v2 = new View (); - var v3 = new View (); - Assert.False (v1.CanFocus); - Assert.Null (v1.TabStop); - - r.Add (v1, v2, v3); - - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - Assert.False (v1.HasFocus); - Assert.False (v2.HasFocus); - Assert.False (v3.HasFocus); - r.Dispose (); - } - - [Fact (Skip = "Causes crash on Ubuntu in Github Action. Bogus test anyway.")] - public void WindowDispose_CanFocusProblem () - { - // Arrange - Application.Init (); - using var top = new Toplevel (); - using var view = new View { X = 0, Y = 1, Text = nameof (WindowDispose_CanFocusProblem) }; - using var window = new Window (); - top.Add (window); - window.Add (view); - - // Act - RunState rs = Application.Begin (top); - Application.End (rs); - top.Dispose (); - Application.Shutdown (); - - // Assert does Not throw NullReferenceException - top.SetFocus (); - } -} diff --git a/UnitTests/View/SubviewTests.cs b/UnitTests/View/SubviewTests.cs index c55afe1046..e25d923a4b 100644 --- a/UnitTests/View/SubviewTests.cs +++ b/UnitTests/View/SubviewTests.cs @@ -17,15 +17,15 @@ public void Added_Removed () v.Added += (s, e) => { - Assert.Same (v.SuperView, e.Parent); - Assert.Same (t, e.Parent); - Assert.Same (v, e.Child); + Assert.Same (v.SuperView, e.SuperView); + Assert.Same (t, e.SuperView); + Assert.Same (v, e.SubView); }; v.Removed += (s, e) => { - Assert.Same (t, e.Parent); - Assert.Same (v, e.Child); + Assert.Same (t, e.SuperView); + Assert.Same (v, e.SubView); Assert.True (v.SuperView == null); }; @@ -108,26 +108,26 @@ public void Initialized_Event_Comparing_With_Added_Event () winAddedToTop.Added += (s, e) => { - Assert.Equal (e.Parent.Frame.Width, winAddedToTop.Frame.Width); - Assert.Equal (e.Parent.Frame.Height, winAddedToTop.Frame.Height); + Assert.Equal (e.SuperView.Frame.Width, winAddedToTop.Frame.Width); + Assert.Equal (e.SuperView.Frame.Height, winAddedToTop.Frame.Height); }; v1AddedToWin.Added += (s, e) => { - Assert.Equal (e.Parent.Frame.Width, v1AddedToWin.Frame.Width); - Assert.Equal (e.Parent.Frame.Height, v1AddedToWin.Frame.Height); + Assert.Equal (e.SuperView.Frame.Width, v1AddedToWin.Frame.Width); + Assert.Equal (e.SuperView.Frame.Height, v1AddedToWin.Frame.Height); }; v2AddedToWin.Added += (s, e) => { - Assert.Equal (e.Parent.Frame.Width, v2AddedToWin.Frame.Width); - Assert.Equal (e.Parent.Frame.Height, v2AddedToWin.Frame.Height); + Assert.Equal (e.SuperView.Frame.Width, v2AddedToWin.Frame.Width); + Assert.Equal (e.SuperView.Frame.Height, v2AddedToWin.Frame.Height); }; svAddedTov1.Added += (s, e) => { - Assert.Equal (e.Parent.Frame.Width, svAddedTov1.Frame.Width); - Assert.Equal (e.Parent.Frame.Height, svAddedTov1.Frame.Height); + Assert.Equal (e.SuperView.Frame.Width, svAddedTov1.Frame.Width); + Assert.Equal (e.SuperView.Frame.Height, svAddedTov1.Frame.Height); }; top.Initialized += (s, e) => @@ -191,7 +191,7 @@ public void Initialized_Event_Comparing_With_Added_Event () //Assert.Equal (top.Viewport.Width, svAddedTov1.Frame.Width); //Assert.Equal (top.Viewport.Height, svAddedTov1.Frame.Height); Assert.False (svAddedTov1.CanFocus); - Assert.Throws (() => svAddedTov1.CanFocus = true); + //Assert.Throws (() => svAddedTov1.CanFocus = true); Assert.False (svAddedTov1.CanFocus); }; @@ -291,7 +291,7 @@ public void Initialized_Event_Will_Be_Invoked_When_Added_Dynamically () Assert.NotEqual (t.Frame.Width, sv1.Frame.Width); Assert.NotEqual (t.Frame.Height, sv1.Frame.Height); Assert.False (sv1.CanFocus); - Assert.Throws (() => sv1.CanFocus = true); + //Assert.Throws (() => sv1.CanFocus = true); Assert.False (sv1.CanFocus); }; @@ -373,4 +373,130 @@ public void Remove_Does_Not_Impact_ContentSize () view.Remove (subview); Assert.Equal (new Size (5, 5), view.GetContentSize ()); } + + [Fact] + public void MoveSubviewToStart () + { + View superView = new (); + + View subview1 = new View () + { + Id = "subview1" + }; + + View subview2 = new View () + { + Id = "subview2" + }; + + View subview3 = new View () + { + Id = "subview3" + }; + + superView.Add (subview1, subview2, subview3); + + superView.MoveSubviewToStart (subview2); + Assert.Equal(subview2, superView.Subviews [0]); + + superView.MoveSubviewToStart (subview3); + Assert.Equal (subview3, superView.Subviews [0]); + } + + + [Fact] + public void MoveSubviewTowardsFront () + { + View superView = new (); + + View subview1 = new View () + { + Id = "subview1" + }; + + View subview2 = new View () + { + Id = "subview2" + }; + + View subview3 = new View () + { + Id = "subview3" + }; + + superView.Add (subview1, subview2, subview3); + + superView.MoveSubviewTowardsStart (subview2); + Assert.Equal (subview2, superView.Subviews [0]); + + superView.MoveSubviewTowardsStart (subview3); + Assert.Equal (subview3, superView.Subviews [1]); + + // Already at front, what happens? + superView.MoveSubviewTowardsStart (subview2); + Assert.Equal (subview2, superView.Subviews [0]); + } + + [Fact] + public void MoveSubviewToEnd () + { + View superView = new (); + + View subview1 = new View () + { + Id = "subview1" + }; + + View subview2 = new View () + { + Id = "subview2" + }; + + View subview3 = new View () + { + Id = "subview3" + }; + + superView.Add (subview1, subview2, subview3); + + superView.MoveSubviewToEnd (subview1); + Assert.Equal (subview1, superView.Subviews [^1]); + + superView.MoveSubviewToEnd (subview2); + Assert.Equal (subview2, superView.Subviews [^1]); + } + + + [Fact] + public void MoveSubviewTowardsEnd () + { + View superView = new (); + + View subview1 = new View () + { + Id = "subview1" + }; + + View subview2 = new View () + { + Id = "subview2" + }; + + View subview3 = new View () + { + Id = "subview3" + }; + + superView.Add (subview1, subview2, subview3); + + superView.MoveSubviewTowardsEnd (subview2); + Assert.Equal (subview2, superView.Subviews [^1]); + + superView.MoveSubviewTowardsEnd (subview1); + Assert.Equal (subview1, superView.Subviews [1]); + + // Already at end, what happens? + superView.MoveSubviewTowardsEnd (subview2); + Assert.Equal (subview2, superView.Subviews [^1]); + } } diff --git a/UnitTests/View/ViewTests.cs b/UnitTests/View/ViewTests.cs index 80c858cd1d..e4f0616edc 100644 --- a/UnitTests/View/ViewTests.cs +++ b/UnitTests/View/ViewTests.cs @@ -206,7 +206,7 @@ Colors.ColorSchemes ["Base"].Focus { root.CanFocus = true; v.CanFocus = true; - Assert.False (v.HasFocus); + Assert.True (v.HasFocus); v.SetFocus (); Assert.True (v.HasFocus); Application.Refresh (); @@ -877,14 +877,6 @@ public void New_Methods_Return_False () Assert.False (r.NewMouseEnterEvent (new() { Flags = MouseFlags.AllEvents })); Assert.False (r.NewMouseLeaveEvent (new() { Flags = MouseFlags.AllEvents })); - var v1 = new View (); - Assert.False (r.OnEnter (v1)); - v1.Dispose (); - - var v2 = new View (); - Assert.False (r.OnLeave (v2)); - v2.Dispose (); - r.Dispose (); // TODO: Add more @@ -1072,7 +1064,6 @@ public void Visible_Sets_Also_Sets_Subviews () Assert.True (win.Visible); Assert.True (win.CanFocus); Assert.True (win.HasFocus); - Assert.True (RunesCount () > 0); win.Visible = false; Assert.True (button.Visible); @@ -1081,21 +1072,18 @@ public void Visible_Sets_Also_Sets_Subviews () Assert.False (win.Visible); Assert.True (win.CanFocus); Assert.False (win.HasFocus); + button.SetFocus (); Assert.False (button.HasFocus); Assert.False (win.HasFocus); + win.SetFocus (); Assert.False (button.HasFocus); Assert.False (win.HasFocus); - top.Draw (); - Assert.True (RunesCount () == 0); win.Visible = true; - win.FocusFirst (null); Assert.True (button.HasFocus); Assert.True (win.HasFocus); - top.Draw (); - Assert.True (RunesCount () > 0); Application.RequestStop (); }; @@ -1103,25 +1091,6 @@ public void Visible_Sets_Also_Sets_Subviews () Application.Run (top); top.Dispose (); Assert.Equal (1, iterations); - - int RunesCount () - { - Cell [,] contents = ((FakeDriver)Application.Driver).Contents; - var runesCount = 0; - - for (var i = 0; i < Application.Driver!.Rows; i++) - { - for (var j = 0; j < Application.Driver!.Cols; j++) - { - if (contents [i, j].Rune != (Rune)' ') - { - runesCount++; - } - } - } - - return runesCount; - } } public class DerivedView : View diff --git a/UnitTests/Views/ColorPickerTests.cs b/UnitTests/Views/ColorPickerTests.cs index e22cb3019e..a55c92b7c4 100644 --- a/UnitTests/Views/ColorPickerTests.cs +++ b/UnitTests/Views/ColorPickerTests.cs @@ -1,4 +1,5 @@ -using Xunit.Abstractions; +using System.Reflection.Emit; +using Xunit.Abstractions; using Color = Terminal.Gui.Color; namespace Terminal.Gui.ViewsTests; @@ -97,7 +98,7 @@ public void ColorPicker_RGB_KeyboardNavigation () [SetupFakeDriver] public void ColorPicker_RGB_MouseNavigation () { - var cp = GetColorPicker (ColorModel.RGB,false); + var cp = GetColorPicker (ColorModel.RGB, false); cp.Draw (); @@ -335,16 +336,11 @@ public void ColorPicker_ChangeValueOnUI_UpdatesAllUIElements () View otherView = new View () { CanFocus = true }; - Application.Current?.Add (otherView); - - cp.Draw (); + Application.Current?.Add (otherView); // thi sets focus to otherView + Assert.True (otherView.HasFocus); - // Change value using text field - TextField rBarTextField = cp.Subviews.OfType ().First (tf => tf.Text == "0"); - - rBarTextField.Text = "128"; - //rBarTextField.OnLeave (cp); // OnLeave should be protected virtual. Don't call it. - otherView.SetFocus (); // Remove focus from the color picker + cp.SetFocus (); + Assert.False (otherView.HasFocus); cp.Draw (); @@ -356,6 +352,27 @@ public void ColorPicker_ChangeValueOnUI_UpdatesAllUIElements () var gTextField = GetTextField (cp, ColorPickerPart.Bar2); var bTextField = GetTextField (cp, ColorPickerPart.Bar3); + Assert.Equal ("R:", r.Text); + Assert.Equal (2, r.TrianglePosition); + Assert.Equal ("0", rTextField.Text); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("0", gTextField.Text); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("0", bTextField.Text); + Assert.Equal ("#000000", hex.Text); + // Change value using text field + TextField rBarTextField = cp.Subviews.OfType ().First (tf => tf.Text == "0"); + + rBarTextField.SetFocus (); + rBarTextField.Text = "128"; + + otherView.SetFocus (); + Assert.True (otherView.HasFocus); + + cp.Draw (); + Assert.Equal ("R:", r.Text); Assert.Equal (9, r.TrianglePosition); Assert.Equal ("128", rTextField.Text); @@ -380,16 +397,23 @@ public void ColorPicker_InvalidHexInput_DoesNotChangeColor () // Enter invalid hex value TextField hexField = cp.Subviews.OfType ().First (tf => tf.Text == "#000000"); + hexField.SetFocus (); hexField.Text = "#ZZZZZZ"; - hexField.OnLeave (cp); - - cp.Draw (); + Assert.True (hexField.HasFocus); + Assert.Equal ("#ZZZZZZ", hexField.Text); var r = GetColorBar (cp, ColorPickerPart.Bar1); var g = GetColorBar (cp, ColorPickerPart.Bar2); var b = GetColorBar (cp, ColorPickerPart.Bar3); var hex = GetTextField (cp, ColorPickerPart.Hex); + Assert.Equal ("#ZZZZZZ", hex.Text); + + // Advance away from hexField to cause validation + cp.AdvanceFocus (NavigationDirection.Forward, null); + + cp.Draw (); + Assert.Equal ("R:", r.Text); Assert.Equal (2, r.TrianglePosition); Assert.Equal ("G:", g.Text); @@ -410,28 +434,38 @@ public void ColorPicker_ClickingDifferentBars_ChangesFocus () cp.Draw (); // Click on Green bar - cp.Subviews.OfType () - .Single () - .OnMouseEvent ( - new () - { - Flags = MouseFlags.Button1Pressed, - Position = new (0, 1) - }); + Application.OnMouseEvent (new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (0, 1) + }); + //cp.Subviews.OfType () + // .Single () + // .OnMouseEvent ( + // new () + // { + // Flags = MouseFlags.Button1Pressed, + // Position = new (0, 1) + // }); cp.Draw (); Assert.IsAssignableFrom (cp.Focused); // Click on Blue bar - cp.Subviews.OfType () - .Single () - .OnMouseEvent ( - new () - { - Flags = MouseFlags.Button1Pressed, - Position = new (0, 2) - }); + Application.OnMouseEvent (new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (0, 2) + }); + //cp.Subviews.OfType () + // .Single () + // .OnMouseEvent ( + // new () + // { + // Flags = MouseFlags.Button1Pressed, + // Position = new (0, 2) + // }); cp.Draw (); @@ -445,6 +479,8 @@ public void ColorPicker_ClickingDifferentBars_ChangesFocus () public void ColorPicker_SwitchingColorModels_ResetsBars () { var cp = GetColorPicker (ColorModel.RGB, false); + cp.BeginInit (); + cp.EndInit (); cp.SelectedColor = new (255, 0); cp.Draw (); @@ -550,7 +586,7 @@ private TextField GetTextField (ColorPicker cp, ColorPickerPart toGet) throw new NotSupportedException ("Corresponding Style option is not enabled"); } - return cp.Subviews.OfType ().ElementAt (hasBarValueTextFields ? (int)toGet : (int)toGet -3); + return cp.Subviews.OfType ().ElementAt (hasBarValueTextFields ? (int)toGet : (int)toGet - 3); case ColorPickerPart.Hex: int offset = hasBarValueTextFields ? 0 : 3; @@ -608,7 +644,7 @@ public void ColorPicker_ChangedEvent_Fires () [SetupFakeDriver] public void ColorPicker_DisposesOldViews_OnModelChange () { - var cp = GetColorPicker (ColorModel.HSL,true); + var cp = GetColorPicker (ColorModel.HSL, true); var b1 = GetColorBar (cp, ColorPickerPart.Bar1); var b2 = GetColorBar (cp, ColorPickerPart.Bar2); @@ -621,7 +657,7 @@ public void ColorPicker_DisposesOldViews_OnModelChange () var hex = GetTextField (cp, ColorPickerPart.Hex); #if DEBUG_IDISPOSABLE - Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3,hex }, b => Assert.False (b.WasDisposed)); + Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3, hex }, b => Assert.False (b.WasDisposed)); #endif cp.Style.ColorModel = ColorModel.RGB; cp.ApplyStyleChanges (); @@ -638,11 +674,11 @@ public void ColorPicker_DisposesOldViews_OnModelChange () // Old bars should be disposed #if DEBUG_IDISPOSABLE - Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3,hex }, b => Assert.True (b.WasDisposed)); + Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3, hex }, b => Assert.True (b.WasDisposed)); #endif - Assert.NotSame (hex,hexAfter); + Assert.NotSame (hex, hexAfter); - Assert.NotSame (b1,b1After); + Assert.NotSame (b1, b1After); Assert.NotSame (b2, b2After); Assert.NotSame (b3, b3After); @@ -654,9 +690,13 @@ public void ColorPicker_DisposesOldViews_OnModelChange () [Fact] [SetupFakeDriver] - public void ColorPicker_TabCompleteColorName() + public void ColorPicker_TabCompleteColorName () { - var cp = GetColorPicker (ColorModel.RGB, true,true); + var cp = GetColorPicker (ColorModel.RGB, true, true); + Application.Navigation = new (); + Application.Current = new (); + Application.Current.Add (cp); + cp.Draw (); var r = GetColorBar (cp, ColorPickerPart.Bar1); @@ -665,7 +705,7 @@ public void ColorPicker_TabCompleteColorName() var name = GetTextField (cp, ColorPickerPart.ColorName); var hex = GetTextField (cp, ColorPickerPart.Hex); - name.FocusFirst (TabBehavior.TabStop); + name.SetFocus (); Assert.True (name.HasFocus); Assert.Same (name, cp.Focused); @@ -676,7 +716,7 @@ public void ColorPicker_TabCompleteColorName() Application.OnKeyDown (Key.A); Application.OnKeyDown (Key.Q); - Assert.Equal ("aq",name.Text); + Assert.Equal ("aq", name.Text); // Auto complete the color name @@ -693,6 +733,7 @@ public void ColorPicker_TabCompleteColorName() Assert.Equal ("#7FFFD4", hex.Text); Application.Current?.Dispose (); + Application.ResetState (); } [Fact] @@ -700,12 +741,16 @@ public void ColorPicker_TabCompleteColorName() public void ColorPicker_EnterHexFor_ColorName () { var cp = GetColorPicker (ColorModel.RGB, true, true); + Application.Navigation = new (); + Application.Current = new (); + Application.Current.Add (cp); + cp.Draw (); var name = GetTextField (cp, ColorPickerPart.ColorName); var hex = GetTextField (cp, ColorPickerPart.Hex); - hex.FocusFirst (TabBehavior.TabStop); + hex.SetFocus (); Assert.True (hex.HasFocus); Assert.Same (hex, cp.Focused); @@ -730,16 +775,19 @@ public void ColorPicker_EnterHexFor_ColorName () Application.OnKeyDown ('4'); - // Tab out of the text field + Assert.True (hex.HasFocus); + + // Tab out of the hex field - should wrap to first focusable subview Application.OnKeyDown (Key.Tab); Assert.False (hex.HasFocus); Assert.NotSame (hex, cp.Focused); // Color name should be recognised as a known string and populated Assert.Equal ("#7FFFD4", hex.Text); - Assert.Equal("Aquamarine", name.Text); + Assert.Equal ("Aquamarine", name.Text); Application.Current?.Dispose (); + Application.ResetState (); } [Fact] @@ -747,7 +795,7 @@ public void TestColorNames () { var colors = new W3CColors (); Assert.Contains ("Aquamarine", colors.GetColorNames ()); - Assert.DoesNotContain ("Save as",colors.GetColorNames ()); + Assert.DoesNotContain ("Save as", colors.GetColorNames ()); } private ColorPicker GetColorPicker (ColorModel colorModel, bool showTextFields, bool showName = false) { @@ -757,13 +805,12 @@ private ColorPicker GetColorPicker (ColorModel colorModel, bool showTextFields, cp.Style.ShowColorName = showName; cp.ApplyStyleChanges (); - Application.Current = new Toplevel () { Width = 20 ,Height = 5}; + Application.Current = new Toplevel () { Width = 20, Height = 5 }; Application.Current.Add (cp); - Application.Current.FocusFirst (null); Application.Current.LayoutSubviews (); + Application.Current.SetFocus (); - Application.Current.FocusFirst (null); return cp; } } diff --git a/UnitTests/Views/ComboBoxTests.cs b/UnitTests/Views/ComboBoxTests.cs index 994a5cf659..366c9d42a9 100644 --- a/UnitTests/Views/ComboBoxTests.cs +++ b/UnitTests/Views/ComboBoxTests.cs @@ -500,9 +500,14 @@ public void HideDropdownListOnClick_True_Highlight_Current_Item () cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); var top = new Toplevel (); - top.Add (cb); + + View otherView = new View () { CanFocus = true }; + + top.Add (otherView, cb); Application.Begin (top); + Assert.True (cb.HasFocus); + Assert.True (cb.HideDropdownListOnClick); Assert.False (cb.IsShow); Assert.Equal (-1, cb.SelectedItem); @@ -806,7 +811,7 @@ public void HideDropdownListOnClick_True_OpenSelectedItem_With_Mouse_And_Key_F4 top.Dispose (); } - [Fact (Skip = "BUGBUG: New focus stuff broke. Fix later.")] + [Fact] [AutoInitShutdown] public void KeyBindings_Command () { @@ -819,7 +824,6 @@ public void KeyBindings_Command () var otherView = new View () { CanFocus = true }; top.Add (otherView); - // top.FocusFirst (null); Application.Begin (top); Assert.True (cb.HasFocus); @@ -839,6 +843,7 @@ public void KeyBindings_Command () Assert.False (Application.OnKeyDown (Key.Enter)); Assert.True (Application.OnKeyDown (Key.F4)); // with no source also expand empty Assert.True (cb.IsShow); + Assert.Equal (-1, cb.SelectedItem); cb.SetSource (source); cb.Text = ""; @@ -852,10 +857,11 @@ public void KeyBindings_Command () Assert.True (Application.OnKeyDown (Key.CursorDown)); // losing focus Assert.False (cb.IsShow); Assert.False (cb.HasFocus); - top.FocusFirst (null); // Gets focus again + cb.SetFocus (); Assert.False (cb.IsShow); Assert.True (cb.HasFocus); cb.Expand (); + Assert.True (cb.IsShow); Assert.Equal (0, cb.SelectedItem); Assert.Equal ("One", cb.Text); @@ -884,6 +890,7 @@ public void KeyBindings_Command () Assert.Equal (0, cb.SelectedItem); Assert.Equal ("One", cb.Text); + cb.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @" One ▼ @@ -896,8 +903,9 @@ public void KeyBindings_Command () Assert.True (cb.IsShow); Assert.Equal (1, cb.SelectedItem); Assert.Equal ("Two", cb.Text); - Application.Begin (top); +// Application.Begin (top); + cb.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @" Two ▼ @@ -910,8 +918,9 @@ public void KeyBindings_Command () Assert.True (cb.IsShow); Assert.Equal (2, cb.SelectedItem); Assert.Equal ("Three", cb.Text); - Application.Begin (top); + //Application.Begin (top); + cb.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @" Three ▼ @@ -960,7 +969,8 @@ public void KeyBindings_Command () Assert.False (cb.IsShow); Assert.Equal (-1, cb.SelectedItem); Assert.Equal ("One", cb.Text); - top.FocusFirst (null); // Gets focus again + + cb.SetFocus (); Assert.True (cb.HasFocus); Assert.False (cb.IsShow); Assert.Equal (-1, cb.SelectedItem); @@ -974,13 +984,16 @@ public void KeyBindings_Command () top.Dispose (); } - [Fact (Skip = "BUGBUG: New focus stuff broke. Fix later.")] + [Fact] public void Source_Equal_Null_Or_Count_Equal_Zero_Sets_SelectedItem_Equal_To_Minus_One () { + Application.Navigation = new (); var cb = new ComboBox (); var top = new Toplevel (); + Application.Current = top; + top.Add (cb); - top.FocusFirst (null); + top.FocusDeepest (NavigationDirection.Forward, null); Assert.Null (cb.Source); Assert.Equal (-1, cb.SelectedItem); ObservableCollection source = []; @@ -1015,5 +1028,6 @@ public void Source_Equal_Null_Or_Count_Equal_Zero_Sets_SelectedItem_Equal_To_Min Assert.Equal (0, cb.Source.Count); Assert.Equal (-1, cb.SelectedItem); Assert.Equal ("", cb.Text); + Application.ResetState (); } } diff --git a/UnitTests/Views/ContextMenuTests.cs b/UnitTests/Views/ContextMenuTests.cs index 57dcfebe36..143e25c8a2 100644 --- a/UnitTests/Views/ContextMenuTests.cs +++ b/UnitTests/Views/ContextMenuTests.cs @@ -1189,7 +1189,13 @@ public void UseSubMenusSingleFrame_True_By_Mouse () }; Toplevel top = new (); RunState rs = Application.Begin (top); + top.SetFocus (); + Assert.NotNull (Application.Current); + cm.Show (); + Assert.True(ContextMenu.IsShow); + Assert.True (Application.Top.Subviews [0].HasFocus); + Assert.Equal(Application.Top.Subviews [0], Application.Navigation.GetFocused()); Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top.Subviews [0].Frame); Application.Refresh (); @@ -1373,14 +1379,14 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () Assert.True (tf1.HasFocus); Assert.False (tf2.HasFocus); - Assert.Equal (2, win.Subviews.Count); + Assert.Equal (4, win.Subviews.Count); // TF & TV add autocomplete popup's to their superviews. Assert.Null (Application.MouseEnteredView); // Right click on tf2 to open context menu Application.OnMouseEvent (new () { Position = new (1, 3), Flags = MouseFlags.Button3Clicked }); Assert.False (tf1.HasFocus); Assert.False (tf2.HasFocus); - Assert.Equal (3, win.Subviews.Count); + Assert.Equal (5, win.Subviews.Count); Assert.True (tf2.ContextMenu.MenuBar.IsMenuOpen); Assert.True (win.Focused is Menu); Assert.True (Application.MouseGrabView is MenuBar); @@ -1390,7 +1396,7 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () Application.OnMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked }); Assert.True (tf1.HasFocus); Assert.False (tf2.HasFocus); - Assert.Equal (2, win.Subviews.Count); + Assert.Equal (4, win.Subviews.Count); // The last context menu bar opened is always preserved Assert.NotNull (tf2.ContextMenu.MenuBar); Assert.Equal (win.Focused, tf1); @@ -1401,7 +1407,7 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () Application.OnMouseEvent (new () { Position = new (1, 3), Flags = MouseFlags.Button1Clicked }); Assert.False (tf1.HasFocus); Assert.True (tf2.HasFocus); - Assert.Equal (2, win.Subviews.Count); + Assert.Equal (4, win.Subviews.Count); // The last context menu bar opened is always preserved Assert.NotNull (tf2.ContextMenu.MenuBar); Assert.Equal (win.Focused, tf2); diff --git a/UnitTests/Views/DatePickerTests.cs b/UnitTests/Views/DatePickerTests.cs index 60cf574b61..7fa24aae3f 100644 --- a/UnitTests/Views/DatePickerTests.cs +++ b/UnitTests/Views/DatePickerTests.cs @@ -23,7 +23,7 @@ public void DatePicker_ChangingCultureChangesFormat () } [Fact] - public void DatePicker_Initialize_ShouldSetCurrentDate () + public void DatePicker_Default_Constructor_ShouldSetCurrenDate () { var datePicker = new DatePicker (); Assert.Equal (DateTime.Now.Date.Day, datePicker.Date.Day); @@ -31,6 +31,24 @@ public void DatePicker_Initialize_ShouldSetCurrentDate () Assert.Equal (DateTime.Now.Date.Year, datePicker.Date.Year); } + [Fact] + public void DatePicker_Constrctor_Now_ShouldSetCurrenDate () + { + var datePicker = new DatePicker (DateTime.Now); + Assert.Equal (DateTime.Now.Date.Day, datePicker.Date.Day); + Assert.Equal (DateTime.Now.Date.Month, datePicker.Date.Month); + Assert.Equal (DateTime.Now.Date.Year, datePicker.Date.Year); + } + + [Fact] + public void DatePicker_X_Y_Init () + { + var datePicker = new DatePicker { Y = Pos.Center (), X = Pos.Center () }; + Assert.Equal (DateTime.Now.Date.Day, datePicker.Date.Day); + Assert.Equal (DateTime.Now.Date.Month, datePicker.Date.Month); + Assert.Equal (DateTime.Now.Date.Year, datePicker.Date.Year); + } + [Fact] public void DatePicker_SetDate_ShouldChangeText () { @@ -59,12 +77,12 @@ public void DatePicker_ShouldNot_SetDateOutOfRange_UsingNextMonthButton () datePicker.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); // Change month to December - Assert.True (datePicker.NewKeyDownEvent (Key.Enter)); + Assert.True (Application.OnKeyDown (Key.Enter)); Assert.Equal (12, datePicker.Date.Month); - // Date should not change as next month button is disabled - Assert.False (datePicker.NewKeyDownEvent (Key.Enter)); - Assert.Equal (12, datePicker.Date.Month); + // Date should change as next month button was disabled, causing focus to advance + Assert.True (Application.OnKeyDown (Key.Enter)); + Assert.Equal (11, datePicker.Date.Month); top.Dispose (); } @@ -88,8 +106,8 @@ public void DatePicker_ShouldNot_SetDateOutOfRange_UsingPreviousMonthButton () Assert.True (datePicker.NewKeyDownEvent (Key.Enter)); Assert.Equal (1, datePicker.Date.Month); - // Date should not change as previous month button is disabled - Assert.False (datePicker.NewKeyDownEvent (Key.Enter)); + // Previous month button is disabled, so focus advanced to edit field + Assert.True (datePicker.NewKeyDownEvent (Key.Enter)); Assert.Equal (1, datePicker.Date.Month); top.Dispose (); } diff --git a/UnitTests/Views/LabelTests.cs b/UnitTests/Views/LabelTests.cs index f339be41c9..0ccf17d3a9 100644 --- a/UnitTests/Views/LabelTests.cs +++ b/UnitTests/Views/LabelTests.cs @@ -63,7 +63,7 @@ public void MouseClick_SetsFocus_OnNextSubview () Assert.False (label.HasFocus); Assert.False (nextSubview.HasFocus); - label.NewMouseEvent (new() { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); + label.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); Assert.False (label.HasFocus); Assert.True (nextSubview.HasFocus); } @@ -164,7 +164,7 @@ public void Set_Text_With_Center () TestHelpers.AssertDriverContentsWithFrameAre (expected, output); top.Dispose (); } - + [Fact] public void Constructors_Defaults () { @@ -1317,33 +1317,32 @@ public void Label_ResizeView_With_Dim_Absolute () } [Fact] - [AutoInitShutdown] - public void Label_CanFocus_True_Get_Focus_By_Keyboard_And_Mouse () + public void Label_CanFocus_True_Get_Focus_By_Keyboard () { Label label = new () { Text = "label" }; - View view = new () { Y = 2, Width = 10, Height = 1, Text = "view", CanFocus = true }; - Toplevel top = new (); - top.Add (label, view); - Application.Begin (top); + View view = new () { Text = "view", CanFocus = true }; + Application.Navigation = new (); + Application.Current = new (); + Application.Current.Add (label, view); - Assert.Equal (new (0, 0, 5, 1), label.Frame); - Assert.Equal (new (0, 2, 10, 1), view.Frame); - Assert.Equal (view, top.MostFocused); + Application.Current.SetFocus (); + Assert.Equal (view, Application.Current.MostFocused); Assert.False (label.CanFocus); Assert.False (label.HasFocus); Assert.True (view.CanFocus); Assert.True (view.HasFocus); - Assert.True (Application.OnKeyDown (Key.Tab)); + // No focused view accepts Tab, and there's no other view to focus, so OnKeyDown returns false + Assert.False (Application.OnKeyDown (Key.Tab)); Assert.False (label.HasFocus); Assert.True (view.HasFocus); - Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); + // Set label CanFocus to true + label.CanFocus = true; Assert.False (label.HasFocus); Assert.True (view.HasFocus); - // Set label CanFocus to true - label.CanFocus = true; + // No focused view accepts Tab, but label can now be focused, so focus should move to it. Assert.True (Application.OnKeyDown (Key.Tab)); Assert.True (label.HasFocus); Assert.False (view.HasFocus); @@ -1352,12 +1351,64 @@ public void Label_CanFocus_True_Get_Focus_By_Keyboard_And_Mouse () Assert.False (label.HasFocus); Assert.True (view.HasFocus); + Application.Current.Dispose (); + Application.ResetState (); + } + + + [Fact] + public void Label_CanFocus_True_Get_Focus_By_Mouse () + { + Label label = new () + { + Text = "label", + X = 0, + Y = 0 + }; + View view = new () + { + Text = "view", + X = 0, + Y = 1, + Width = 4, + Height = 1, + CanFocus = true + }; + Application.Current = new () + { + Width = 10, + Height = 10 + }; + Application.Current.Add (label, view); + + Application.Current.SetFocus (); + Assert.Equal (view, Application.Current.MostFocused); + Assert.False (label.CanFocus); + Assert.False (label.HasFocus); + Assert.True (view.CanFocus); + Assert.True (view.HasFocus); + + // label can't focus so clicking on it has no effect + Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); + Assert.False (label.HasFocus); + Assert.True (view.HasFocus); + + // Set label CanFocus to true + label.CanFocus = true; + Assert.False (label.HasFocus); + Assert.True (view.HasFocus); + + // label can focus, so clicking on it set focus Application.OnMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked }); Assert.True (label.HasFocus); Assert.False (view.HasFocus); - Application.OnMouseEvent (new () { Position = new (0, 2), Flags = MouseFlags.Button1Clicked }); + // click on view + Application.OnMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked }); Assert.False (label.HasFocus); Assert.True (view.HasFocus); + + Application.Current.Dispose (); + Application.ResetState (); } } diff --git a/UnitTests/Views/ListViewTests.cs b/UnitTests/Views/ListViewTests.cs index 6d2e5decc6..399dbf9c2b 100644 --- a/UnitTests/Views/ListViewTests.cs +++ b/UnitTests/Views/ListViewTests.cs @@ -612,7 +612,7 @@ public void OnEnter_Does_Not_Throw_Exception () var lv = new ListView (); var top = new View (); top.Add (lv); - Exception exception = Record.Exception (lv.SetFocus); + Exception exception = Record.Exception (() => lv.SetFocus()); Assert.Null (exception); } diff --git a/UnitTests/Views/MenuBarTests.cs b/UnitTests/Views/MenuBarTests.cs index 9b10672901..e069a652bb 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -1,10 +1,54 @@ -using System.Text; -using Xunit.Abstractions; +using Xunit.Abstractions; namespace Terminal.Gui.ViewsTests; public class MenuBarTests (ITestOutputHelper output) { + [Fact] + [AutoInitShutdown] + public void AddMenuBarItem_RemoveMenuItem_Dynamically () + { + var menuBar = new MenuBar (); + var menuBarItem = new MenuBarItem { Title = "_New" }; + var action = ""; + var menuItem = new MenuItem { Title = "_Item", Action = () => action = "I", Parent = menuBarItem }; + Assert.Equal ("n", menuBarItem.HotKey); + Assert.Equal ("i", menuItem.HotKey); + Assert.Empty (menuBar.Menus); + menuBarItem.AddMenuBarItem (menuItem); + 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); + + var top = new Toplevel (); + top.Add (menuBar); + Application.Begin (top); + + top.NewKeyDownEvent (Key.N.WithAlt); + Application.MainLoop.RunIteration (); + Assert.True (menuBar.IsMenuOpen); + Assert.Equal ("", action); + + top.NewKeyDownEvent (Key.I); + Application.MainLoop.RunIteration (); + Assert.False (menuBar.IsMenuOpen); + Assert.Equal ("I", action); + + 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); + + menuBarItem.RemoveMenuItem (); + Assert.Empty (menuBar.Menus); + Assert.DoesNotContain (Key.N.WithAlt, menuBar.KeyBindings.Bindings); + + top.Dispose (); + } + [Fact] [AutoInitShutdown] public void AllowNullChecked_Get_Set () @@ -155,6 +199,30 @@ public void CanExecute_HotKey () top.Dispose (); } + [Fact] + [AutoInitShutdown] + public void Click_Another_View_Close_An_Open_Menu () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ] + }; + + var btnClicked = false; + var btn = new Button { Y = 4, Text = "Test" }; + btn.Accept += (s, e) => btnClicked = true; + var top = new Toplevel (); + top.Add (menu, btn); + Application.Begin (top); + + Application.OnMouseEvent (new () { Position = new (0, 4), Flags = MouseFlags.Button1Clicked }); + Assert.True (btnClicked); + top.Dispose (); + } + // TODO: Lots of tests in here really test Menu and MenuItem - Move them to MenuTests.cs [Fact] @@ -1319,6 +1387,7 @@ bool FnAction (string s) return true; } + // Declare a variable for the function Func fnActionVariable = FnAction; @@ -2560,7 +2629,7 @@ expectedMenu.Menus [i].Children.Length > 0 top.Draw (); TestHelpers.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (Application.OnKeyDown (menu.Key)); Assert.False (menu.IsMenuOpen); Assert.True (tf.HasFocus); top.Draw (); @@ -2799,6 +2868,19 @@ public void Separator_Does_Not_Throws_Pressing_Menu_Hotkey () Assert.False (menu.NewKeyDownEvent (Key.Q.WithAlt)); } + [Fact] + public void SetMenus_With_Same_HotKey_Does_Not_Throws () + { + var mb = new MenuBar (); + + var i1 = new MenuBarItem ("_heey", "fff", () => { }, () => true); + + mb.Menus = new [] { i1 }; + mb.Menus = new [] { i1 }; + + Assert.Equal (Key.H, mb.Menus [0].HotKey); + } + [Fact] [AutoInitShutdown] public void ShortCut_Activates () @@ -2837,6 +2919,31 @@ public void ShortCut_Activates () top.Dispose (); } + [Fact] + public void Update_ShortcutKey_KeyBindings_Old_ShortcutKey_Is_Removed () + { + var menuBar = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new ("New", "Create New", null, null, null, Key.A.WithCtrl) + } + ) + ] + }; + + Assert.Contains (Key.A.WithCtrl, menuBar.KeyBindings.Bindings); + + menuBar.Menus [0].Children [0].ShortcutKey = Key.B.WithCtrl; + + Assert.DoesNotContain (Key.A.WithCtrl, menuBar.KeyBindings.Bindings); + Assert.Contains (Key.B.WithCtrl, menuBar.KeyBindings.Bindings); + } + [Fact] public void UseKeysUpDownAsKeysLeftRight_And_UseSubMenusSingleFrame_Cannot_Be_Both_True () { @@ -3000,11 +3107,9 @@ public void UseSubMenusSingleFrame_False_By_Mouse () Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 8, 1), pos); - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ); top.Draw (); expected = @" @@ -3019,14 +3124,12 @@ public void UseSubMenusSingleFrame_False_By_Mouse () pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 10, 6), pos); - Assert.False ( - menu.NewMouseEvent ( - new () - { - Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Top.Subviews [1] - } - ) - ); + menu.NewMouseEvent ( + new () + { + Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Top.Subviews [1] + } + ); top.Draw (); expected = @" @@ -3064,11 +3167,9 @@ public void UseSubMenusSingleFrame_False_By_Mouse () pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 10, 6), pos); - Assert.False ( - menu.NewMouseEvent ( - new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } - ) - ); + menu.NewMouseEvent ( + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } + ); top.Draw (); expected = @" @@ -3345,7 +3446,7 @@ public void UseSubMenusSingleFrame_True_By_Mouse () pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 15, 7), pos); - Assert.False (menu.NewMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [2] })); + menu.NewMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [2] }); top.Draw (); expected = @" @@ -3514,11 +3615,9 @@ public void UseSubMenusSingleFrame_True_Without_Border () pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 8, 4), pos); - Assert.False ( - menu.NewMouseEvent ( - new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [1] } - ) - ); + menu.NewMouseEvent ( + new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [1] } + ); top.Draw (); expected = @" @@ -3532,11 +3631,9 @@ public void UseSubMenusSingleFrame_True_Without_Border () pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 13, 5), pos); - Assert.False ( - menu.NewMouseEvent ( - new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [2] } - ) - ); + menu.NewMouseEvent ( + new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.Subviews [2] } + ); top.Draw (); expected = @" @@ -3549,11 +3646,9 @@ public void UseSubMenusSingleFrame_True_Without_Border () pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); Assert.Equal (new (1, 0, 8, 4), pos); - Assert.False ( - menu.NewMouseEvent ( - new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } - ) - ); + menu.NewMouseEvent ( + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } + ); top.Draw (); expected = @" @@ -3611,23 +3706,6 @@ public class ExpectedMenuBar : MenuBar // The expected strings when the menu is closed public string ClosedMenuText => MenuBarText + "\n"; - // Each MenuBar title has a 1 space pad on each side - // See `static int leftPadding` and `static int rightPadding` on line 1037 of Menu.cs - public string MenuBarText - { - get - { - var txt = string.Empty; - - foreach (MenuBarItem m in Menus) - { - txt += " " + m.Title + " "; - } - - return txt; - } - } - public string ExpectedBottomRow (int i) { return $"{CM.Glyphs.LLCorner}{new (CM.Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{CM.Glyphs.LRCorner} \n"; @@ -3662,6 +3740,23 @@ public string ExpectedTopRow (int i) return $"{CM.Glyphs.ULCorner}{new (CM.Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{CM.Glyphs.URCorner} \n"; } + // Each MenuBar title has a 1 space pad on each side + // See `static int leftPadding` and `static int rightPadding` on line 1037 of Menu.cs + public string MenuBarText + { + get + { + var txt = string.Empty; + + foreach (MenuBarItem m in Menus) + { + txt += " " + m.Title + " "; + } + + return txt; + } + } + // Padding for the X of the sub menu Frame // Menu.cs - Line 1239 in `internal void OpenMenu` is where the Menu is created private string ExpectedPadding (int i) @@ -3703,111 +3798,4 @@ public CustomWindow () Add (menu); } } - - [Fact] - [AutoInitShutdown] - public void Click_Another_View_Close_An_Open_Menu () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - - var btnClicked = false; - var btn = new Button { Y = 4, Text = "Test" }; - btn.Accept += (s, e) => btnClicked = true; - var top = new Toplevel (); - top.Add (menu, btn); - Application.Begin (top); - - Application.OnMouseEvent (new () { Position = new (0, 4), Flags = MouseFlags.Button1Clicked }); - Assert.True (btnClicked); - top.Dispose (); - } - - [Fact] - public void Update_ShortcutKey_KeyBindings_Old_ShortcutKey_Is_Removed () - { - var menuBar = new MenuBar () - { - Menus = - [ - new MenuBarItem ( - "_File", - new MenuItem [] - { - new MenuItem ("New", "Create New", null, null, null, Key.A.WithCtrl) - } - ) - ] - }; - - Assert.Contains (Key.A.WithCtrl, menuBar.KeyBindings.Bindings); - - menuBar.Menus [0].Children [0].ShortcutKey = Key.B.WithCtrl; - - Assert.DoesNotContain (Key.A.WithCtrl, menuBar.KeyBindings.Bindings); - Assert.Contains (Key.B.WithCtrl, menuBar.KeyBindings.Bindings); - } - - [Fact] - public void SetMenus_With_Same_HotKey_Does_Not_Throws () - { - var mb = new MenuBar (); - - var i1 = new MenuBarItem ("_heey", "fff", () => { }, () => true); - - mb.Menus = new MenuBarItem [] { i1 }; - mb.Menus = new MenuBarItem [] { i1 }; - - Assert.Equal (Key.H, mb.Menus [0].HotKey); - } - - [Fact] - [AutoInitShutdown] - public void AddMenuBarItem_RemoveMenuItem_Dynamically () - { - var menuBar = new MenuBar (); - var menuBarItem = new MenuBarItem { Title = "_New" }; - var action = ""; - var menuItem = new MenuItem { Title = "_Item", Action = () => action = "I", Parent = menuBarItem }; - Assert.Equal ("n", menuBarItem.HotKey); - Assert.Equal ("i", menuItem.HotKey); - Assert.Empty (menuBar.Menus); - menuBarItem.AddMenuBarItem (menuItem); - 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); - - var top = new Toplevel (); - top.Add (menuBar); - Application.Begin (top); - - top.NewKeyDownEvent (Key.N.WithAlt); - Application.MainLoop.RunIteration (); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("", action); - - top.NewKeyDownEvent (Key.I); - Application.MainLoop.RunIteration (); - Assert.False (menuBar.IsMenuOpen); - Assert.Equal ("I", action); - - 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); - - menuBarItem.RemoveMenuItem (); - Assert.Empty (menuBar.Menus); - Assert.DoesNotContain (Key.N.WithAlt, menuBar.KeyBindings.Bindings); - - top.Dispose (); - } } diff --git a/UnitTests/Views/OverlappedTests.cs b/UnitTests/Views/OverlappedTests.cs index 0f4858ce14..5c42a5fe21 100644 --- a/UnitTests/Views/OverlappedTests.cs +++ b/UnitTests/Views/OverlappedTests.cs @@ -1,5 +1,6 @@ #nullable enable using System.Threading; +using JetBrains.Annotations; using Xunit.Abstractions; namespace Terminal.Gui.ViewsTests; @@ -1231,7 +1232,7 @@ public void SetFocusToNextViewWithWrap_ShouldFocusNextView () Assert.Equal (superView.MostFocused, current); // Act - ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView.TabIndexes, NavigationDirection.Forward); + ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current!.SuperView!.Subviews, NavigationDirection.Forward); // Assert Assert.True (view1.HasFocus); @@ -1277,16 +1278,9 @@ private class TestToplevel : Toplevel { public bool IsFocused { get; private set; } - public override bool OnEnter (View view) + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) { - IsFocused = true; - return base.OnEnter (view); - } - - public override bool OnLeave (View view) - { - IsFocused = false; - return base.OnLeave (view); + IsFocused = newHasFocus; } } @@ -1298,16 +1292,9 @@ public TestView () } public bool IsFocused { get; private set; } - public override bool OnEnter (View view) - { - IsFocused = true; - return base.OnEnter (view); - } - - public override bool OnLeave (View view) + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) { - IsFocused = false; - return base.OnLeave (view); + IsFocused = newHasFocus; } } } diff --git a/UnitTests/Views/RadioGroupTests.cs b/UnitTests/Views/RadioGroupTests.cs index 15286ad42d..2442eb98fb 100644 --- a/UnitTests/Views/RadioGroupTests.cs +++ b/UnitTests/Views/RadioGroupTests.cs @@ -78,18 +78,19 @@ public void KeyBindings_Are_Added_Correctly () [Fact] public void KeyBindings_Command () { + Application.Navigation = new (); var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test" } }; Application.Current = new Toplevel (); Application.Current.Add (rg); rg.SetFocus(); Assert.Equal(Orientation.Vertical, rg.Orientation); Assert.Equal(0, rg.SelectedItem); - Assert.True (Application.OnKeyDown (Key.CursorUp)); // Should not change (should focus prev if there was one) + Assert.False (Application.OnKeyDown (Key.CursorUp)); // Should not change (should focus prev view if there was one, which there isn't) Assert.Equal (0, rg.SelectedItem); Assert.True (Application.OnKeyDown (Key.CursorDown)); Assert.True (Application.OnKeyDown (Key.Space)); Assert.Equal (1, rg.SelectedItem); - Assert.True (Application.OnKeyDown (Key.CursorDown)); // Should not change (should focus prev if there was one) + Assert.False (Application.OnKeyDown (Key.CursorDown)); // Should not change (should focus prev view if there was one, which there isn't) Assert.True (Application.OnKeyDown (Key.Space)); Assert.Equal (1, rg.SelectedItem); Assert.True (Application.OnKeyDown (Key.Home)); @@ -100,7 +101,7 @@ public void KeyBindings_Command () Assert.Equal (1, rg.SelectedItem); Assert.True (Application.OnKeyDown (Key.Space)); Assert.Equal (1, rg.SelectedItem); - Application.Current.Dispose (); + Application.ResetState(); } [Fact] diff --git a/UnitTests/Views/ScrollBarViewTests.cs b/UnitTests/Views/ScrollBarViewTests.cs index 15c463d789..103ab4e3e3 100644 --- a/UnitTests/Views/ScrollBarViewTests.cs +++ b/UnitTests/Views/ScrollBarViewTests.cs @@ -294,7 +294,7 @@ public void ChangedPosition_Update_The_Hosted_View () [Fact] [AutoInitShutdown] - public void ClearOnVisibleFalse_Gets_Sets () + public void Visible_Gets_Sets () { var text = "This is a test\nThis is a test\nThis is a test\nThis is a test\nThis is a test\nThis is a test"; @@ -302,7 +302,7 @@ public void ClearOnVisibleFalse_Gets_Sets () var top = new Toplevel (); top.Add (label); - var sbv = new ScrollBarView (label, true, false) { Size = 100, ClearOnVisibleFalse = false }; + var sbv = new ScrollBarView (label, true, false) { Size = 100 }; Application.Begin (top); Assert.True (sbv.Visible); @@ -351,18 +351,18 @@ This is a tes▼ _output ); - sbv.ClearOnVisibleFalse = true; sbv.Visible = false; Assert.False (sbv.Visible); + top.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @" -This is a tes -This is a tes -This is a tes -This is a tes -This is a tes -This is a tes +This is a test +This is a test +This is a test +This is a test +This is a test +This is a test ", _output ); diff --git a/UnitTests/Views/ScrollViewTests.cs b/UnitTests/Views/ScrollViewTests.cs index c8472f45f5..f109eeb141 100644 --- a/UnitTests/Views/ScrollViewTests.cs +++ b/UnitTests/Views/ScrollViewTests.cs @@ -1,4 +1,5 @@ using System.Text; +using JetBrains.Annotations; using Xunit.Abstractions; namespace Terminal.Gui.ViewsTests; @@ -859,7 +860,7 @@ public void DrawTextFormatter_Respects_The_Clip_Bounds () "; TestHelpers.AssertDriverContentsAre (expected, output); - + top.Dispose (); } @@ -1119,28 +1120,20 @@ public CustomButton (string fill, string text, int width, int height) CanFocus = true; } - public override bool OnEnter (View view) - { - Border.LineStyle = LineStyle.None; - Border.Thickness = new (0); - labelFill.Visible = true; - view = this; - - return base.OnEnter (view); - } - - public override bool OnLeave (View view) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedVew) { - Border.LineStyle = LineStyle.Single; - Border.Thickness = new (1); - labelFill.Visible = false; - - if (view == null) + if (newHasFocus) { - view = this; + Border.LineStyle = LineStyle.None; + Border.Thickness = new (0); + labelFill.Visible = true; + } + else + { + Border.LineStyle = LineStyle.Single; + Border.Thickness = new (1); + labelFill.Visible = false; } - - return base.OnLeave (view); } } } diff --git a/UnitTests/Views/ShortcutTests.cs b/UnitTests/Views/ShortcutTests.cs index e2e9afbc79..1d6ac98ce2 100644 --- a/UnitTests/Views/ShortcutTests.cs +++ b/UnitTests/Views/ShortcutTests.cs @@ -211,7 +211,7 @@ public void KeyBindingScope_Changing_Adjusts_KeyBindings () Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys); Assert.DoesNotContain (Key.A, Application.KeyBindings.Bindings.Keys); } - + [Theory] [InlineData (Orientation.Horizontal)] [InlineData (Orientation.Vertical)] @@ -363,7 +363,7 @@ public void MouseClick_Fires_Accept (int x, int expectedAccept) shortcut.Accept += (s, e) => accepted++; Application.OnMouseEvent ( - new() + new () { Position = new (x, 0), Flags = MouseFlags.Button1Clicked @@ -418,7 +418,7 @@ public void MouseClick_Button_CommandView_Fires_Accept (int x, int expectedAccep //Assert.True (shortcut.HasFocus); Application.OnMouseEvent ( - new() + new () { Position = new (x, 0), Flags = MouseFlags.Button1Clicked @@ -601,7 +601,8 @@ public void ColorScheme_SetsAndGetsCorrectly () [Fact] public void ColorScheme_SetColorScheme_Does_Not_Fault_3664 () { - Application.Current = new Toplevel (); + Application.Current = new (); + Application.Navigation = new (); Shortcut shortcut = new Shortcut (); Application.Current.ColorScheme = null; @@ -613,6 +614,7 @@ public void ColorScheme_SetColorScheme_Does_Not_Fault_3664 () Assert.NotNull (shortcut.ColorScheme); Application.Current.Dispose (); + Application.ResetState (); } } diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index e5b3c5fd3a..f8f2554346 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -369,7 +369,7 @@ public void MouseClick_Right_Left_Arrows_ChangesTab_With_Border () top.Dispose (); } - [Fact] + [Fact (Skip="#2491 - A good test for Tab nav, but currently broken. TabView has exposes some interesting edge cases.")] [AutoInitShutdown] public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () { diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index 3f4a4bfed2..f5d03f7bcd 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -616,7 +616,7 @@ public void PageDown_ExcludesHeaders () top.Add (tableView); Application.Begin (top); - top.FocusFirst (null); + top.FocusDeepest (NavigationDirection.Forward, null); Assert.True (tableView.HasFocus); Assert.Equal (0, tableView.RowOffset); @@ -671,7 +671,7 @@ public void ScrollDown_OneLineAtATime () tableView.SelectedRow = 3; // row is 0 indexed so this is the 4th visible row // Scroll down - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorDown }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorDown }); // Scrolled off the page by 1 row so it should only have moved down 1 line of RowOffset Assert.Equal (4, tableView.SelectedRow); @@ -723,7 +723,7 @@ public void ScrollIndicators () TestHelpers.AssertDriverContentsAre (expected, output); // Scroll right - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); // since A is now pushed off screen we get indicator showing // that user can scroll left to see first column @@ -738,8 +738,8 @@ public void ScrollIndicators () TestHelpers.AssertDriverContentsAre (expected, output); // Scroll right twice more (to end of columns) - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); tableView.Draw (); @@ -798,7 +798,7 @@ public void ScrollRight_SmoothScrolling () TestHelpers.AssertDriverContentsAre (expected, output); // Scroll right - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); tableView.Draw (); @@ -858,7 +858,7 @@ public void ScrollRight_WithoutSmoothScrolling () TestHelpers.AssertDriverContentsAre (expected, output); // Scroll right - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); tableView.Draw (); @@ -1035,12 +1035,13 @@ public void TableView_Activate () } [Theory] - [SetupFakeDriver] + [AutoInitShutdown] [InlineData (false)] [InlineData (true)] public void TableView_ColorsTest_ColorGetter (bool focused) { TableView tv = SetUpMiniTable (out DataTable dt); + tv.LayoutSubviews (); // width exactly matches the max col widths @@ -1062,12 +1063,12 @@ public void TableView_ColorsTest_ColorGetter (bool focused) bStyle.ColorGetter = a => Convert.ToInt32 (a.CellValue) == 2 ? cellHighlight : null; - // private method for forcing the view to be focused/not focused - MethodInfo setFocusMethod = - typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); + var top = new Toplevel (); + top.Add (tv); + Application.Begin (top); - // when the view is/isn't focused - setFocusMethod.Invoke (tv, new object [] { focused, tv, true }); + tv.HasFocus = focused; + Assert.Equal(focused, tv.HasFocus); tv.Draw (); @@ -1126,10 +1127,12 @@ public void TableView_ColorsTest_ColorGetter (bool focused) tv.ColorScheme.Normal, focused ? tv.ColorScheme.Focus : tv.ColorScheme.HotNormal ); + + top.Dispose (); } [Theory] - [SetupFakeDriver] + [AutoInitShutdown] [InlineData (false)] [InlineData (true)] public void TableView_ColorsTest_RowColorGetter (bool focused) @@ -1152,14 +1155,13 @@ public void TableView_ColorsTest_RowColorGetter (bool focused) // when B is 2 use the custom highlight color for the row tv.Style.RowColorGetter += e => Convert.ToInt32 (e.Table [e.RowIndex, 1]) == 2 ? rowHighlight : null; + + var top = new Toplevel (); + top.Add (tv); + Application.Begin (top); - // private method for forcing the view to be focused/not focused - MethodInfo setFocusMethod = - typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); - - // when the view is/isn't focused - setFocusMethod.Invoke (tv, new object [] { focused, tv, true }); - + tv.HasFocus = focused; + Assert.Equal (focused, tv.HasFocus); tv.Draw (); var expected = @" @@ -1217,10 +1219,11 @@ public void TableView_ColorsTest_RowColorGetter (bool focused) tv.ColorScheme.Normal, focused ? tv.ColorScheme.Focus : tv.ColorScheme.HotNormal ); + top.Dispose (); } [Theory] - [SetupFakeDriver] + [AutoInitShutdown] [InlineData (false)] [InlineData (true)] public void TableView_ColorTests_FocusedOrNot (bool focused) @@ -1231,12 +1234,12 @@ public void TableView_ColorTests_FocusedOrNot (bool focused) // width exactly matches the max col widths tv.Viewport = new (0, 0, 5, 4); - // private method for forcing the view to be focused/not focused - MethodInfo setFocusMethod = - typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); + var top = new Toplevel (); + top.Add (tv); + Application.Begin (top); - // when the view is/isn't focused - setFocusMethod.Invoke (tv, new object [] { focused, tv, true }); + tv.HasFocus = focused; + Assert.Equal (focused, tv.HasFocus); tv.Draw (); @@ -1261,7 +1264,7 @@ public void TableView_ColorTests_FocusedOrNot (bool focused) tv.ColorScheme.Normal, focused ? tv.ColorScheme.Focus : tv.ColorScheme.HotNormal ); - + top.Dispose (); } [Theory] @@ -1277,12 +1280,8 @@ public void TableView_ColorTests_InvertSelectedCellFirstCharacter (bool focused) // width exactly matches the max col widths tv.Viewport = new (0, 0, 5, 4); - // private method for forcing the view to be focused/not focused - MethodInfo setFocusMethod = - typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); - - // when the view is/isn't focused - setFocusMethod.Invoke (tv, new object [] { focused, tv, true }); + tv.HasFocus = focused; + Assert.Equal (focused, tv.HasFocus); tv.Draw (); @@ -1569,7 +1568,7 @@ public void Test_CollectionNavigator () tv.Table = new EnumerableTableSource ( new [] { "fish", "troll", "trap", "zoo" }, - new() { { "Name", t => t }, { "EndsWith", t => t.Last () } } + new () { { "Name", t => t }, { "EndsWith", t => t.Last () } } ); tv.LayoutSubviews (); @@ -1594,11 +1593,11 @@ public void Test_CollectionNavigator () Assert.False (tv.HasFocus); // already on fish - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.F }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.F }); Assert.Equal (0, tv.SelectedRow); // not focused - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.Z }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.Z }); Assert.Equal (0, tv.SelectedRow); // ensure that TableView has the input focus @@ -1606,42 +1605,42 @@ public void Test_CollectionNavigator () top.Add (tv); Application.Begin (top); - top.FocusFirst (null); + top.FocusDeepest (NavigationDirection.Forward, null); Assert.True (tv.HasFocus); // already on fish - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.F }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.F }); Assert.Equal (0, tv.SelectedRow); // move to zoo - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.Z }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.Z }); Assert.Equal (3, tv.SelectedRow); // move to troll - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.T }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.T }); Assert.Equal (1, tv.SelectedRow); // move to trap - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.T }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.T }); Assert.Equal (2, tv.SelectedRow); // change columns to navigate by column 2 Assert.Equal (0, tv.SelectedColumn); Assert.Equal (2, tv.SelectedRow); - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); Assert.Equal (1, tv.SelectedColumn); Assert.Equal (2, tv.SelectedRow); // nothing ends with t so stay where you are - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.T }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.T }); Assert.Equal (2, tv.SelectedRow); //jump to fish which ends in h - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.H }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.H }); Assert.Equal (0, tv.SelectedRow); // jump to zoo which ends in o - tv.NewKeyDownEvent (new() { KeyCode = KeyCode.O }); + tv.NewKeyDownEvent (new () { KeyCode = KeyCode.O }); Assert.Equal (3, tv.SelectedRow); top.Dispose (); } @@ -1884,7 +1883,7 @@ public void TestColumnStyle_FirstColumnVisibleFalse_CursorStaysAt1 (bool useHome Assert.Equal (1, tableView.SelectedColumn); tableView.NewKeyDownEvent ( - new() { KeyCode = useHome ? KeyCode.Home : KeyCode.CursorLeft } + new () { KeyCode = useHome ? KeyCode.Home : KeyCode.CursorLeft } ); // Expect the cursor to stay at 1 @@ -1935,7 +1934,7 @@ public void TestColumnStyle_LastColumnVisibleFalse_CursorStaysAt2 (bool useEnd) Assert.Equal (2, tableView.SelectedColumn); tableView.NewKeyDownEvent ( - new() { KeyCode = useEnd ? KeyCode.End : KeyCode.CursorRight } + new () { KeyCode = useEnd ? KeyCode.End : KeyCode.CursorRight } ); // Expect the cursor to stay at 2 @@ -2032,12 +2031,12 @@ public void TestColumnStyle_VisibleFalse_CursorStepsOverInvisibleColumns () tableView.Style.GetOrCreateColumnStyle (1).Visible = false; tableView.SelectedColumn = 0; - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); // Expect the cursor navigation to skip over the invisible column(s) Assert.Equal (2, tableView.SelectedColumn); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorLeft }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorLeft }); // Expect the cursor navigation backwards to skip over invisible column too Assert.Equal (0, tableView.SelectedColumn); @@ -2170,7 +2169,7 @@ public void TestControlClick_MultiSelect_ThreeRowTable_FullRowSelect () // Clicking in bottom row tv.NewMouseEvent ( - new() { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } + new () { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } ); // should select that row @@ -2178,7 +2177,7 @@ public void TestControlClick_MultiSelect_ThreeRowTable_FullRowSelect () // shift clicking top row tv.NewMouseEvent ( - new() { Position = new (1, 2), Flags = MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl } + new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl } ); // should extend the selection @@ -2196,14 +2195,14 @@ public void TestControlClick_MultiSelect_ThreeRowTable_FullRowSelect () [SetupFakeDriver] public void TestEnumerableDataSource_BasicTypes () { - ((FakeDriver)Application.Driver!).SetBufferSize(100,100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); var tv = new TableView (); tv.ColorScheme = Colors.ColorSchemes ["TopLevel"]; tv.Viewport = new (0, 0, 50, 6); tv.Table = new EnumerableTableSource ( new [] { typeof (string), typeof (int), typeof (float) }, - new() + new () { { "Name", t => t.Name }, { "Namespace", t => t.Namespace }, { "BaseType", t => t.BaseType } @@ -2243,7 +2242,7 @@ public void TestFullRowSelect_AlwaysUseNormalColorForVerticalCellLines () // Clicking in bottom row tv.NewMouseEvent ( - new() { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } + new () { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } ); // should select that row @@ -2298,7 +2297,7 @@ public void TestFullRowSelect_SelectionColorDoesNotStop_WhenShowVerticalCellLine // Clicking in bottom row tv.NewMouseEvent ( - new() { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } + new () { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } ); // should select that row @@ -2351,7 +2350,7 @@ public void TestFullRowSelect_SelectionColorStopsAtTableEdge_WithCellLines () // Clicking in bottom row tv.NewMouseEvent ( - new() { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } + new () { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } ); // should select that row @@ -2407,7 +2406,7 @@ public void TestListTableSource (Orientation orient, bool parallel) tv.ColorScheme = Colors.ColorSchemes ["TopLevel"]; tv.Viewport = new (0, 0, 25, 4); - tv.Style = new() + tv.Style = new () { ShowHeaders = false, ShowHorizontalHeaderOverline = false, ShowHorizontalHeaderUnderline = false }; @@ -2532,7 +2531,7 @@ public void TestShiftClick_MultiSelect_TwoRowTable_FullRowSelect () // Clicking in bottom row tv.NewMouseEvent ( - new() { Position = new (1, 3), Flags = MouseFlags.Button1Clicked } + new () { Position = new (1, 3), Flags = MouseFlags.Button1Clicked } ); // should select that row @@ -2540,7 +2539,7 @@ public void TestShiftClick_MultiSelect_TwoRowTable_FullRowSelect () // shift clicking top row tv.NewMouseEvent ( - new() { Position = new (1, 2), Flags = MouseFlags.Button1Clicked | MouseFlags.ButtonShift } + new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked | MouseFlags.ButtonShift } ); // should extend the selection @@ -3048,14 +3047,14 @@ public void TestToggleCells_MultiSelectOn () Assert.Equal (1, s2.Y); // Go back to the toggled cell - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorUp }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorUp }); // Toggle off - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.Space }); // Go Left - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorLeft }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorLeft }); selectedCell = tableView.GetAllSelectedCells ().Single (); Assert.Equal (0, selectedCell.X); @@ -3074,10 +3073,10 @@ public void TestToggleCells_MultiSelectOn_FullRowSelect () tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); // Toggle Select Cell 0,0 - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.Space }); // Go Down - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorDown }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorDown }); TableSelection m = tableView.MultiSelectedRegions.Single (); Assert.True (m.IsToggled); @@ -3087,13 +3086,13 @@ public void TestToggleCells_MultiSelectOn_FullRowSelect () //First row toggled and Second row active = 12 selected cells Assert.Equal (12, tableView.GetAllSelectedCells ().Count ()); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorUp }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorUp }); Assert.Single (tableView.MultiSelectedRegions.Where (r => r.IsToggled)); // Can untoggle at 1,0 even though 0,0 was initial toggle because FullRowSelect is on - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.Space }); #pragma warning disable xUnit2029 Assert.Empty (tableView.MultiSelectedRegions.Where (r => r.IsToggled)); @@ -3112,16 +3111,16 @@ public void TestToggleCells_MultiSelectOn_SquareSelectToggled () tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); // Make a square selection - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.ShiftMask | KeyCode.CursorRight }); Assert.Equal (4, tableView.GetAllSelectedCells ().Count ()); // Toggle the square selected region on - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.Space }); // Go Right - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorRight }); //Toggled on square + the active cell (x=2,y=1) Assert.Equal (5, tableView.GetAllSelectedCells ().Count ()); @@ -3130,11 +3129,11 @@ public void TestToggleCells_MultiSelectOn_SquareSelectToggled () // Untoggle the rectangular region by hitting toggle in // any cell in that rect - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorUp }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorLeft }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorUp }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorLeft }); Assert.Equal (4, tableView.GetAllSelectedCells ().Count ()); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.Space }); Assert.Single (tableView.GetAllSelectedCells ()); } @@ -3153,17 +3152,17 @@ public void TestToggleCells_MultiSelectOn_Two_SquareSelects_BothToggled () tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); // Make first square selection (0,0 to 1,1) - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorRight }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.ShiftMask | KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.Space }); Assert.Equal (4, tableView.GetAllSelectedCells ().Count ()); // Make second square selection leaving 1 unselected line between them - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorLeft }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorDown }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorDown }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); - tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorRight }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorLeft }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorDown }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.CursorDown }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); + tableView.NewKeyDownEvent (new () { KeyCode = KeyCode.ShiftMask | KeyCode.CursorRight }); // 2 square selections Assert.Equal (8, tableView.GetAllSelectedCells ().Count ()); @@ -3236,7 +3235,7 @@ private TableView GetPetTable (out EnumerableTableSource source) tv.Table = source = new ( pets, - new() + new () { { "Name", p => p.Name }, { "Kind", p => p.Kind } } diff --git a/UnitTests/Views/TextFieldTests.cs b/UnitTests/Views/TextFieldTests.cs index 5f39fc4c83..a5c6fe4e1e 100644 --- a/UnitTests/Views/TextFieldTests.cs +++ b/UnitTests/Views/TextFieldTests.cs @@ -78,7 +78,7 @@ string GetContents () public void Cancel_TextChanging_ThenBackspace () { var tf = new TextField (); - tf.RestoreFocus (); + tf.SetFocus (); tf.NewKeyDownEvent (Key.A.WithShift); Assert.Equal ("A", tf.Text); @@ -126,7 +126,6 @@ public void CanFocus_False_Wont_Focus_With_Mouse () Assert.False (fv.CanFocus); Assert.False (fv.HasFocus); - Assert.Throws (() => tf.CanFocus = true); fv.CanFocus = true; tf.CanFocus = true; @@ -147,7 +146,7 @@ public void CanFocus_False_Wont_Focus_With_Mouse () ); Assert.Equal ("some ", tf.SelectedText); // Setting CanFocus to false don't change the SelectedText - Assert.False (tf.CanFocus); + Assert.True (tf.CanFocus); // v2: CanFocus is not longer automatically changed Assert.False (tf.HasFocus); Assert.False (fv.CanFocus); Assert.False (fv.HasFocus); @@ -909,7 +908,7 @@ public void OnEnter_Does_Not_Throw_If_Not_IsInitialized_SetCursorVisibility () var tf = new TextField { Width = 10 }; top.Add (tf); - Exception exception = Record.Exception (tf.SetFocus); + Exception exception = Record.Exception (() => tf.SetFocus ()); Assert.Null (exception); } @@ -929,7 +928,7 @@ public void Paste_Always_Clear_The_SelectedText () public void Backspace_From_End () { var tf = new TextField { Text = "ABC" }; - tf.RestoreFocus (); + tf.SetFocus (); Assert.Equal ("ABC", tf.Text); tf.BeginInit (); tf.EndInit (); @@ -956,7 +955,7 @@ public void Backspace_From_End () public void Backspace_From_Middle () { var tf = new TextField { Text = "ABC" }; - tf.RestoreFocus (); + tf.SetFocus (); tf.CursorPosition = 2; Assert.Equal ("ABC", tf.Text); @@ -1984,4 +1983,68 @@ public override void Before (MethodInfo methodUnderTest) }; } } + + [Fact] + public void Autocomplete_Popup_Added_To_SuperView_On_Init () + { + View superView = new () + { + CanFocus = true, + }; + + TextField t = new (); + + superView.Add (t); + Assert.Single (superView.Subviews); + + superView.BeginInit (); + superView.EndInit (); + + Assert.Equal (2, superView.Subviews.Count); + } + + + [Fact] + public void Autocomplete__Added_To_SuperView_On_Add () + { + View superView = new () + { + CanFocus = true, + Id = "superView", + }; + + superView.BeginInit (); + superView.EndInit (); + Assert.Empty (superView.Subviews); + + TextField t = new () + { + Id = "t" + }; + + superView.Add (t); + + Assert.Equal (2, superView.Subviews.Count); + } + + + [Fact] + public void Autocomplete_Visible_False_By_Default () + { + View superView = new () + { + CanFocus = true, + }; + + TextField t = new (); + + superView.Add (t); + superView.BeginInit (); + superView.EndInit (); + + Assert.Equal (2, superView.Subviews.Count); + + Assert.True (t.Visible); + Assert.False (t.Autocomplete.Visible); + } } diff --git a/UnitTests/Views/TextViewTests.cs b/UnitTests/Views/TextViewTests.cs index 8925db7afa..364fb228dd 100644 --- a/UnitTests/Views/TextViewTests.cs +++ b/UnitTests/Views/TextViewTests.cs @@ -137,7 +137,6 @@ public void CanFocus_False_Wont_Focus_With_Mouse () Assert.False (fv.CanFocus); Assert.False (fv.HasFocus); - Assert.Throws (() => tv.CanFocus = true); fv.CanFocus = true; tv.CanFocus = true; tv.NewMouseEvent (new MouseEvent { Position = new (1, 0), Flags = MouseFlags.Button1DoubleClicked }); @@ -152,7 +151,7 @@ public void CanFocus_False_Wont_Focus_With_Mouse () tv.NewMouseEvent (new MouseEvent { Position = new (1, 0), Flags = MouseFlags.Button1DoubleClicked }); Assert.Equal ("some ", tv.SelectedText); // Setting CanFocus to false don't change the SelectedText - Assert.False (tv.CanFocus); + Assert.True (tv.CanFocus); // v2: CanFocus is not longer automatically changed Assert.False (tv.HasFocus); Assert.False (fv.CanFocus); Assert.False (fv.HasFocus); @@ -8558,4 +8557,67 @@ void ButtonAccept (object sender, HandledEventArgs e) } } + [Fact] + public void Autocomplete_Popup_Added_To_SuperView_On_Init () + { + View superView = new () + { + CanFocus = true, + }; + + TextView t = new (); + + superView.Add (t); + Assert.Single (superView.Subviews); + + superView.BeginInit (); + superView.EndInit (); + + Assert.Equal (2, superView.Subviews.Count); + } + + + [Fact] + public void Autocomplete__Added_To_SuperView_On_Add () + { + View superView = new () + { + CanFocus = true, + Id = "superView", + }; + + superView.BeginInit (); + superView.EndInit (); + Assert.Empty (superView.Subviews); + + TextView t = new () + { + Id = "t" + }; + + superView.Add (t); + + Assert.Equal (2, superView.Subviews.Count); + } + + + [Fact] + public void Autocomplete_Visible_False_By_Default () + { + View superView = new () + { + CanFocus = true, + }; + + TextView t = new (); + + superView.Add (t); + superView.BeginInit (); + superView.EndInit (); + + Assert.Equal (2, superView.Subviews.Count); + + Assert.True (t.Visible); + Assert.False (t.Autocomplete.Visible); + } } diff --git a/UnitTests/Views/TileViewTests.cs b/UnitTests/Views/TileViewTests.cs index 686d807107..205409efb1 100644 --- a/UnitTests/Views/TileViewTests.cs +++ b/UnitTests/Views/TileViewTests.cs @@ -973,13 +973,13 @@ public void TestNestedContainer3RightAnd1Down_RendersNicely () Assert.Equal (6, subSplit.Tiles.ElementAt (0).ContentView.Frame.Width); Assert.Equal (0, subSplit.Tiles.ElementAt (0).ContentView.Frame.Y); Assert.Equal (5, subSplit.Tiles.ElementAt (0).ContentView.Frame.Height); - Assert.IsType (subSplit.Tiles.ElementAt (0).ContentView.Subviews.Single ()); + //Assert.IsType (subSplit.Tiles.ElementAt (0).ContentView.Subviews.Single ()); Assert.Equal (0, subSplit.Tiles.ElementAt (1).ContentView.Frame.X); Assert.Equal (6, subSplit.Tiles.ElementAt (1).ContentView.Frame.Width); Assert.Equal (6, subSplit.Tiles.ElementAt (1).ContentView.Frame.Y); Assert.Equal (4, subSplit.Tiles.ElementAt (1).ContentView.Frame.Height); - Assert.IsType (subSplit.Tiles.ElementAt (1).ContentView.Subviews.Single ()); + //Assert.IsType (subSplit.Tiles.ElementAt (1).ContentView.Subviews.Single ()); } [Fact] @@ -1524,13 +1524,13 @@ public void TestNestedContainer3RightAnd1Down_WithBorder_RendersNicely () Assert.Equal (5, subSplit.Tiles.ElementAt (0).ContentView.Frame.Width); Assert.Equal (0, subSplit.Tiles.ElementAt (0).ContentView.Frame.Y); Assert.Equal (4, subSplit.Tiles.ElementAt (0).ContentView.Frame.Height); - Assert.IsType (subSplit.Tiles.ElementAt (0).ContentView.Subviews.Single ()); + //Assert.IsType (subSplit.Tiles.ElementAt (0).ContentView.Subviews.Single ()); Assert.Equal (0, subSplit.Tiles.ElementAt (1).ContentView.Frame.X); Assert.Equal (5, subSplit.Tiles.ElementAt (1).ContentView.Frame.Width); Assert.Equal (5, subSplit.Tiles.ElementAt (1).ContentView.Frame.Y); Assert.Equal (3, subSplit.Tiles.ElementAt (1).ContentView.Frame.Height); - Assert.IsType (subSplit.Tiles.ElementAt (1).ContentView.Subviews.Single ()); + //Assert.IsType (subSplit.Tiles.ElementAt (1).ContentView.Subviews.Single ()); } [Fact] diff --git a/UnitTests/Views/ToplevelTests.cs b/UnitTests/Views/ToplevelTests.cs index 8b855799f7..ea6e656d8f 100644 --- a/UnitTests/Views/ToplevelTests.cs +++ b/UnitTests/Views/ToplevelTests.cs @@ -425,7 +425,7 @@ public void Internal_Tests () #endif } - [Fact] + [Fact (Skip = "#2491 - Test is broken until #2491 is more mature.")] [AutoInitShutdown] public void KeyBindings_Command () { @@ -544,7 +544,7 @@ public void KeyBindings_Command () Assert.Equal (tvW1, top.MostFocused); #if UNIX_KEY_BINDINGS Assert.True (Application.OnKeyDown (new (Key.I.WithCtrl))); - Assert.Equal (win1, top.Focused); + Assert.Equal (win1, top.GetFocused ()); Assert.Equal (tf2W1, top.MostFocused); #endif Assert.True (Application.OnKeyDown (Key.Tab.WithShift)); // Ignored. TextView eats shift-tab by default @@ -851,6 +851,7 @@ public void GetLocationThatFits_With_Border_Null_Not_Throws () Assert.Null (exception); } +#if V2_NEW_FOCUS_IMPL [Fact] [AutoInitShutdown] public void OnEnter_OnLeave_Triggered_On_Application_Begin_End () @@ -890,7 +891,6 @@ public void OnEnter_OnLeave_Triggered_On_Application_Begin_End () top.Dispose (); } - [Fact] [AutoInitShutdown] public void OnEnter_OnLeave_Triggered_On_Application_Begin_End_With_Modal () @@ -1072,13 +1072,14 @@ public void OnEnter_OnLeave_Triggered_On_Application_Begin_End_With_More_Topleve Assert.Equal (4, steps [^1]); top.Dispose (); } +#endif [Fact] [AutoInitShutdown] public void PositionCursor_SetCursorVisibility_To_Invisible_If_Focused_Is_Null () { var tf = new TextField { Width = 5, Text = "test" }; - var view = new View { Width = 10, Height = 10 }; + var view = new View { Width = 10, Height = 10, CanFocus = true }; view.Add (tf); var top = new Toplevel (); top.Add (view); diff --git a/UnitTests/Views/TreeTableSourceTests.cs b/UnitTests/Views/TreeTableSourceTests.cs index 99eaf5f289..5f17475cc5 100644 --- a/UnitTests/Views/TreeTableSourceTests.cs +++ b/UnitTests/Views/TreeTableSourceTests.cs @@ -289,7 +289,7 @@ private TableView GetTreeTable (out TreeView tree) var top = new Toplevel (); top.Add (tableView); - top.RestoreFocus (); + top.SetFocus (); Assert.Equal (tableView, top.MostFocused); return tableView; diff --git a/UnitTests/Views/TreeViewTests.cs b/UnitTests/Views/TreeViewTests.cs index a770d1d9d2..240c18a267 100644 --- a/UnitTests/Views/TreeViewTests.cs +++ b/UnitTests/Views/TreeViewTests.cs @@ -483,7 +483,11 @@ public void ObjectActivationButton_RightClick () public void ObjectActivationButton_SetToNull () { TreeView tree = CreateTree (out Factory f, out Car car1, out _); + Assert.Null (tree.SelectedObject); + Assert.True (tree.SetFocus ()); + tree.SelectedObject = null; + Assert.Null (tree.SelectedObject); // disable activation tree.ObjectActivationButton = null; @@ -500,6 +504,7 @@ public void ObjectActivationButton_SetToNull () Assert.False (called); + // double click does nothing because we changed button to null tree.NewMouseEvent (new MouseEvent { Flags = MouseFlags.Button1DoubleClicked }); @@ -1338,13 +1343,13 @@ public void HotKey_Command_Does_Not_Accept () var treeView = new TreeView (); var accepted = false; -treeView.Accept += OnAccept; -treeView.InvokeCommand (Command.HotKey); + treeView.Accept += OnAccept; + treeView.InvokeCommand (Command.HotKey); -Assert.False (accepted); + Assert.False (accepted); -return; -void OnAccept (object sender, HandledEventArgs e) { accepted = true; } + return; + void OnAccept (object sender, HandledEventArgs e) { accepted = true; } } diff --git a/UnitTests/Views/WindowTests.cs b/UnitTests/Views/WindowTests.cs index a010227f75..554593d230 100644 --- a/UnitTests/Views/WindowTests.cs +++ b/UnitTests/Views/WindowTests.cs @@ -201,24 +201,4 @@ public void New_Initializes () Assert.Null (windowWithFrame1234.MostFocused); Assert.Equal (TextDirection.LeftRight_TopBottom, windowWithFrame1234.TextDirection); } - - [Fact] - [AutoInitShutdown] - public void OnCanFocusChanged_Only_Must_ContentView_Forces_SetFocus_After_IsInitialized_Is_True () - { - var win1 = new Window { Id = "win1", Width = 10, Height = 1 }; - var view1 = new View { Id = "view1", Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true }; - var win2 = new Window { Id = "win2", Y = 6, Width = 10, Height = 1 }; - var view2 = new View { Id = "view2", Width = Dim.Fill (), Height = Dim.Fill (), CanFocus = true }; - win2.Add (view2); - win1.Add (view1, win2); - - Application.Begin (win1); - - Assert.True (win1.HasFocus); - Assert.True (view1.HasFocus); - Assert.False (win2.HasFocus); - Assert.False (view2.HasFocus); - win1.Dispose (); - } } diff --git a/docfx/docs/View.md b/docfx/docs/View.md index ca1c733a26..9677ad2ccc 100644 --- a/docfx/docs/View.md +++ b/docfx/docs/View.md @@ -132,7 +132,7 @@ This covers my thinking on how we will refactor `View` and the classes in the `V ## Design * `Responder`("Responder base class implemented by objects that want to participate on keyboard and mouse input.") remains mostly unchanged, with minor changes: - * Methods that take `View` parameters (e.g. `OnEnter`) change to take `Responder` (bad OO design). + * Methods that take `View` parameters change to take `Responder` (bad OO design). * Nuke `IsOverriden` (bad OO design) * Move `View.Data` to `Responder` (primitive) * Move `Command` and `KeyBinding` stuff from `View`. diff --git a/docfx/docs/cursor.md b/docfx/docs/cursor.md index 917f8015d1..7b6de830fc 100644 --- a/docfx/docs/cursor.md +++ b/docfx/docs/cursor.md @@ -10,6 +10,8 @@ See end for list of issues this design addresses. ## Lexicon & Taxonomy +- Navigation - Refers to the user-experience for moving Focus between views in the application view-hierarchy. See [Navigation](navigation.md) for a deep-dive. +- Focus - Indicates which View in the view-hierarchy is currently the one receiving keyboard input. Only one view-heirachy in an applicstion can have focus (`view.HasFocus == true`), and there is only one View in a focused heirarchy that is the most-focused; the one recieving keyboard input. See [Navigation](navigation.md) for a deep-dive. - Cursor - A visual indicator to the user where keyboard input will have an impact. There is one Cursor per terminal sesssion. - Cursor Location - The top-left corner of the Cursor. In text entry scenarios, new text will be inserted to the left/top of the Cursor Location. - Cursor Size - The width and height of the cursor. Currently the size is limited to 1x1. diff --git a/docfx/docs/index.md b/docfx/docs/index.md index 05e6dd9ea7..565dc54089 100644 --- a/docfx/docs/index.md +++ b/docfx/docs/index.md @@ -2,6 +2,8 @@ A toolkit for building rich Terminal User Interface (TUI) apps with .NET that run on Windows, the Mac, and Linux/Unix. + (This is the v2 API documentation. For v1 go here: https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.html) + ## Features * **[Cross Platform](drivers.md)** - Windows, Mac, and Linux. Terminal drivers for Curses, Windows, and the .NET Console mean apps will work well on both color and monochrome terminals. Apps also work over SSH. diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index cbe3f92f11..f1dc66e22c 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -12,6 +12,9 @@ Tenets higher in the list have precedence over tenets lower in the list. * **The Source of Truth is Wikipedia** - We use this [Wikipedia article](https://en.wikipedia.org/wiki/Table_of_keyboard_shortcuts) as our guide for default key bindings. +* **If It's Hot, It Works** - If a View with a [HotKey](~/api/Terminal.Gui.View.yml#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: diff --git a/docfx/docs/migratingfromv1.md b/docfx/docs/migratingfromv1.md index 7bd2f7e04c..fe37f1493d 100644 --- a/docfx/docs/migratingfromv1.md +++ b/docfx/docs/migratingfromv1.md @@ -6,7 +6,7 @@ For detailed breaking change documentation check out this Discussion: https://gi ## View Constructors -> Initializers -In v1, [View](~/api/Terminal.Gui.View.yml) and most sub-classes, had multiple constructors that took a variety of parameters. In v2, the constructors have been replaced with initializers. This change was made to simplify the API and make it easier to use. In addition, the v1 constructors drove a false (and needlessly complex) distinction between "Absoulte" and "Computed" layout. In v2, the layout system is much simpler and more intuitive. +In v1, [View](~/api/Terminal.Gui.View.yml) and most sub-classes had multiple constructors that took a variety of parameters. In v2, the constructors have been replaced with initializers. This change was made to simplify the API and make it easier to use. In addition, the v1 constructors drove a false (and needlessly complex) distinction between "Absolute" and "Computed" layout. In v2, the layout system is much simpler and more intuitive. ### How to Fix @@ -85,12 +85,12 @@ When measuring the screen space taken up by a `string` you can use the extension In v1, [View](~/api/Terminal.Gui.View.yml) was derived from `Responder` which supported `IDisposable`. In v2, `Responder` has been removed and [View](~/api/Terminal.Gui.View.yml) is the base-class supporting `IDisposable`. -In v1, [Application.Init](~/api/Terminal.Gui./Terminal.Gui.Application.Init) automatically created a toplevel view and set [Applicaton.Top](~/api/Terminal.Gui.Applicaton.Top.yml). In v2, [Application.Init](~/api/Terminal.Gui.Application.Init.yml) no longer automatically creates a toplevel or sets [Applicaton.Top](~/api/Terminal.Gui.Applicaton.Top.yml); app developers must explicitly create the toplevel view and pass it to [Appliation.Run](~/api/Terminal.Gui.Appliation.Run.yml) (or use `Application.Run`). Developers are responsible for calling `Dispose` on any toplevel they create before exiting. +In v1, [Application.Init](~/api/Terminal.Gui./Terminal.Gui.Application.Init) automatically created a toplevel view and set [Application.Top](~/api/Terminal.Gui.Application.Top.yml). In v2, [Application.Init](~/api/Terminal.Gui.Application.Init.yml) no longer automatically creates a toplevel or sets [Application.Top](~/api/Terminal.Gui.Application.Top.yml); app developers must explicitly create the toplevel view and pass it to [Application.Run](~/api/Terminal.Gui.Application.Run.yml) (or use `Application.Run`). Developers are responsible for calling `Dispose` on any toplevel they create before exiting. ### How to Fix * Replace `Responder` with [View](~/api/Terminal.Gui.View.yml) -* Update any code that assumes `Application.Init` automatically created a toplevel view and set `Applicaton.Top`. +* Update any code that assumes `Application.Init` automatically created a toplevel view and set `Application.Top`. * Update any code that assumes `Application.Init` automatically disposed of the toplevel view when the application exited. ## [Pos](~/api/Terminal.Gui.Pos.yml) and [Dim](~/api/Terminal.Gui.Dim.yml) types now adhere to standard C# idioms @@ -114,7 +114,7 @@ In v1, [Application.Init](~/api/Terminal.Gui./Terminal.Gui.Application.Init) aut In v2, the layout system has been improved to make it easier to create complex user interfaces. If you are using custom layouts in your application, you may need to update them to use the new layout system. -* The distinction between `Absoulte Layout` and `Computed Layout` has been removed, as has the `LayoutStyle` enum. v1 drew a false distinction between these styles. +* The distinction between `Absolute Layout` and `Computed Layout` has been removed, as has the `LayoutStyle` enum. v1 drew a false distinction between these styles. * [View.Frame](~/api/Terminal.Gui.View.Frame.yml) now represents the position and size of the view in the superview's coordinate system. The `Frame` property is of type `Rectangle`. * [View.Bounds](~/api/Terminal.Gui.View.Bounds.yml) has been replaced by [View.Viewport](~/api/Terminal.Gui.View.Viewport.yml). The `Viewport` property represents the visible area of the view in its own coordinate system. The `Viewport` property is of type `Rectangle`. * [View.GetContentSize()](~/api/Terminal.Gui.View.GetContentSize.yml) represents the size of the view's content. This replaces `ScrollView` and `ScrollBarView` in v1. See more below. @@ -150,7 +150,7 @@ In v2, the `Border`, `Margin`, and `Padding` properties have been added to all v ## Built-in Scrolling -In v1, scrolling was enabled by using `ScrollView` or `ScrollBarView`. In v2, the base [View](~/api/Terminal.Gui.View.yml) class supports scrolling inherently. The area of a view visible to the user at a given moment was previously a rectangle called `Bounds`. `Bounds.Location` was always `Point.Empty`. In v2 the visible area is a rectangle called `Viewport` which is a protal into the Views content, which can be bigger (or smaller) than the area visible to the user. Causing a view to scroll is as simple as changing `View.Viewport.Location`. The View's content described by [View.GetContentSize()](~/api/Terminal.Gui.View.GetContentSize.yml). See [Layout](layout.md) for details. +In v1, scrolling was enabled by using `ScrollView` or `ScrollBarView`. In v2, the base [View](~/api/Terminal.Gui.View.yml) class supports scrolling inherently. The area of a view visible to the user at a given moment was previously a rectangle called `Bounds`. `Bounds.Location` was always `Point.Empty`. In v2 the visible area is a rectangle called `Viewport` which is a protal into the Views content, which can be bigger (or smaller) than the area visible to the user. Causing a view to scroll is as simple as changing `View.Viewport.Location`. The View's content is described by [View.GetContentSize()](~/api/Terminal.Gui.View.GetContentSize.yml). See [Layout](layout.md) for details. ### How to Fix @@ -176,6 +176,7 @@ The API for handling keyboard input is significantly improved. See [Keyboard API * Use [View.Keybindings](~/api/Terminal.Gui.View.Keybindings.yml) to configure key bindings to `Command`s. * It should be very uncommon for v2 code to override `OnKeyPressed` etc... * Anywhere `Ctrl+Q` was hard-coded as the "quit key", replace with `Application.QuitKey`. +* See *Navigation* below for more information on v2's navigation keys. * Replace `Application.RootKeyEvent` with `Application.KeyDown`. If the reason for subscribing to RootKeyEvent was to enable an application-wide action based on a key-press, consider using Application.KeyBindings instead. ```diff @@ -207,17 +208,82 @@ The API for mouse input is now internally consistent and easier to use. + Application.MouseEvent(object? sender, MouseEvent mouseEvent) ``` -## Cursor and Focus +## Navigation - `Cursor`, `Focus`, `TabStop` etc... The cursor and focus system has been redesigned in v2 to be more consistent and easier to use. If you are using custom cursor or focus logic in your application, you may need to update it to use the new system. -### How to Fix +### Cursor + +In v1, whether the cursor (the flashing caret) was visible or not was controlled by `View.CursorVisibility` which was an enum extracted from Ncruses/Terminfo. It only works in some cases on Linux, and only partially with `WindowsDriver`. The position of the cursor was the same as `ConsoleDriver.Row`/`Col` and determined by the last call to `ConsoleDriver.Move`. `View.PositionCursor()` could be overridden by views to cause `Application` to call `ConsoleDriver.Move` on behalf of the app and to manage setting `CursorVisibility`. This API was confusing and bug-prone. + +In v2, the API is (NOT YET IMPLEMENTED) simplified. A view simply reports the style of cursor it wants and the Viewport-relative location: + +* `public Point? CursorPosition` + - If `null` the cursor is not visible + - If `{}` the cursor is visible at the `Point`. +* `public event EventHandler? CursorPositionChanged` +* `public int? CursorStyle` + - If `null` the default cursor style is used. + - If `{}` specifies the style of cursor. See [cursor.md](cursor.md) for more. +* `Application` now has APIs for querying available cursor styles. +* The details in `ConsoleDriver` are no longer available to applications. + +#### How to Fix (Cursor API) -* Use [Application.MostFocusedView](~/api/Terminal.Gui.Application.MostFocusedView.yml) to get the most focused view in the application. * Use [View.CursorPosition](~/api/Terminal.Gui.View.CursorPosition.yml) to set the cursor position in a view. Set [View.CursorPosition](~/api/Terminal.Gui.View.CursorPosition.yml) to `null` to hide the cursor. * Set [View.CursorVisibility](~/api/Terminal.Gui.View.CursorVisibility.yml) to the cursor style you want to use. * Remove any overrides of `OnEnter` and `OnLeave` that explicitly change the cursor. +### Focus + +See [navigation.md](navigation.md) for more details. +See also [Keyboard](keyboard.md) where HotKey is covered more deeply... + +* In v1 it was not possible to remove focus from a view. `HasFocus` as a get-only property. In v2, `view.HasFocus` can be set as well. Setting to `true` is equivalent to calling `view.SetFocus`. Setting to `false` is equivalent to calling `view.SuperView.AdvanceFocus` (which might not actually cause `view` to stop having focus). +* In v1, calling `super.Add (view)` where `view.CanFocus == true` caused all views up the hierarchy (all SuperViews) to get `CanFocus` set to `true` as well. In v2, developers need to explicitly set `CanFocus` for any view in the view-hierarchy where focus is desired. This simplifies the implementation and removes confusing automatic behavior. +* In v1, if `view.CanFocus == true`, `Add` would automatically set `TabStop`. In v2, the automatic setting of `TabStop` in `Add` is retained because it is not overly complex to do so and is a nice convenience for developers to not have to set both `Tabstop` and `CanFocus`. Note v2 does NOT automatically change `CanFocus` if `TabStop` is changed. +* `view.TabStop` now describes the behavior of a view in the focus chain. the `TabBehavior` enum includes `NoStop` (the view may be focusable, but not via next/prev keyboard nav), `TabStop` (the view may be focusable, and `NextTabStop`/`PrevTabStop` keyboard nav will stop), `TabGroup` (the view may be focusable, and `NextTabGroup`/`PrevTabGroup` keyboard nav will stop). +* In v1, the `View.Focused` property was a cache of which view in `SubViews/TabIndexes` had `HasFocus == true`. There was a lot of logic for keeping this property in sync. In v2, `View.Focused` is a get-only, computed property. +* In v1, the `View.MostFocused` property recursed down the subview-hierarchy on each get. In addition, because only one View in an application can be the "most focused", it doesn't make sense for this property to be on every View. In v2, this API is removed. Use `Application.Navigation.GetFocused()` instead. +* The v1 APIs `View.EnsureFocus`/`FocusNext`/`FocusPrev`/`FocusFirst`/`FocusLast` are replaced in v2 with these APIs that accomplish the same thing, more simply. + - `public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior)` + - `public bool FocusDeepest (NavigationDirection direction, TabBehavior? behavior)` +* In v1, the `View.OnEnter/Enter` and `View.OnLeave/Leave` virtual methods/events could be used to notify that a view had gained or lost focus, but had confusing semantics around what it mean to override (requiring calling `base`) and bug-ridden behavior on what the return values signified. The "Enter" and "Leave" terminology was confusing. In v2, `View.OnHasFocusChanging/HasFocusChanging` and `View.OnHasFocusChanged/HasFocusChanged` replace `View.OnEnter/Enter` and `View.OnLeave/Leave`. These virtual methods/events follow standard Terminal.Gui event patterns. The `View.OnHasFocusChanging/HasFocusChanging` event supports being cancelled. +* In v1, the concept of `Mdi` views included a large amount of complex code (in `Toplevel` and `Application`) for dealing with navigation across overlapped Views. This has all been radically simplified in v2. Any View can work in an "overlapped" or "tiled" way. See [navigation.md](navigation.md) for more details. +* The `View.TabIndex` and `View.TabIndexes` have been removed. Change the order of the views in `View.Subviews` to change the navigation order (using, for example `View.MoveSubviewTowardsStart()`). + +### How to Fix (Focus API) + +* Use [Application.Navigation.GetFocused()](~/api/Terminal.Gui.Application.Navigation.GetFocused.yml) to get the most focused view in the application. +* Use [Application.Navigation.AdvanceFocus()](~/api/Terminal.Gui.Application.Navigation.AdvanceFocus.yml) to cause focus to change. + +### Keyboard Navigation + +In v2, `HotKey`s can be used to navigate across the entire application view-hierarchy. They work independently of `Focus`. This enables a user to navigate across a complex UI of nested subviews if needed (even in overlapped scenarios). An example use-case is the `AllViewsTester` scenario. + +In v2, unlike v1, multiple Views in an application (even within the same SuperView) can have the same `HotKey`. Each press of the `HotKey` will invoke the next `HotKey` across the View hierarchy (NOT IMPLEMENTED YET)* + +In v1, the keys used for navigation were both hard-coded and configurable, but in an inconsistent way. `Tab` and `Shift+Tab` worked consistently for navigating between Subviews, but were not configurable. `Ctrl+Tab` and `Ctrl+Shift+Tab` navigated across `Overlapped` views and had configurable "alternate" versions (`Ctrl+PageDown` and `Ctrl+PageUp`). + +In v2, this is made consistent and configurable: + +- `Application.NextTabStopKey` (`Key.Tab`) - Navigates to the next subview that is a `TabStop` (see below). If there is no next, the first subview that is a `TabStop` will gain focus. +- `Application.PrevTabStopKey` (`Key.Tab.WithShift`) - Opposite of `Application.NextTabStopKey`. +- `Key.CursorRight` - Operates identically to `Application.NextTabStopKey`. +- `Key.CursorDown` - Operates identically to `Application.NextTabStopKey`. +- `Key.CursorLeft` - Operates identically to `Application.PrevTabStopKey`. +- `Key.CursorUp` - Operates identically to `Application.PrevTabStopKey`. +- `Application.NextTabGroupKey` (`Key.F6`) - Navigates to the next view in the view-hierarchy that is a `TabGroup` (see below). If there is no next, the first view which is a `TabGroup`` will gain focus. +- `Application.PrevTabGroupKey` (`Key.F6.WithShift`) - Opposite of `Application.NextTabGroupKey`. + +`F6` was chosen to match [Windows](https://learn.microsoft.com/en-us/windows/apps/design/input/keyboard-accelerators#common-keyboard-accelerators) + +These keys are all registered as `KeyBindingScope.Application` key bindings by `Application`. Because application-scoped key bindings have the lowest priority, Views can override the behaviors of these keys (e.g. `TextView` overrides `Key.Tab` by default, enabling the user to enter `\t` into text). The `AllViews_AtLeastOneNavKey_Leaves` unit test ensures all built-in Views have at least one of the above keys that can advance. + +### How to Fix (Keyboard Navigation) + +... + ## Button.Clicked Event Renamed The `Button.Clicked` event has been renamed `Button.Accept` @@ -261,7 +327,7 @@ public class TimeoutEventArgs : EventArgs { ``` ## How To Fix -If you previously had a lamda expression, you can simply add the extra arguments: +If you previously had a lambda expression, you can simply add the extra arguments: ```diff - btnLogin.Clicked += () => { /*do something*/ }; @@ -288,7 +354,7 @@ If you have used a named method instead of a lamda you will need to update the s All public classes that were previously [nested classes](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/nested-types) are now in the root namespace as their own classes. ### How To Fix -Replace references to to nested types with the new standalone version +Replace references to nested types with the new standalone version ```diff - var myTab = new TabView.Tab(); @@ -331,7 +397,7 @@ The [Aligner](~/api/Terminal.Gui.Aligner.yml) class makes it easy to align eleme In v1 `CheckBox` used `bool?` to represent the 3 states. To support consistent behavior for the `Accept` event, `CheckBox` was refactored to use the new `CheckState` enum instead of `bool?`. -Additionally the `Toggle` event was renamed `CheckStateChanging` and made cancelable. The `Toggle` method was renamed to `AdvanceCheckState`. +Additionally, the `Toggle` event was renamed `CheckStateChanging` and made cancelable. The `Toggle` method was renamed to `AdvanceCheckState`. ### How to Fix @@ -368,3 +434,13 @@ In v1, you could add timeouts via `Application.MainLoop.AddTimeout` among other + Application.AddTimeout (TimeSpan time, Func callback) ``` +## `SendSubviewXXX` renamed and corrected + +In v1, the `View` methods to move Subviews within the Subviews list were poorly named and actually operated in reverse of what their names suggested. + +In v2, these methods have been named correctly. + +- `SendSubViewToBack` -> `MoveSubviewToStart` - Moves the specified subview to the start of the list. +- `SendSubViewBackward` -> `MoveSubviewTowardsStart` - Moves the specified subview one position towards the start of the list. +- `SendSubViewToFront` -> `MoveSubviewToEnd` - Moves the specified subview to the end of the list. +- `SendSubViewForward` -> `MoveSubviewTowardsEnd` - Moves the specified subview one position towards the end of the list. diff --git a/docfx/docs/navigation.md b/docfx/docs/navigation.md index 0b87ac0a74..c68b56d619 100644 --- a/docfx/docs/navigation.md +++ b/docfx/docs/navigation.md @@ -1,7 +1,5 @@ # Navigation Deep Dive -**Navigation** refers to the user-experience for moving Focus between views in the application view-hierarchy. It applies to the following questions: - - What are the visual cues that help the user know which element of an application is receiving keyboard and mouse input (which one has focus)? - How does the user change which element of an application has focus? - How does the user change which element of an application has focus? @@ -11,14 +9,16 @@ ## Lexicon & Taxonomy -- **Navigation** - Refers to the user-experience for moving Focus between views in the application view-hierarchy. -- **Focus** - Indicates which view-hierarchy is receiving keyboard input. Only one view-hierarchy in an application can have focus (`top.HasFocus == true`), and there one, and only one, View in a focused hierarchy that is the most-focused; the one receiving keyboard input. +- **Navigation** refers to the user experience for moving focus between views in the application view-hierarchy. +- **Focus** - Refers to the state where a particular UI element (`View`), such as a button, input field, or any interactive component, is actively selected and ready to receive user input. When an element has focus, it typically responds to keyboard events and other interactions. +- **Focus Chain** - The ordered sequence of UI elements that can receive focus, starting from the currently focused element and extending to its parent (SuperView) elements up to the root of the focus tree (`Application.Top`). This chain determines the path that focus traversal follows within the application. Only one focus chain in an application can have focus (`top.HasFocus == true`), and there is one, and only one, View in a focus chain that is the most-focused; the one receiving keyboard input. - **Cursor** - A visual indicator to the user where keyboard input will have an impact. There is one Cursor per terminal session. See [Cursor](cursor.md) for a deep-dive. -- **Tab** - Describes the `Tab` key found on all keyboards, a break in text that is wider than a space, or a UI element that is a stop-point for keyboard navigation. The use of the word "Tab" for this comes from the typewriter, and is re-enforced by the existence of a `Tab` key on all keyboards. +- **Focus Ordering** - The order focusable Views are navigated. Focus Ordering is typically used in UI frameworks to enable screen readers and improve the Accessibility of an application. In v1, `TabIndex`/`TabIndexes` enabled Focus Ordering. +- **Tab** - Describes the `Tab` key found on all keyboards, a break in text that is wider than a space, or a UI element that is a stop-point for keyboard navigation. The use of the word "Tab" for this comes from the typewriter, and is reinforced by the existence of a `Tab` key on all keyboards. - **TabStop** - A `View` that is an ultimate stop-point for keyboard navigation. In this usage, ultimate means the `View` has no focusable subviews. The `Application.NextTabStopKey` and `Application.PrevTabStopKey` are `Key.Tab` and `Key.Tab.WithShift` respectively. These keys navigate only between peer-views. - **TabGroup** - A `View` that is a container for other focusable views. The `Application.NextTabGroupKey` and `Application.PrevTabGroupKey` are `Key.PageDown.WithCtrl` and `Key.PageUp.WithCtrl` respectively. These keys enable the user to use the keyboard to navigate up and down the view-hierarchy. -- **Enter** / **Gain** - Means a View that previously was not focused is now becoming focused. "The View is entering focus" is the same as "The View is gaining focus". -- **Leave** / **Lose** - Means a View that previously was focused is now becoming un-focused. "The View is leaving focus" is the same as "The View is losing focus". +- **Enter** / **Gain** - Means a View that previously was not focused is now becoming focused. "The View is entering focus" is the same as "The View is gaining focus". These terms are legacy terms from v1. +- **Leave** / **Lose** - Means a View that previously was focused is now becoming un-focused. "The View is leaving focus" is the same as "The View is losing focus". These terms are legacy terms from v1. ## Tenets for Terminal.Gui UI Navigation (Unless you know better ones...) @@ -28,11 +28,11 @@ Tenets higher in the list have precedence over tenets lower in the list. * **One Focus Per App** - It should not be possible to have two views be the "most focused" view in an application. -* **There's Always a Way With The Keyboard** - The framework strives to ensure users' wanting to use the keyboard can't get into a situation where some element of the application is not accessible via the keyboard. For example, we have unit tests that ensure built-in Views will all have at least one navigation key that advances focus. Another example: As long as a View with a HotKey is visible and enabled, regardless of view hierarchy, if the user presses that hotkey, the action defined by the hotkey will happen (and, by default the View that defines it will be focused). +* **There's Always a Way With The Keyboard** - The framework strives to ensure users' wanting to use the keyboard can't get into a situation where some element of the application is not accessible via the keyboard. For example, we have unit tests that ensure built-in Views will all have at least one navigation key that advances focus. Another example: As long as a View with a HotKey is visible and enabled, regardless of view-hierarchy, if the user presses that hotkey, the action defined by the hotkey will happen (and, by default the View that defines it will be focused). -* **Flexible Overrides** - The framework makes it easy for navigation changes to be made from code and enables changing of behavior to be done in flexible ways. For example a view can be prevented from getting focus by setting `CanFocus` to `false`, overriding `OnEnter` and returning `true` to cancel, or subscribing to `Enter` and setting `Cancel` to `true`. +* **Flexible Overrides** - The framework makes it easy for navigation changes to be made from code and enables changing of behavior to be done in flexible ways. For example a view can be prevented from getting focus by setting `CanFocus` to `false` or overriding `OnHasFocusChanging` and returning `true` to cancel. -* **Decouple Concepts** - In v1 `CanFocus` is tightly coupled with `HasFocus`, `TabIndex`, `TabIndexes`, and `TabStop` and vice-versa. There is a bunch of "magic" logic that automatically attempts to keep these concepts aligned. This results in a bunch of poorly specified, hard to test, and fragile APIs. In v2 we strive to keep the related navigation concepts decoupled. For example, `CanFocus` and `TabStop` completely distinct. A view with `CanFocus == true` can have `TabStop == NoStop` and still be focusable with the mouse. +* **Decouple Concepts** - In v1 `CanFocus` is tightly coupled with `HasFocus`, `TabIndex`, `TabIndexes`, and `TabStop` and vice-versa. There was a bunch of "magic" logic that automatically attempted to keep these concepts aligned. This resulted in a poorly specified, hard-to-test, and fragile API. In v2 we strive to keep the related navigation concepts decoupled. For example, `CanFocus` and `TabStop` are decoupled. A view with `CanFocus == true` can have `TabStop == NoStop` and still be focusable with the mouse. # Design @@ -59,9 +59,9 @@ These keys are all registered as `KeyBindingScope.Application` key bindings by ` See also [Keyboard](keyboard.md) where HotKey is covered more deeply... -In v2, `HotKey`s can be used to navigate across the entire application view-hierarchy. They work independently of `Focus`. This enables a user to navigate across a complex UI of nested subviews if needed (even in overlapped scenarios). An example use-case is the `AllViewsTester` scenario. +In v2, `HotKey`s can be used to navigate across the entire application view-hierarchy. They work independently of `Focus`. This enables a user to navigate across a complex UI of nested subviews if needed (even in overlapped scenarios). An example use case is the `AllViewsTester` scenario. -Additionally, in v2, multiple Views in an application (even within the same SuperView) can have the same HotKey. Each press of the HotKey will invoke the next HotKey across the View hierarchy (NOT IMPLEMENTED YET - And may be too complex to actually implement for v2.) +Additionally, in v2, multiple Views in an application (even within the same SuperView) can have the same HotKey. Each press of the HotKey will invoke the next HotKey across the View hierarchy (NOT IMPLEMENTED YET see https://github.com/gui-cs/Terminal.Gui/issues/3554). ## Mouse Navigation @@ -73,9 +73,9 @@ Mouse-based navigation is straightforward in comparison to keyboard: If a view i The answer to both questions is: -If the View was previously focused, and focus left, the system keeps a record of the Subview that was previously most-focused and restores focus to that Subview (`RestoreFocus()`). +If the View was previously focused, the system keeps a record of the Subview that was previously most-focused and restores focus to that Subview (`RestoreFocus()`). -If the View was not previously focused, `FindDeepestFocusableView()` is used to find the deepest focusable view and call `SetFocus()` on it. +If the View was not previously focused, `AdvanceFocus()` is called. For this to work properly, there must be logic that removes the focus-cache used by `RestoreFocus()` if something changes that makes the previously-focusable view not focusable (e.g. if Visible has changed). @@ -98,7 +98,7 @@ Causes the focus to advance (forward or backwards) to the next View in the appli The implementation is simple: ```cs -return Application.GetFocused()?.AdvanceFocus (direction, behavior) ?? false; +return Application.Current?.AdvanceFocus (direction, behavior); ``` This method is called from the `Command` handlers bound to the application-scoped keybindings created during `Application.Init`. It is `public` as a convenience. @@ -109,11 +109,12 @@ This method replaces about a dozen functions in v1 (scattered across `Applicatio At the View-level, navigation is encapsulated within `View.Navigation.cs`. + ## What makes a View focusable? First, only Views that are visible and enabled can gain focus. Both `Visible` and `Enabled` must be `true` for a view to be focusable. -For visible and enabled Views, the `CanFocus` property is then used to determine whether the `View` is focusable. `CanFocus` must be `true` for a View to gain focus. However, even if `CanFocus` is `true`, other factor can prevent the view from gaining focus... +For visible and enabled Views, the `CanFocus` property is then used to determine whether the `View` is focusable. `CanFocus` must be `true` for a View to gain focus. However, even if `CanFocus` is `true`, other factors can prevent the view from gaining focus... A visible, enabled, and `CanFocus == true` view can be focused if the user uses the mouse to clicks on it or if code explicitly calls `View.SetFocus()`. Of course, the view itself or some other code can cancel the focus (e.g. by overriding `OnEnter`). @@ -121,7 +122,7 @@ For keyboard navigation, the `TabStop` property is a filter for which views are * `null` - This View is still being initialized; acts as a signal to `set_CanFocus` to set `TabStop` to `TabBehavior.TabStop` as convince for the most common use-case. Equivalent to `TabBehavior.NoStop` when determining if a view is focusable by the keyboard or not. * `TabBehavior.NoStop` - Prevents the user from using keyboard navigation to cause view (and by definition it's subviews) to gain focus. Note: The view can still be focused using code or the mouse. -* `TabBehavior.TabStop` - Indicates a View is a focusable view with no focusable subviews. `Application.Next/PrevTabStopKey` will advance ONLY through the peer-Views (`SuperView.Subviews`). +* `TabBehavior.TabStop` - Indicates a View is a focusable view with no focusable subviews. `Application.Next/PrevTabStopKey` will advance ONLY through the peer-Views (`SuperView.Subviews`). * `TabBehavior.GroupStop` - Indicates a View is a focusable container for other focusable views and enables keyboard navigation across these containers. This applies to both tiled and overlapped views. For example, `FrameView` is a simple view designed to be a visible container of other views tiled scenarios. It has `TabStop` set to `TabBehavior.GroupStop` (and `Arrangement` set to `ViewArrangement.Fixed`). Likewise, `Window` is a simple view designed to be a visible container of other views in overlapped scenarios. It has `TabStop` set to `TabBehavior.GroupStop` (and `Arrangement` set to `ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped`). `Application.Next/PrevGroupStopKey` will advance across all `GroupStop` views in the application (unless blocked by a `NoStop` SuperView). @@ -129,7 +130,7 @@ For keyboard navigation, the `TabStop` property is a filter for which views are `View.HasFocus` indicates whether the `View` is focused or not. It is the definitive signal. If the view has no focusable Subviews then this property also indicates the view is the most-focused view in the application. -Setting this property to `true` has the same effect as calling `View.SetFocus ()`, which also means the focus may not actually change as a result. +Setting this property to `true` has the same effect as calling `View.SetFocus ()`, which also means the focus may not change as a result. If `v.HasFocus == true` then @@ -245,3 +246,100 @@ A bunch of the above is the proposed design. Eventually `Toplevel` will be delet - The old `Toplevel` and `OverlappedTop` code. Only utilized when `IsOverlappedContainer == true` - The new code path that treats all Views the same but relies on the appropriate combination of `TabBehavior` and `ViewArrangement` settings as well as `IRunnable`. + +# Rough Design Notes + +## Accesibilty Tenets + +See https://devblogs.microsoft.com/dotnet/the-journey-to-accessible-apps-keyboard-accessible/ + +https://github.com/dotnet/maui/issues/1646 + +## Focus Chain & DOM ideas + +The navigation/focus code in `View.Navigation.cs` has been rewritten in v2 (in https://github.com/gui-cs/Terminal.Gui/pull/3627) to simplify and make more robust. + +The design is fundamentally the same as in v1: The logic for tracking and updating the focus chain is based on recursion up and down the `View.Subviews`/`View.SuperView` hierarchy. In this model, there is the need for tracking state during recursion, leading to APIs like the following: + +```cs +// From v1/early v2: Note the `force` param. +private void SetHasFocus (bool newHasFocus, View view, bool force = false) + +// From #3627: Note the `traversingUp` param + private bool EnterFocus ([CanBeNull] View leavingView, bool traversingUp = false) +``` + +The need for these "special-case trackers" is clear evidence of poor architecture. Both implementations work, and the #3627 version is far cleaner, but a better design could result in further simplification. + +For example, moving to a model where `Application` is responsible for tracking and updating the focus chain instead `View`. We would introduce a formalization of the *Focus Chain*. + +**Focus Chain**: A sequence or hierarchy of UI elements (Views) that determines the order in which keyboard focus is navigated within an application. This chain represents the potential paths that focus can take, ensuring that each element can be reached through keyboard navigation. Instead of using recursion, the Focus Chain can be implemented using lists or trees to maintain and update the focus state efficiently at the `Application` level. + +By using lists or trees, you can manage the focus state without the need for recursive traversal, making the navigation model more scalable and easier to maintain. This approach allows you to explicitly define the order and structure of focusable elements, providing greater control over the navigation flow. + +Now, the interesting thing about this, is it really starts to look like a DOM! + +Designing a DOM (Document Object Model) for UI library involves creating a structured representation of the UI elements and their relationships. + +1. Hierarchy and Structure- Root Node: The top-level node representing the entire application or window. + - View Nodes: Each UI element (View) is a node in the DOM. These nodes can have child nodes, representing nested or contained elements. +2. Node Properties- Attributes: Each node can have attributes such as id, class, style, and custom properties specific to the View. + - State: Nodes can maintain state information, such as whether they are focused, visible, enabled, etc. +3. Traversal Methods- Parent-Child Relationships: Nodes maintain references to their parent and children, allowing traversal up and down the hierarchy. + - Sibling Relationships: Nodes can also maintain references to their previous and next siblings for easier navigation. +4. Event Handling- Event Listeners: Nodes can have event listeners attached to handle user interactions like clicks, key presses, and focus changes. + - Event Propagation: Events can propagate through the DOM, allowing for capturing and bubbling phases similar to web DOM events. +5. Focus Management- Focus Chain: Maintain a list or tree of focusable nodes to manage keyboard navigation efficiently. + - Focus Methods: Methods to programmatically set and get focus, ensuring the correct element is focused based on user actions or application logic. +6. Mouse Events - Mouse handling in Terminal.Gui involves capturing and responding to mouse events such as clicks, drags, and scrolls. In v2, mouse events are managed at the View level, but for a DOM-like structure, this should be centralized. +7. Layout - The Pos/Dim system in Terminal.Gui is used for defining the layout of views. It allows for dynamic positioning and sizing based on various constraints. For a DOM-model we'd maintain the Pos/Dim system but ensure the layout calculations are managed by the DOM manager. +8. Drawing - Drawing in Terminal.Gui involves rendering text, colors, and shapes. This is handled within the View class today. In a DOM model we'd centralize the drawing logic in the DOM manager to ensure consistent rendering. + +This is all well and good, however we are NOT going to fully transition to a DOM in v2. But we may start with Focus/Navigation (item 3 above). Would could retain the existing external `View` API for focus (e.g. `View.SetFocus`, `Focused`, `CanFocus`, `TabIndexes`, etc...) but refactor the implementation of those to leverage a `FocusChain` (or `FocusManager`) at the `Application` level. + +(Crap code generated by Copilot; but gets the idea across): + +```cs +public class FocusChain { + private List focusableViews = new List(); + private View currentFocusedView; + + public void RegisterView(View view) { + if (view.CanFocus) { + focusableViews.Add(view); + focusableViews = focusableViews.OrderBy(v => v.TabIndex).ToList(); + } + } + + public void UnregisterView(View view) { + focusableViews.Remove(view); + } + + public void SetFocus(View view) { + if (focusableViews.Contains(view)) { + currentFocusedView?.LeaveFocus(); + currentFocusedView = view; + currentFocusedView.EnterFocus(); + } + } + + public View GetFocusedView() { + return currentFocusedView; + } + + public void MoveFocusNext() { + if (focusableViews.Count == 0) return; + int currentIndex = focusableViews.IndexOf(currentFocusedView); + int nextIndex = (currentIndex + 1) % focusableViews.Count; + SetFocus(focusableViews[nextIndex]); + } + + public void MoveFocusPrevious() { + if (focusableViews.Count == 0) return; + int currentIndex = focusableViews.IndexOf(currentFocusedView); + int previousIndex = (currentIndex - 1 + focusableViews.Count) % focusableViews.Count; + SetFocus(focusableViews[previousIndex]); + } +} +``` + diff --git a/docfx/docs/newinv2.md b/docfx/docs/newinv2.md index 96f68c8881..4659e4db56 100644 --- a/docfx/docs/newinv2.md +++ b/docfx/docs/newinv2.md @@ -30,7 +30,7 @@ The entire library has been reviewed and simplified. As a result, the API is mor * New! *`Dim.Auto`* - Automatically sizes the view to fitthe view's Text, SubViews, or ContentArea. * Improved! *`Pos.AnchorEnd ()`* - New to v2 is `Pos.AnchorEnd ()` (with no parameters) which allows a view to be anchored to the right or bottom of the Superview. * New! *`Pos.Align ()`* - Aligns a set of views horizontally or vertically (left, rigth, center, etc...). -* ... +* Keyboard [Navigation](navigation.md) has been revamped to be more reliability and ensure TUI apps built with Terminal.Gui are accessible. ## New and Improved Built-in Views @@ -44,6 +44,7 @@ The entire library has been reviewed and simplified. As a result, the API is mor * *[MenuBar](~/api/Terminal.Gui.MenuBar.yml)* - COMING SOON! New implementation based on `Bar` * *[ContextMenu](~/api/Terminal.Gui.ContextMenu.yml)* - COMING SOON! New implementation based on `Bar` * *[FileDialog](~/api/Terminal.Gui.FileDialog.yml)* - The new, modern file dialog includes icons (in TUI!) for files/folders, search, and a `TreeView`. +* [ColorPicker](~/api/Terminal.Gui/ColorPicker.yml)* - Fully supports TrueColor with the ability to choose a color using HSV, RGB, or HSL as well as W3C standard color names. ## Configuration Manager