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 };