From d407683d5b0821f62ac2f8d9925c9ca22fb10c0d Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 27 Jul 2024 17:21:47 -0400 Subject: [PATCH] Made View.Navigation nullable enable. Changed TabIndex to int?. Changed TabStop to int?. Changed TabStop flags. --- Terminal.Gui/View/Adornment/Margin.cs | 4 +- Terminal.Gui/View/Adornment/ShadowView.cs | 4 +- .../Navigation/{TabStop.cs => TabBehavior.cs} | 14 +- Terminal.Gui/View/View.Navigation.cs | 1030 ++++++++--------- Terminal.Gui/View/View.cs | 4 - Terminal.Gui/Views/ComboBox.cs | 16 +- Terminal.Gui/Views/FileDialog.cs | 6 +- Terminal.Gui/Views/TileView.cs | 2 +- UICatalog/Scenarios/Buttons.cs | 2 +- UnitTests/View/NavigationTests.cs | 38 +- 10 files changed, 552 insertions(+), 568 deletions(-) rename Terminal.Gui/View/Navigation/{TabStop.cs => TabBehavior.cs} (51%) diff --git a/Terminal.Gui/View/Adornment/Margin.cs b/Terminal.Gui/View/Adornment/Margin.cs index 9f96a54a5..ac2705f7e 100644 --- a/Terminal.Gui/View/Adornment/Margin.cs +++ b/Terminal.Gui/View/Adornment/Margin.cs @@ -226,12 +226,12 @@ public class Margin : Adornment { case ShadowStyle.Transparent: // BUGBUG: This doesn't work right for all Border.Top sizes - Need an API on Border that gives top-right location of line corner. - _rightShadow.Y = Parent.Border.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0; + _rightShadow.Y = Parent!.Border.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0; break; case ShadowStyle.Opaque: // BUGBUG: This doesn't work right for all Border.Top sizes - Need an API on Border that gives top-right location of line corner. - _rightShadow.Y = Parent.Border.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0; + _rightShadow.Y = Parent!.Border.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0; _bottomShadow.X = Parent.Border.Thickness.Left > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).X + 1 : 0; break; diff --git a/Terminal.Gui/View/Adornment/ShadowView.cs b/Terminal.Gui/View/Adornment/ShadowView.cs index b4ffa1466..1ca027ade 100644 --- a/Terminal.Gui/View/Adornment/ShadowView.cs +++ b/Terminal.Gui/View/Adornment/ShadowView.cs @@ -113,7 +113,7 @@ internal class ShadowView : View { Driver.Move (i, screen.Y); - if (i < Driver.Contents.GetLength (1) && screen.Y < Driver.Contents.GetLength (0)) + if (i < Driver.Contents!.GetLength (1) && screen.Y < Driver.Contents.GetLength (0)) { Driver.AddRune (Driver.Contents [screen.Y, i].Rune); } @@ -141,7 +141,7 @@ internal class ShadowView : View { Driver.Move (screen.X, i); - if (screen.X < Driver.Contents.GetLength (1) && i < Driver.Contents.GetLength (0)) + if (Driver.Contents is { } && screen.X < Driver.Contents.GetLength (1) && i < Driver.Contents.GetLength (0)) { Driver.AddRune (Driver.Contents [i, screen.X].Rune); } diff --git a/Terminal.Gui/View/Navigation/TabStop.cs b/Terminal.Gui/View/Navigation/TabBehavior.cs similarity index 51% rename from Terminal.Gui/View/Navigation/TabStop.cs rename to Terminal.Gui/View/Navigation/TabBehavior.cs index 2a68f1b9c..1fd87b7d6 100644 --- a/Terminal.Gui/View/Navigation/TabStop.cs +++ b/Terminal.Gui/View/Navigation/TabBehavior.cs @@ -1,20 +1,14 @@ namespace Terminal.Gui; /// -/// Describes a TabStop; a stop-point for keyboard navigation between Views. +/// Describes how behaves. A TabStop is a stop-point for keyboard navigation between Views. /// -/// -/// -/// TabStop does not impact whether a view is focusable or not. determines this independently of TabStop. -/// -/// -[Flags] -public enum TabStop +public enum TabBehavior { /// /// The View will not be a stop-poknt for keyboard-based navigation. /// - None = 0, + NoStop = 0, /// /// The View will be a stop-point for keybaord-based navigation across Views (e.g. if the user presses `Tab`). @@ -22,7 +16,7 @@ public enum TabStop TabStop = 1, /// - /// The View will be a stop-point for keyboard-based navigation across TabGroups (e.g. if the user preesses (`Ctrl-PageDown`). + /// The View will be a stop-point for keyboard-based navigation across groups (e.g. if the user preesses (`Ctrl-PageDown`). /// TabGroup = 2, } diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index e573e7dbe..6c7ea5194 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -4,9 +4,424 @@ 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 + /// . + /// itself. + /// + /// + /// + /// If there is no next/previous view, the focus is set to the view itself. + /// + /// + /// + /// If will advance into ... + /// + /// if focus was changed to another subview (or stayed on this one), + /// otherwise. + /// + public bool AdvanceFocus (NavigationDirection direction, bool acrossGroupOrOverlapped = false) + { + if (!CanBeVisible (this)) + { + return false; + } + + FocusDirection = direction; + + if (TabIndexes is null || TabIndexes.Count == 0) + { + return false; + } + + if (Focused is null) + { + switch (direction) + { + case NavigationDirection.Forward: + FocusFirst (); + + break; + case NavigationDirection.Backward: + FocusLast (); + + break; + default: + throw new ArgumentOutOfRangeException (nameof (direction), direction, null); + } + + return Focused is { }; + } + + var focusedFound = false; + + foreach (View w in direction == NavigationDirection.Forward + ? TabIndexes.ToArray () + : TabIndexes.ToArray ().Reverse ()) + { + if (w.HasFocus) + { + // A subview has focus, tell *it* to FocusNext + if (w.AdvanceFocus (direction, acrossGroupOrOverlapped)) + { + // The subview changed which of it's subviews had focus + return true; + } + + if (acrossGroupOrOverlapped && Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + return false; + } + + //Debug.Assert (w.HasFocus); + + if (w.Focused is null) + { + // No next focusable view was found. + if (w.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + // Keep focus w/in w + return false; + } + } + + // The subview has no subviews that can be next. Cache that we found a focused subview. + focusedFound = true; + + continue; + } + + // The subview does not have focus, but at least one other that can. Can this one be focused? + if (focusedFound && w.CanFocus && w.TabStop == TabBehavior.TabStop && w.Visible && w.Enabled) + { + // Make Focused Leave + Focused.SetHasFocus (false, w); + + // If the focused view is overlapped don't focus on the next if it's not overlapped. + //if (acrossGroupOrOverlapped && Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)/* && !w.Arrangement.HasFlag (ViewArrangement.Overlapped)*/) + //{ + // return false; + //} + + // If the focused view is not overlapped and the next is, skip it + if (!acrossGroupOrOverlapped && !Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped)) + { + continue; + } + + switch (direction) + { + case NavigationDirection.Forward: + w.FocusFirst (); + + break; + case NavigationDirection.Backward: + w.FocusLast (); + + break; + } + + SetFocus (w); + + return true; + } + } + + if (Focused is { }) + { + // Leave + Focused.SetHasFocus (false, this); + + // Signal that nothing is focused, and callers should try a peer-subview + Focused = null; + } + + return false; + } + + /// Gets or sets a value indicating whether this can be focused. + /// + /// + /// must also have set to . + /// + /// + /// When set to , if an attempt is made to make this view focused, the focus will be set to + /// the next focusable view. + /// + /// + /// When set to , the will be set to -1. + /// + /// + /// When set to , the values of and 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 + /// will have no effect on . + /// + /// + 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; + } + + _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?.FocusFirstOrLast (); + + // 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); + + if (SuperView.Focused is null && Application.Current is { }) + { + Application.Current.AdvanceFocus (NavigationDirection.Forward); + } + + ApplicationOverlapped.BringOverlappedTopToFront (); + } + } + + if (_subviews is { } && IsInitialized) + { + 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 (); + } + } + + OnCanFocusChanged (); + SetNeedsDisplay (); + } + } + + /// Raised when has been changed. + /// + /// 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; } + + /// + /// Focuses the first focusable view in if one exists. If there are no views in + /// then the focus is set to the view itself. + /// + /// + /// If , only subviews where has + /// set + /// will be considered. + /// + public void FocusFirst (bool overlappedOnly = false) + { + if (!CanBeVisible (this)) + { + return; + } + + if (_tabIndexes is null) + { + SuperView?.SetFocus (this); + + return; + } + + foreach (View view in _tabIndexes.Where (v => !overlappedOnly || v.Arrangement.HasFlag (ViewArrangement.Overlapped))) + { + if (view.CanFocus && view.TabStop == TabBehavior.TabStop && view.Visible && view.Enabled) + { + SetFocus (view); + + return; + } + } + } + + /// + /// Focuses the last focusable view in if one exists. If there are no views in + /// then the focus is set to the view itself. + /// + /// + /// If , only subviews where has + /// set + /// will be considered. + /// + public void FocusLast (bool overlappedOnly = false) + { + if (!CanBeVisible (this)) + { + return; + } + + if (_tabIndexes is null) + { + SuperView?.SetFocus (this); + + return; + } + + foreach (View view in _tabIndexes.Where (v => !overlappedOnly || v.Arrangement.HasFlag (ViewArrangement.Overlapped)).Reverse ()) + { + if (view.CanFocus && view.TabStop == TabBehavior.TabStop && view.Visible && view.Enabled) + { + SetFocus (view); + + return; + } + } + } + + /// + /// 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 + { + // Force the specified view to have focus + set => SetHasFocus (value, this, true); + get => _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. + /// + /// The most focused Subview. + public View MostFocused + { + get + { + if (Focused is null) + { + return null; + } + + View most = Focused.MostFocused; + + if (most is { }) + { + return most; + } + + return Focused; + } + } + + /// Invoked when the property from a view is changed. + /// + /// Raises the event. + /// + 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). @@ -53,19 +468,32 @@ public partial class View // Focus and cross-view navigation management (TabStop return false; } - /// Raised when the view is gaining (entering) focus. Can be cancelled. - /// - /// Raised by the virtual method. - /// - public event EventHandler Enter; + /// + /// 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); + } - /// Raised when the view is losing (leaving) focus. Can be cancelled. - /// - /// Raised by the virtual method. - /// - public event EventHandler Leave; + return; + } - private NavigationDirection _focusDirection; + // Recursively set focus upwards in the view hierarchy + if (SuperView is { }) + { + SuperView.SetFocus (this); + } + else + { + SetFocus (this); + } + } /// /// INTERNAL API that gets or sets the focus direction for this view and all subviews. @@ -87,247 +515,23 @@ public partial class View // Focus and cross-view navigation management (TabStop } } - private bool _hasFocus; - /// - /// Gets or sets whether this view has focus. + /// INTERNAL helper for calling or based on + /// . + /// FocusDirection is not public. This API is thus non-deterministic from a public API perspective. /// - /// - /// - /// 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 + internal void FocusFirstOrLast () { - // Force the specified view to have focus - set => SetHasFocus (value, this, true); - get => _hasFocus; - } - - /// - /// 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) + if (Focused is null && _subviews?.Count > 0) { - _hasFocus = newHasFocus; - - if (newHasFocus) + if (FocusDirection == NavigationDirection.Forward) { - OnEnter (view); + FocusFirst (); } else { - OnLeave (view); + FocusLast (); } - - SetNeedsDisplay (); - } - - // Remove focus down the chain of subviews if focus is removed - if (!newHasFocus && Focused is { }) - { - View f = Focused; - f.OnLeave (view); - f.SetHasFocus (false, view); - Focused = null; - } - } - - // 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; - - // 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; - - /// Gets or sets a value indicating whether this can be focused. - /// - /// - /// must also have set to . - /// - /// - /// When set to , if an attempt is made to make this view focused, the focus will be set to the next focusable view. - /// - /// - /// When set to , the will be set to -1. - /// - /// - /// When set to , the values of and 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 will have no effect on . - /// - /// - 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; - } - - _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 (_canFocus && _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 = SuperView is { } ? SuperView._tabIndexes.IndexOf (this) : -1; - //} - - if (TabStop == TabStop.None && _canFocus) - { - TabStop = TabStop.TabStop; - } - - if (!_canFocus && SuperView?.Focused == this) - { - SuperView.Focused = null; - } - - if (!_canFocus && HasFocus) - { - SetHasFocus (false, this); - SuperView?.FocusFirstOrLast (); - - // 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); - - if (SuperView.Focused is null && Application.Current is { }) - { - Application.Current.AdvanceFocus (NavigationDirection.Forward); - } - - ApplicationOverlapped.BringOverlappedTopToFront (); - } - } - - if (_subviews is { } && IsInitialized) - { - 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 (); - } - } - - OnCanFocusChanged (); - SetNeedsDisplay (); - } - } - - /// Raised when has been changed. - /// - /// Raised by the virtual method. - /// - public event EventHandler CanFocusChanged; - - /// Invoked when the property from a view is changed. - /// - /// Raises the event. - /// - public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); } - - /// Returns the currently focused Subview inside this view, or if nothing is focused. - /// The currently focused Subview. - public View Focused { get; private set; } - - /// - /// Returns the most focused Subview in the chain of subviews (the leaf view that has the focus), or - /// if nothing is focused. - /// - /// The most focused Subview. - public View MostFocused - { - get - { - if (Focused is null) - { - return null; - } - - View most = Focused.MostFocused; - - if (most is { }) - { - return most; - } - - return Focused; } } @@ -407,258 +611,52 @@ public partial class View // Focus and cross-view navigation management (TabStop } /// - /// Causes this view to be focused. All focusable views up the Superview hierarchy will also be 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. /// - public void SetFocus () + /// 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 (!CanBeVisible (this) || !Enabled) + if (HasFocus != newHasFocus || force) { - if (HasFocus) + _hasFocus = newHasFocus; + + if (newHasFocus) { - // If this view is focused, make it leave focus - SetHasFocus (false, this); - } - - return; - } - - // Recursively set focus upwards in the view hierarchy - if (SuperView is { }) - { - SuperView.SetFocus (this); - } - else - { - SetFocus (this); - } - } - - /// - /// INTERNAL helper for calling or based on - /// . - /// FocusDirection is not public. This API is thus non-deterministic from a public API perspective. - /// - internal void FocusFirstOrLast () - { - if (Focused is null && _subviews?.Count > 0) - { - if (FocusDirection == NavigationDirection.Forward) - { - FocusFirst (); + OnEnter (view); } else { - FocusLast (); - } - } - } - - /// - /// Focuses the first focusable view in if one exists. If there are no views in - /// then the focus is set to the view itself. - /// - /// - /// If , only subviews where has - /// set - /// will be considered. - /// - public void FocusFirst (bool overlappedOnly = false) - { - if (!CanBeVisible (this)) - { - return; - } - - if (_tabIndexes is null) - { - SuperView?.SetFocus (this); - - return; - } - - foreach (View view in _tabIndexes.Where (v => !overlappedOnly || v.Arrangement.HasFlag (ViewArrangement.Overlapped))) - { - if (view.CanFocus && view.TabStop.HasFlag (TabStop.TabStop) && view.Visible && view.Enabled) - { - SetFocus (view); - - return; - } - } - } - - /// - /// Focuses the last focusable view in if one exists. If there are no views in - /// then the focus is set to the view itself. - /// - /// - /// If , only subviews where has - /// set - /// will be considered. - /// - public void FocusLast (bool overlappedOnly = false) - { - if (!CanBeVisible (this)) - { - return; - } - - if (_tabIndexes is null) - { - SuperView?.SetFocus (this); - - return; - } - - foreach (View view in _tabIndexes.Where (v => !overlappedOnly || v.Arrangement.HasFlag (ViewArrangement.Overlapped)).Reverse ()) - { - if (view.CanFocus && view.TabStop.HasFlag (TabStop.TabStop) && view.Visible && view.Enabled) - { - SetFocus (view); - - return; - } - } - } - - /// - /// Advances the focus to the next or previous view in , based on - /// . - /// itself. - /// - /// - /// - /// If there is no next/previous view, the focus is set to the view itself. - /// - /// - /// - /// If will advance into ... - /// - /// if focus was changed to another subview (or stayed on this one), - /// otherwise. - /// - public bool AdvanceFocus (NavigationDirection direction, bool acrossGroupOrOverlapped = false) - { - if (!CanBeVisible (this)) - { - return false; - } - - FocusDirection = direction; - - if (TabIndexes is null || TabIndexes.Count == 0) - { - return false; - } - - if (Focused is null) - { - switch (direction) - { - case NavigationDirection.Forward: - FocusFirst (); - - break; - case NavigationDirection.Backward: - FocusLast (); - - break; - default: - throw new ArgumentOutOfRangeException (nameof (direction), direction, null); + OnLeave (view); } - return Focused is { }; + SetNeedsDisplay (); } - var focusedFound = false; - - foreach (View w in direction == NavigationDirection.Forward - ? TabIndexes.ToArray () - : TabIndexes.ToArray ().Reverse ()) + // Remove focus down the chain of subviews if focus is removed + if (!newHasFocus && Focused is { }) { - if (w.HasFocus) - { - // A subview has focus, tell *it* to FocusNext - if (w.AdvanceFocus (direction, acrossGroupOrOverlapped)) - { - // The subview changed which of it's subviews had focus - return true; - } - else - { - if (acrossGroupOrOverlapped && Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - return false; - } - } - - //Debug.Assert (w.HasFocus); - - if (w.Focused is null) - { - // No next focusable view was found. - if (w.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - // Keep focus w/in w - return false; - } - } - // The subview has no subviews that can be next. Cache that we found a focused subview. - focusedFound = true; - - continue; - } - - // The subview does not have focus, but at least one other that can. Can this one be focused? - if (focusedFound && w.CanFocus && w.TabStop.HasFlag (TabStop.TabStop) && w.Visible && w.Enabled) - { - // Make Focused Leave - Focused.SetHasFocus (false, w); - - // If the focused view is overlapped don't focus on the next if it's not overlapped. - //if (acrossGroupOrOverlapped && Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)/* && !w.Arrangement.HasFlag (ViewArrangement.Overlapped)*/) - //{ - // return false; - //} - - // If the focused view is not overlapped and the next is, skip it - if (!acrossGroupOrOverlapped && !Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - continue; - } - - switch (direction) - { - case NavigationDirection.Forward: - w.FocusFirst (); - - break; - case NavigationDirection.Backward: - w.FocusLast (); - - break; - } - - SetFocus (w); - - return true; - } - } - - if (Focused is { }) - { - // Leave - Focused.SetHasFocus (false, this); - - // Signal that nothing is focused, and callers should try a peer-subview + View f = Focused; + f.OnLeave (view); + f.SetHasFocus (false, view); Focused = null; } - - return false; } #region Tab/Focus Handling +#nullable enable + private List _tabIndexes; // TODO: This should be a get-only property? @@ -667,19 +665,15 @@ public partial class View // Focus and cross-view navigation management (TabStop /// The tabIndexes. public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty; - // TODO: Change this to int? and use null to indicate the view has not yet been added to the tab order. - private int _tabIndex = -1; // -1 indicates the view has not yet been added to TabIndexes - private int _oldTabIndex; + 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. /// /// /// - /// If the value is -1, the view is not part of the tab order. - /// - /// - /// On set, if is , will be set to -1. + /// If , the view is not part of the tab order. /// /// /// On set, if is or has not TabStops, will @@ -689,30 +683,20 @@ public partial class View // Focus and cross-view navigation management (TabStop /// On set, if has only one TabStop, will be set to 0. /// /// - /// See also . + /// See also . /// /// - public int TabIndex + public int? TabIndex { get => _tabIndex; // TOOD: This should be a get-only property. Introduce SetTabIndex (int value) (or similar). set { - //// BUGBUG: Property setters should set the property to the value passed in and not have side effects. - //if (!CanFocus) - //{ - // // BUGBUG: Property setters should set the property to the value passed in and not have side effects. - // // BUGBUG: TabIndex = -1 should not be used to indicate that the view is not in the tab order. That's what TabStop is for. - // _tabIndex = -1; - - // return; - //} - - // Once a view is in the tab order, it should not be removed from the tab order; set TabStop to None instead. + // 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 {}); - // BUGBUG: Property setters should set the property to the value passed in and not have side effects. if (SuperView?._tabIndexes is null || SuperView?._tabIndexes.Count == 1) { // BUGBUG: Property setters should set the property to the value passed in and not have side effects. @@ -728,13 +712,13 @@ public partial class View // Focus and cross-view navigation management (TabStop _tabIndex = value > SuperView!.TabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : value < 0 ? 0 : value; - _tabIndex = GetGreatestTabIndexInSuperView (_tabIndex); + _tabIndex = GetGreatestTabIndexInSuperView ((int)_tabIndex); if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) { // 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 (_tabIndex, this); + SuperView._tabIndexes.Insert ((int)_tabIndex, this); ReorderSuperViewTabIndexes (); } } @@ -757,7 +741,7 @@ public partial class View // Focus and cross-view navigation management (TabStop foreach (View superViewTabStop in SuperView._tabIndexes) { - if (superViewTabStop._tabIndex == -1 || superViewTabStop == this) + if (superViewTabStop._tabIndex is null || superViewTabStop == this) { continue; } @@ -782,7 +766,7 @@ public partial class View // Focus and cross-view navigation management (TabStop foreach (View superViewTabStop in SuperView._tabIndexes) { - if (superViewTabStop._tabIndex == -1) + if (superViewTabStop._tabIndex is null) { continue; } @@ -792,22 +776,30 @@ public partial class View // Focus and cross-view navigation management (TabStop } } - private TabStop _tabStop = TabStop.None; + private TabBehavior? _tabStop; /// - /// Gets or sets whether the view is a stop-point for keyboard navigation. + /// Gets or sets the behavior of for keyboard navigation. /// /// - /// - /// TabStop is independent of . If is , the view will not gain - /// focus even if this property is set and vice-versa. - /// - /// - /// The default keyboard navigation keys are Key.Tab and Key>Tab.WithShift. These can be changed by - /// modifying the key bindings (see ) of the SuperView. - /// + /// + /// If the tab stop has not been set and setting to true will set it + /// to + /// . + /// + /// + /// TabStop is independent of . If is , the + /// view will not gain + /// 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.WithCtrl and Key>Key.Tab.WithCtrl.WithShift. + /// /// - public TabStop TabStop + public TabBehavior? TabStop { get => _tabStop; set @@ -816,14 +808,16 @@ public partial class View // Focus and cross-view navigation management (TabStop { return; } - _tabStop = value; - // If TabIndex is -1 it means this view has not yet been added to TabIndexes (TabStop has not been set previously). - if (TabIndex == -1) + Debug.Assert (value is { }); + + if (_tabStop is null && TabIndex is null) { - TabIndex = SuperView is { } ? SuperView._tabIndexes.Count : 0; + // This view has not yet been added to TabIndexes (TabStop has not been set previously). + TabIndex = GetGreatestTabIndexInSuperView(SuperView is { } ? SuperView._tabIndexes.Count : 0); } - ReorderSuperViewTabIndexes(); + + _tabStop = value; } } diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index a95634d9a..9977dc881 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -184,10 +184,6 @@ public partial class View : Responder, ISupportInitializeNotification //SetupMouse (); SetupText (); - - CanFocus = false; - //TabIndex = -1; - TabStop = TabStop.None; } /// diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index ac3967f24..1df87a210 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -29,7 +29,7 @@ public class ComboBox : View, IDesignable public ComboBox () { _search = new TextField (); - _listview = new ComboListView (this, HideDropdownListOnClick) { CanFocus = true, TabStop = TabStop.None }; + _listview = new ComboListView (this, HideDropdownListOnClick) { CanFocus = true }; _search.TextChanged += Search_Changed; _search.Accept += Search_Accept; @@ -329,9 +329,9 @@ public class ComboBox : View, IDesignable IsShow = false; HideList (); } - else if (_listview.TabStop.HasFlag (TabStop)) + else if (_listview.TabStop?.HasFlag (TabBehavior.TabStop) ?? false) { - _listview.TabStop = TabStop.None; + _listview.TabStop = TabBehavior.NoStop; } return base.OnLeave (view); @@ -455,7 +455,7 @@ public class ComboBox : View, IDesignable private void FocusSelectedItem () { _listview.SelectedItem = SelectedItem > -1 ? SelectedItem : 0; - _listview.TabStop = TabStop.TabStop; + _listview.TabStop = TabBehavior.TabStop; _listview.SetFocus (); OnExpanded (); } @@ -491,7 +491,7 @@ public class ComboBox : View, IDesignable Reset (true); _listview.Clear (); - _listview.TabStop = TabStop.None; + _listview.TabStop = TabBehavior.NoStop; SuperView?.SendSubviewToBack (this); Rectangle rect = _listview.ViewportToScreen (_listview.IsInitialized ? _listview.Viewport : Rectangle.Empty); SuperView?.SetNeedsDisplay (rect); @@ -505,7 +505,7 @@ public class ComboBox : View, IDesignable // jump to list if (_searchSet?.Count > 0) { - _listview.TabStop = TabStop.TabStop; + _listview.TabStop = TabBehavior.TabStop; _listview.SetFocus (); if (_listview.SelectedItem > -1) @@ -519,7 +519,7 @@ public class ComboBox : View, IDesignable } else { - _listview.TabStop = TabStop.None; + _listview.TabStop = TabBehavior.NoStop; SuperView?.AdvanceFocus (NavigationDirection.Forward); } @@ -721,7 +721,7 @@ public class ComboBox : View, IDesignable private void Selected () { IsShow = false; - _listview.TabStop = TabStop.None; + _listview.TabStop = TabBehavior.NoStop; if (_listview.Source.Count == 0 || (_searchSet?.Count ?? 0) == 0) { diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 94d6957c1..20ba03dda 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -464,8 +464,8 @@ public class FileDialog : Dialog _btnOk.X = Pos.Right (_btnCancel) + 1; // Flip tab order too for consistency - int p1 = _btnOk.TabIndex; - int p2 = _btnCancel.TabIndex; + int? p1 = _btnOk.TabIndex; + int? p2 = _btnCancel.TabIndex; _btnOk.TabIndex = p2; _btnCancel.TabIndex = p1; @@ -513,7 +513,7 @@ public class FileDialog : Dialog // TODO: Does not work, if this worked then we could tab to it instead // of having to hit F9 CanFocus = true, - TabStop = TabStop.TabStop, + TabStop = TabBehavior.TabStop, Menus = [_allowedTypeMenu] }; AllowedTypeMenuClicked (0); diff --git a/Terminal.Gui/Views/TileView.cs b/Terminal.Gui/Views/TileView.cs index f6e168865..cc27c00b3 100644 --- a/Terminal.Gui/Views/TileView.cs +++ b/Terminal.Gui/Views/TileView.cs @@ -871,7 +871,7 @@ public class TileView : View public TileViewLineView (TileView parent, int idx) { CanFocus = false; - TabStop = TabStop.TabStop; + TabStop = TabBehavior.TabStop; Parent = parent; Idx = idx; diff --git a/UICatalog/Scenarios/Buttons.cs b/UICatalog/Scenarios/Buttons.cs index 124286347..5685913aa 100644 --- a/UICatalog/Scenarios/Buttons.cs +++ b/UICatalog/Scenarios/Buttons.cs @@ -22,7 +22,7 @@ public class Buttons : Scenario }; // Add a label & text field so we can demo IsDefault - var editLabel = new Label { X = 0, Y = 0, TabStop = TabStop.TabStop, Text = "TextField (to demo IsDefault):" }; + var editLabel = new Label { X = 0, Y = 0, Text = "TextField (to demo IsDefault):" }; main.Add (editLabel); // Add a TextField using Absolute layout. diff --git a/UnitTests/View/NavigationTests.cs b/UnitTests/View/NavigationTests.cs index 0c88372ef..7e38c7f63 100644 --- a/UnitTests/View/NavigationTests.cs +++ b/UnitTests/View/NavigationTests.cs @@ -253,12 +253,12 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews v2.CanFocus = true; Assert.Equal (r.TabIndexes.IndexOf (v2), v2.TabIndex); Assert.Equal (0, v2.TabIndex); - Assert.Equal (TabStop.TabStop, v2.TabStop); + Assert.Equal (TabBehavior.TabStop, v2.TabStop); v1.CanFocus = true; Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); Assert.Equal (1, v1.TabIndex); - Assert.Equal (TabStop.TabStop, v1.TabStop); + Assert.Equal (TabBehavior.TabStop, v1.TabStop); v1.TabIndex = 2; Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); @@ -268,18 +268,18 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews Assert.Equal (1, v1.TabIndex); Assert.Equal (r.TabIndexes.IndexOf (v3), v3.TabIndex); Assert.Equal (2, v3.TabIndex); - Assert.Equal (TabStop.TabStop, v3.TabStop); + Assert.Equal (TabBehavior.TabStop, v3.TabStop); v2.CanFocus = false; Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); Assert.Equal (1, v1.TabIndex); - Assert.Equal (TabStop.TabStop, v1.TabStop); + 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 (TabStop.TabStop, v2.TabStop); // TabStop is not changed + 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 (TabStop.TabStop, v3.TabStop); + Assert.Equal (TabBehavior.TabStop, v3.TabStop); r.Dispose (); } @@ -1373,9 +1373,9 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews public void TabStop_All_False_And_All_True_And_Changing_TabStop_Later () { var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabStop.None }; - var v2 = new View { CanFocus = true, TabStop = TabStop.None }; - var v3 = new View { CanFocus = true, TabStop = TabStop.None }; + 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); @@ -1384,17 +1384,17 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); - v1.TabStop = TabStop.TabStop; + v1.TabStop = TabBehavior.TabStop; r.AdvanceFocus (NavigationDirection.Forward); Assert.True (v1.HasFocus); Assert.False (v2.HasFocus); Assert.False (v3.HasFocus); - v2.TabStop = TabStop.TabStop; + v2.TabStop = TabBehavior.TabStop; r.AdvanceFocus (NavigationDirection.Forward); Assert.False (v1.HasFocus); Assert.True (v2.HasFocus); Assert.False (v3.HasFocus); - v3.TabStop = TabStop.TabStop; + v3.TabStop = TabBehavior.TabStop; r.AdvanceFocus (NavigationDirection.Forward); Assert.False (v1.HasFocus); Assert.False (v2.HasFocus); @@ -1464,9 +1464,9 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews public void TabStop_And_CanFocus_Mixed_And_BothFalse () { var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabStop.None }; - var v2 = new View { CanFocus = false, TabStop = TabStop.TabStop }; - var v3 = new View { CanFocus = false, TabStop = TabStop.None }; + 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); @@ -1489,9 +1489,9 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews public void TabStop_Are_All_False_And_CanFocus_Are_All_True () { var r = new View (); - var v1 = new View { CanFocus = true, TabStop = TabStop.None }; - var v2 = new View { CanFocus = true, TabStop = TabStop.None }; - var v3 = new View { CanFocus = true, TabStop = TabStop.None }; + 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); @@ -1537,7 +1537,7 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews [Theory] [CombinatorialData] - public void TabStop_And_CanFocus_Are_Decoupled (bool canFocus, TabStop tabStop) + public void TabStop_And_CanFocus_Are_Decoupled (bool canFocus, TabBehavior tabStop) { var view = new View { CanFocus = canFocus, TabStop = tabStop };