#nullable enable using System.Diagnostics; using System.Reflection.PortableExecutable; namespace Terminal.Gui; public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...) { private bool _canFocus; /// /// Advances the focus to the next or previous view in the focus chain, based on /// . /// itself. /// /// /// /// If there is no next/previous view to advance to, the focus is set to the view itself. /// /// /// See the View Navigation Deep Dive for more information: /// /// /// /// /// /// if focus was changed to another subview (or stayed on this one), /// otherwise. /// public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior) { if (!CanBeVisible (this)) // TODO: is this check needed? { return false; } View? focused = Focused; if (focused is { } && focused.AdvanceFocus (direction, behavior)) { return true; } // AdvanceFocus did not advance - do we wrap, or move up to the superview? View [] focusChain = GetFocusChain (direction, behavior); if (focusChain.Length == 0) { return false; } // Special case TabGroup if (behavior == TabBehavior.TabGroup) { if (direction == NavigationDirection.Forward && focused == focusChain [^1] && SuperView is null) { // We're at the top of the focus chain. Go back down the focus chain and focus the first TabGroup View [] views = GetFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup); if (views.Length > 0) { View [] subViews = views [0].GetFocusChain (NavigationDirection.Forward, TabBehavior.TabStop); if (subViews.Length > 0) { if (subViews [0].SetFocus ()) { return true; } } } } if (direction == NavigationDirection.Backward && focused == focusChain [0]) { // We're at the bottom of the focus chain View [] views = GetFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup); if (views.Length > 0) { View [] subViews = views [^1].GetFocusChain (NavigationDirection.Forward, TabBehavior.TabStop); if (subViews.Length > 0) { if (subViews [0].SetFocus ()) { return true; } } } } } int focusedIndex = focusChain.IndexOf (Focused); // Will return -1 if Focused can't be found or is null var next = 0; // Assume we wrap to start of the focus chain if (focusedIndex < focusChain.Length - 1) { // We're moving w/in the subviews next = focusedIndex + 1; } else { // Determine if focus should remain in this focus chain, or move to the superview's focus chain if (SuperView is { }) { // If we are TabStop, and we have at least one other focusable peer, move to the SuperView's chain if (TabStop == TabBehavior.TabStop && SuperView is { } && SuperView.GetFocusChain (direction, behavior).Length > 1) { return false; } // TabGroup is special-cased. if (focused?.TabStop == TabBehavior.TabGroup) { if (SuperView?.GetFocusChain (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 = focusChain [next]; if (view.HasFocus) { // We could not advance return view == this; } // 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 focusSet; } /// Gets or sets a value indicating whether this can be focused. /// /// /// See the View Navigation Deep Dive for more information: /// /// /// 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 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 property to will cause to be set to /// " as a convenience. Changing this property to /// will have no effect on . /// /// public bool CanFocus { get => _canFocus; set { if (_canFocus == value) { return; } _canFocus = value; if (TabStop is null && _canFocus) { TabStop = TabBehavior.TabStop; } if (!_canFocus && HasFocus) { // If CanFocus is set to false and this view has focus, make it leave focus HasFocus = false; } if (_canFocus && !HasFocus && Visible && SuperView is { Focused: null }) { // If CanFocus is set to true and this view does not have focus, make it enter focus SetFocus (); } OnCanFocusChanged (); } } /// Raised when has been changed. /// /// Raised by the virtual method. /// public event EventHandler? CanFocusChanged; /// /// Focuses the deepest focusable Subview if one exists. If there are no focusable Subviews then the focus is set to /// the view itself. /// /// /// /// if a subview other than this was focused. public bool FocusDeepest (NavigationDirection direction, TabBehavior? behavior) { View? deepest = FindDeepestFocusableView (direction, behavior); if (deepest is { }) { return deepest.SetFocus (); } return SetFocus (); } /// Gets the currently focused Subview or Adornment of this view, or if nothing is focused. public View? Focused { get { View? focused = Subviews.FirstOrDefault (v => v.HasFocus); if (focused is { }) { return focused; } // How about in Adornments? if (Margin is { HasFocus: true }) { return Margin; } if (Border is { HasFocus: true }) { return Border; } if (Padding is { HasFocus: true }) { return Padding; } return null; } } /// Returns a value indicating if this View is currently on Top (Active) public bool IsCurrentTop => Application.Top == this; /// /// Returns the most focused Subview down the subview-hierarchy. /// /// 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; 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); } /// /// 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 () { View [] indicies = GetFocusChain (NavigationDirection.Forward, TabStop); if (Focused is null && _previouslyFocused is { } && indicies.Contains (_previouslyFocused)) { return _previouslyFocused.SetFocus (); } return false; } private View? FindDeepestFocusableView (NavigationDirection direction, TabBehavior? behavior) { View [] indicies = GetFocusChain (direction, behavior); foreach (View v in indicies) { return v.FindDeepestFocusableView (direction, behavior); } return null; } #region HasFocus // Backs `HasFocus` and is the ultimate source of truth whether a View has focus or not. private bool _hasFocus; /// /// Gets or sets whether this view has focus. /// /// /// /// See the View Navigation Deep Dive for more information: /// /// /// 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 { set { if (HasFocus == 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 { SetHasFocusFalse (null); if (_hasFocus) { // force it. _hasFocus = false; } } } get => _hasFocus; } /// /// 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. /// /// /// /// See the View Navigation Deep Dive for more information: /// /// public bool SetFocus () { (bool focusSet, bool _) = SetHasFocusTrue (Application.Navigation?.GetFocused ()); return focusSet; } /// /// A cache of the subview that was focused when this view last lost focus. This is used by . /// private View? _previouslyFocused; /// /// 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. /// /// /// 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) { Debug.Assert (ApplicationNavigation.IsInHierarchy (SuperView, this)); // Pre-conditions if (_hasFocus) { return (false, false); } var thisAsAdornment = this as Adornment; View? superViewOrParent = thisAsAdornment?.Parent ?? SuperView; if (CanFocus && superViewOrParent is { CanFocus: false }) { Debug.WriteLine ($@"WARNING: Attempt to FocusChanging where SuperView.CanFocus == false. {this}"); return (false, false); } if (!CanBeVisible (this) || !Enabled) { return (false, false); } if (!CanFocus) { return (false, false); } bool previousValue = HasFocus; bool cancelled = NotifyFocusChanging (false, true, previousFocusedView, this); if (cancelled) { return (false, true); } // 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 (superViewOrParent is { HasFocus: false } sv) { (bool focusSet, bool svCancelled) = sv.SetHasFocusTrue (previousFocusedView, true); if (!focusSet) { return (false, svCancelled); } } if (_hasFocus) { // Something else beat us to the change (likely a FocusChanged handler). return (true, false); } // By setting _hasFocus to true we definitively change HasFocus for this view. // Get whatever peer has focus, if any View? focusedPeer = superViewOrParent?.Focused; _hasFocus = true; // Ensure that the peer loses focus focusedPeer?.SetHasFocusFalse (this, true); if (!traversingUp) { // Restore focus to the previously focused subview if (!RestoreFocus ()) { // 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 Application.Navigation?.SetFocused (this); } } } if (previousFocusedView is { HasFocus: true } && GetFocusChain (NavigationDirection.Forward, TabStop).Contains (previousFocusedView)) { previousFocusedView.SetHasFocusFalse (this); } _previouslyFocused = null; if (Arrangement.HasFlag (ViewArrangement.Overlapped)) { SuperView?.MoveSubviewToEnd (this); } NotifyFocusChanged (HasFocus, previousFocusedView, this); SetNeedsDisplay (); // Post-conditions - prove correctness if (HasFocus == previousValue) { throw new InvalidOperationException ("NotifyFocusChanging was not cancelled and the HasFocus value did not change."); } return (true, false); } private bool NotifyFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) { // Call the virtual method if (OnHasFocusChanging (currentHasFocus, newHasFocus, currentFocused, newFocused)) { // The event was cancelled return true; } var args = new HasFocusEventArgs (currentHasFocus, newHasFocus, currentFocused, newFocused); HasFocusChanging?.Invoke (this, args); if (args.Cancel) { // The event was cancelled return true; } return false; } /// /// Invoked when is about to change. This method is called before the /// event is raised. /// /// /// /// 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. /// /// /// /// Cancel the event to prevent the focus from changing. /// /// /// Use to be notified after the focus has changed. /// /// public event EventHandler? HasFocusChanging; /// /// 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) { throw new InvalidOperationException ("SetHasFocusFalse should not be called if the view does not have focus."); } var thisAsAdornment = this as Adornment; View? superViewOrParent = thisAsAdornment?.Parent ?? SuperView; // If newFocusedVew is null, we need to find the view that should get focus, and SetFocus on it. if (!traversingDown && newFocusedView is null) { if (superViewOrParent?._previouslyFocused is { }) { if (superViewOrParent._previouslyFocused != this) { superViewOrParent?._previouslyFocused?.SetFocus (); // The above will cause SetHasFocusFalse, so we can return return; } } if (superViewOrParent is { }) { if (superViewOrParent.AdvanceFocus (NavigationDirection.Forward, TabStop)) { // The above will cause SetHasFocusFalse, so we can return return; } newFocusedView = superViewOrParent; } if (Application.Navigation is { } && Application.Top is { }) { // Temporarily ensure this view can't get focus bool prevCanFocus = _canFocus; _canFocus = false; bool restoredFocus = Application.Top!.RestoreFocus (); _canFocus = prevCanFocus; if (restoredFocus) { // The above caused SetHasFocusFalse, so we can return return; } } // No other focusable view to be found. Just "leave" us... } // 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)) { // 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); Debug.Assert (_hasFocus); } bottom = bottom.SuperView; } if (bottom == this && bottom.SuperView is Adornment a) { //a.SetHasFocusFalse (newFocusedView, true); Debug.Assert (_hasFocus); } Debug.Assert (_hasFocus); } if (superViewOrParent is { }) { superViewOrParent._previouslyFocused = this; } bool previousValue = HasFocus; // Note, can't be cancelled. NotifyFocusChanging (HasFocus, !HasFocus, newFocusedView, this); // Get whatever peer has focus, if any so we can update our superview's _previouslyMostFocused View? focusedPeer = superViewOrParent?.Focused; // Set HasFocus false _hasFocus = false; if (Application.Navigation is { }) { View? appFocused = Application.Navigation.GetFocused (); if (appFocused is { } || appFocused == this) { Application.Navigation.SetFocused (newFocusedView ?? superViewOrParent); } } NotifyFocusChanged (HasFocus, this, newFocusedView); if (_hasFocus) { // Notify caused HasFocus to change to true. return; } // Post-conditions - prove correctness if (HasFocus == previousValue) { throw new InvalidOperationException ("SetHasFocusFalse and the HasFocus value did not change."); } SetNeedsDisplay (); } private void NotifyFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) { // 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; #endregion HasFocus #region Tab/Focus Handling /// /// Gets the subviews and Adornments of this view that are scoped to the specified behavior and direction. If behavior is null, all focusable subviews and /// Adornments are returned. /// /// /// /// internal View [] GetFocusChain (NavigationDirection direction, TabBehavior? behavior) { IEnumerable? filteredSubviews; if (behavior.HasValue) { filteredSubviews = _subviews?.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true }); } else { filteredSubviews = _subviews?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true }); } // How about in Adornments? if (Padding is { CanFocus: true, Visible: true, Enabled: true } && Padding.TabStop == behavior) { filteredSubviews = filteredSubviews?.Append (Padding); } if (Border is { CanFocus: true, Visible: true, Enabled: true } && Border.TabStop == behavior) { filteredSubviews = filteredSubviews?.Append (Border); } if (Margin is { CanFocus: true, Visible: true, Enabled: true } && Margin.TabStop == behavior) { filteredSubviews = filteredSubviews?.Append (Margin); } if (direction == NavigationDirection.Backward) { filteredSubviews = filteredSubviews?.Reverse (); } return filteredSubviews?.ToArray () ?? Array.Empty (); } private TabBehavior? _tabStop; /// /// Gets or sets the behavior of for keyboard navigation. /// /// /// /// /// See the View Navigation Deep Dive for more information: /// /// /// /// 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.F6) and (Key>Key.F6.WithShift). /// /// public TabBehavior? TabStop { get => _tabStop; set { if (_tabStop is { } && _tabStop == value) { return; } _tabStop = value; } } #endregion Tab/Focus Handling }