using System.Diagnostics; using System.Reflection.Metadata.Ecma335; using Microsoft.CodeAnalysis.Operations; using static Terminal.Gui.FakeDriver; namespace Terminal.Gui; public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...) { #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. /// /// /// /// 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) { if (value) { // NOTE: If Application.Navigation is null, we pass null to FocusChanging. For unit tests. if (FocusChanging (Application.Navigation?.GetFocused ())) { // The change happened // HasFocus is now true } } else { FocusChanged (null); } } } 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. /// public bool SetFocus () { return FocusChanging (Application.Navigation?.GetFocused ()); } /// /// 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 FocusChanging ([CanBeNull] View previousFocusedView, bool traversingUp = false) { Debug.Assert (ApplicationNavigation.IsInHierarchy (SuperView, this)); // Pre-conditions if (_hasFocus) { return false; } if (CanFocus && SuperView is { CanFocus: false }) { Debug.WriteLine($@"WARNING: Attempt to FocusChanging where SuperView.CanFocus == false. {this}"); return false; } if (!CanBeVisible (this) || !Enabled) { return false; } if (!CanFocus) { return false; } bool previousValue = HasFocus; if (!traversingUp) { if (NotifyFocusChanging (previousFocusedView)) { return false; } // If we're here, we can be focused. But we may have subviews. // Restore focus to the previously most focused subview in the subview-hierarchy if (RestoreFocus (TabStop)) { // A subview was focused. We're done because the subview has focus and it recursed up the superview hierarchy. return true; } // Couldn't restore focus, so use Advance to navigate to the next focusable subview if (AdvanceFocus (NavigationDirection.Forward, null)) { // A subview was focused. We're done because the subview has focus and it recursed up the superview hierarchy. return true; } } // If we're here, we're the most-focusable view in the application OR we're traversing up the superview hierarchy. // If we previously had a subview with focus (`Focused = subview`), we need to make sure that all subviews down the `subview`-hierarchy LeaveFocus. // LeaveFocus will recurse down the subview hierarchy and will also set PreviouslyMostFocused View focused = Focused; focused?.FocusChanged (this, true); // We need to ensure all superviews up the superview hierarchy have focus. // Any of them may cancel gaining focus. In which case we need to back out. if (SuperView is { HasFocus: false } sv) { // Tell EnterFocus that we're traversing up the superview hierarchy if (!sv.FocusChanging (previousFocusedView, true)) { // The change was cancelled return false; } } // If we're here: // - we're the most-focusable view in the application // - all superviews up the superview hierarchy have focus. // - By setting _hasFocus to true we definitively change HasFocus for this view. // Get whatever peer has focus, if any View focusedPeer = SuperView?.Focused; _hasFocus = true; // Ensure that the peer loses focus focusedPeer?.FocusChanged (this, true); // We're the most focused view in the application, we need to set the focused view to this view. Application.Navigation?.SetFocused (this); SetNeedsDisplay (); // Post-conditions - prove correctness if (HasFocus == previousValue) { throw new InvalidOperationException ($"FocusChanging was not cancelled and the HasFocus value did not change."); } return true; } private bool NotifyFocusChanging (View leavingView) { // Call the virtual method if (OnHasFocusChanging (leavingView)) { // The event was cancelled return true; } var args = new FocusEventArgs (leavingView, this); HasFocusChanging?.Invoke (this, args); if (args.Cancel) { // The event was cancelled return true; } return false; } /// Virtual method invoked when the focus is changing to this View. /// The view that is currently Focused. May be . /// , if the event is to be cancelled, otherwise. protected virtual bool OnHasFocusChanging ([CanBeNull] View currentlyFocusedView) { return false; } /// Raised when the view is gaining (entering) focus. Can be cancelled. public event EventHandler HasFocusChanging; /// /// Called when focus has changed to another view. /// /// The view that now has focus. If there is no view that has focus. /// private void FocusChanged ([CanBeNull] View focusedVew, bool traversingDown = false) { // Pre-conditions if (!_hasFocus) { throw new InvalidOperationException ($"FocusChanged should not be called if the view does not have focus."); } // If enteringView is null, we need to find the view that should get focus, and SetFocus on it. if (!traversingDown && focusedVew is null) { if (SuperView?._previouslyMostFocused is { } && SuperView?._previouslyMostFocused != this) { SuperView?._previouslyMostFocused?.SetFocus (); // The above will cause FocusChanged, so we can return return; } if (SuperView is {} && SuperView.AdvanceFocus (NavigationDirection.Forward, TabStop)) { // The above will cause FocusChanged, so we can return return; } //if (Application.Navigation is { }) //{ // // Temporarily ensure this view can't get focus // bool prevCanFocus = _canFocus; // _canFocus = false; // ApplicationNavigation.MoveNextView (); // _canFocus = prevCanFocus; // // The above will cause LeaveFocus, 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 { } && (focusedVew is null || mostFocused != focusedVew)) { // Start at the bottom and work our way up to us View bottom = mostFocused; while (bottom is { } && bottom != this) { if (bottom.HasFocus) { bottom.FocusChanged (focusedVew, true); } bottom = bottom.SuperView; } _previouslyMostFocused = mostFocused; } bool previousValue = HasFocus; // Call the virtual method - NOTE: Leave cannot be cancelled OnHasFocusChanged (focusedVew); var args = new FocusEventArgs (focusedVew, this); HasFocusChanged?.Invoke (this, args); // Get whatever peer has focus, if any View focusedPeer = SuperView?.Focused; _hasFocus = false; if (!traversingDown && CanFocus && Visible && Enabled) { // Now ensure all views up the superview-hierarchy are unfocused if (SuperView is { HasFocus: true } && focusedPeer == this) { SuperView.FocusChanged (focusedVew); } } // Post-conditions - prove correctness if (HasFocus == previousValue) { throw new InvalidOperationException ($"LeaveFocus and the HasFocus value did not change."); } SetNeedsDisplay (); } /// /// Caches the most focused subview when this view is losing focus. This is used by . /// [CanBeNull] private View _previouslyMostFocused; /// Virtual method invoked after another view gets focus. May be . /// The view is now focused. protected virtual void OnHasFocusChanged ([CanBeNull] View focusedVew) { return; } /// Raised when the view is gaining (entering) focus. Can not be cancelled. public event EventHandler HasFocusChanged; #endregion HasFocus /// /// 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 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; } if (TabIndexes is null || TabIndexes.Count == 0) { return false; } View focused = Focused; if (focused is {} && focused.AdvanceFocus (direction, behavior)) { return true; } View[] index = GetScopedTabIndexes (direction, behavior); if (index.Length == 0) { return false; } var focusedIndex = index.IndexOf (Focused); int next = 0; if (focusedIndex < index.Length - 1) { next = focusedIndex + 1; } else { if (behavior == TabBehavior.TabGroup && behavior == TabStop && SuperView?.TabStop == TabBehavior.TabGroup) { // Go down the subview-hierarchy and leave // BUGBUG: This doesn't seem right Focused.HasFocus = false; // TODO: Should we check the return value of SetHasFocus? return false; } } View view = index [next]; if (view.HasFocus) { // We could not advance return true; } // The subview does not have focus, but at least one other that can. Can this one be focused? return view.FocusChanging (Focused); } /// /// 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 (TabBehavior? behavior) { if (Focused is null && _subviews?.Count > 0) { if (_previouslyMostFocused is { }/* && (behavior is null || _previouslyMostFocused.TabStop == behavior)*/) { return _previouslyMostFocused.SetFocus (); } return false; } return false; } /// /// 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; } } ///// ///// Internal API that causes to enter focus. ///// must be a subview. ///// Recursively sets focus up the superview hierarchy. ///// ///// ///// if got focus. //private bool SetFocus (View viewToEnterFocus) //{ // if (viewToEnterFocus is null) // { // return false; // } // if (!viewToEnterFocus.CanFocus || !viewToEnterFocus.Visible || !viewToEnterFocus.Enabled) // { // return false; // } // // If viewToEnterFocus is already the focused view, don't do anything // if (Focused?._hasFocus == true && Focused == viewToEnterFocus) // { // return false; // } // // If a subview has focus and viewToEnterFocus is the focused view's superview OR viewToEnterFocus is this view, // // then make viewToEnterFocus.HasFocus = true and return // if ((Focused?._hasFocus == true && Focused?.SuperView == viewToEnterFocus) || viewToEnterFocus == this) // { // if (!viewToEnterFocus._hasFocus) // { // viewToEnterFocus._hasFocus = true; // } // // viewToEnterFocus is already focused // return true; // } // // Make sure that viewToEnterFocus is a subview of this view // View c; // for (c = viewToEnterFocus._superView; c != null; c = c._superView) // { // if (c == this) // { // break; // } // } // if (c is null) // { // throw new ArgumentException (@$"The specified view {viewToEnterFocus} is not part of the hierarchy of {this}."); // } // // If a subview has focus, make it leave focus. This will leave focus up the hierarchy. // Focused?.SetHasFocus (false, viewToEnterFocus); // // make viewToEnterFocus Focused and enter focus // View f = Focused; // Focused = viewToEnterFocus; // Focused?.SetHasFocus (true, f, true); // Focused?.FocusDeepest (null, NavigationDirection.Forward); // // Recursively set focus up the superview hierarchy // if (SuperView is { }) // { // // BUGBUG: If focus is cancelled at any point, we should stop and restore focus to the previous focused view // SuperView.SetFocus (this); // } // else // { // // BUGBUG: this makes no sense in the new design // // If there is no SuperView, then this is a top-level view // SetFocus (this); // } // // TODO: Temporary hack to make Application.Navigation.FocusChanged work // if (HasFocus && Focused.Focused is null) // { // Application.Navigation?.SetFocused (Focused); // } // // TODO: This is a temporary hack to make overlapped non-Toplevels have a zorder. See also: View.OnDrawContent. // if (viewToEnterFocus is { } && (viewToEnterFocus.TabStop == TabBehavior.TabGroup && viewToEnterFocus.Arrangement.HasFlag (ViewArrangement.Overlapped))) // { // viewToEnterFocus.TabIndex = 0; // } // return true; //} #if AUTO_CANFOCUS // 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; #endif 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 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 { } && SuperView.Focused is 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; /// Gets the currently focused Subview of this view, or if nothing is focused. [CanBeNull] public View Focused { get { return Subviews.FirstOrDefault (v => v.HasFocus); } } /// /// Focuses the deepest focusable view in if one exists. If there are no views in /// 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 (); } [CanBeNull] private View FindDeepestFocusableView (NavigationDirection direction, TabBehavior? behavior) { var indicies = GetScopedTabIndexes (direction, behavior); foreach (View v in indicies) { if (v.TabIndexes.Count == 0) { return v; } return v.FindDeepestFocusableView (direction, behavior); } return null; } /// Returns a value indicating if this View is currently on Top (Active) public bool IsCurrentTop => Application.Current == this; /// Invoked when the property from a view is changed. /// /// Raises the event. /// public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); } #region Tab/Focus Handling #nullable enable private List? _tabIndexes; // TODO: This should be a get-only property? // BUGBUG: This returns an AsReadOnly list, but isn't declared as such. /// Gets a list of the subviews that are a . /// The tabIndexes. public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty; /// /// Gets TabIndexes that are scoped to the specified behavior and direction. If behavior is null, all TabIndexes are returned. /// /// /// /// GetScopedTabIndexes private View [] GetScopedTabIndexes (NavigationDirection direction, TabBehavior? behavior) { IEnumerable? indicies; if (behavior.HasValue) { indicies = _tabIndexes?.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true }); } else { indicies = _tabIndexes?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true }); } if (direction == NavigationDirection.Backward) { indicies = indicies?.Reverse (); } return indicies?.ToArray () ?? Array.Empty (); } 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 view is not part of the tab order. /// /// /// On set, if is or has not TabStops, will /// be set to 0. /// /// /// On set, if has only one TabStop, will be set to 0. /// /// /// See also . /// /// public int? TabIndex { get => _tabIndex; // TOOD: This should be a get-only property. Introduce SetTabIndex (int value) (or similar). set { // 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 { }); 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. _tabIndex = 0; return; } if (_tabIndex == value && TabIndexes.IndexOf (this) == value) { return; } _tabIndex = value > SuperView!.TabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : value < 0 ? 0 : value; _tabIndex = GetGreatestTabIndexInSuperView ((int)_tabIndex); if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) { // BUGBUG: we have to use _tabIndexes and not TabIndexes because TabIndexes returns is a read-only version of _tabIndexes SuperView._tabIndexes.Remove (this); SuperView._tabIndexes.Insert ((int)_tabIndex, this); UpdatePeerTabIndexes (); } return; // Updates the s of the views in the 's to match their order in . void UpdatePeerTabIndexes () { if (SuperView is null) { return; } var i = 0; foreach (View superViewTabStop in SuperView._tabIndexes) { if (superViewTabStop._tabIndex is null) { continue; } superViewTabStop._tabIndex = i; i++; } } } } /// /// Gets the greatest of the 's that is less /// than or equal to . /// /// /// The minimum of and the 's . private int GetGreatestTabIndexInSuperView (int idx) { if (SuperView is null) { return 0; } var i = 0; foreach (View superViewTabStop in SuperView._tabIndexes) { if (superViewTabStop._tabIndex is null || superViewTabStop == this) { continue; } i++; } return Math.Min (i, idx); } private TabBehavior? _tabStop; /// /// Gets or sets the behavior of for keyboard navigation. /// /// /// /// 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 == value) { return; } Debug.Assert (value is { }); if (_tabStop is null && TabIndex is null) { // This view has not yet been added to TabIndexes (TabStop has not been set previously). TabIndex = GetGreatestTabIndexInSuperView (SuperView is { } ? SuperView._tabIndexes.Count : 0); } _tabStop = value; } } #endregion Tab/Focus Handling }