using System.Diagnostics; 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 recursively set to /// for any focused subviews. /// /// public bool HasFocus { set => SetHasFocus (value, this); get => _hasFocus; } /// /// Causes this view to be focused. Calling this method has the same effect as setting to /// . /// public void SetFocus () { HasFocus = true; } /// /// 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. /// /// If is and there is a focused subview ( /// is not ), /// this method will recursively remove focus from any focused subviews of . /// private bool SetHasFocus (bool newHasFocus, View view) { if (HasFocus != newHasFocus) { if (newHasFocus) { Debug.Assert (view is null || SuperView is null || ApplicationNavigation.IsInHierarchy (SuperView, view)); if (EnterFocus (view)) { // The change happened // HasFocus is now true } else { // The event was cancelled or view is not focusable return false; } } else { if (LeaveFocus (view)) { // The change happened // HasFocus is now false } else { // The event was cancelled or view was not focused return false; } } SetNeedsDisplay (); } return true; } /// /// Called when view is entering focus. 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 EnterFocus ([CanBeNull] View leavingView) { // Pre-conditions if (_hasFocus) { throw new InvalidOperationException ($"EnterFocus should not be called if the view already has focus."); } if (CanFocus && SuperView?.CanFocus == false) { throw new InvalidOperationException ($"It is not possible to EnterFocus if the View's SuperView has CanFocus = false."); } if (!CanBeVisible (this) || !Enabled) { return false; } if (!CanFocus) { return false; } // Verify all views up the superview-hierarchy are focusable. if (SuperView is { }) { View super = SuperView; while (super is { }) { // TODO: Check Visible & Enabled too! if (!super.CanFocus) { return false; } super = super.SuperView; } } bool previousValue = HasFocus; // Call the virtual method if (OnEnter (leavingView)) { // The event was cancelled return false; } var args = new FocusEventArgs (leavingView, this); Enter?.Invoke (this, args); if (args.Cancel) { // The event was cancelled return false; } // If we're here, we can be focused. But we may have subviews. // Restore focus to the most focused subview in the subview-hierarchy if (!RestoreFocus (TabStop)) { // Couldn't restore focus, so use Advance to navigate to the next focusable subview if (!AdvanceFocus (NavigationDirection.Forward, TabStop)) { // Couldn't advance, so we're the most-focusable view in the application // We now need to ensure all views up the superview hierarchy have focus. // Any of them may cancel gaining focus. In which case we need to back out. // All views down the superview-hierarchy need to have HasFocus == true // PLACEHOLDER:But just using SetHasFocus on them will infinitely recurse. So we need to do it manually. if (SuperView is { HasFocus: false } super) { if (!super.SetHasFocus (true, this)) { // The change was cancelled return false; } SuperView.Focused = this; } Application.Navigation?.SetFocused (this); } } // Now, these things need to be true for us to set _hasFocus to true: // All views down v's view-hierarchy need to gain focus, if they don't already have it. // Any of them may cancel gaining focus, so we need to do that first. _hasFocus = true; // By setting _hasFocus to true we've definitively changed HasFocus for this view. // But we can back out below if a subview or superview cancels gaining focus. // All views down the superview-hierarchy need to have HasFocus == true if (SuperView is { }) { SuperView.Focused = this; } // Post-conditions - prove correctness if (HasFocus == previousValue) { throw new InvalidOperationException ($"EnterFocus was not cancelled and the HasFocus value did not change."); } return true; } /// Virtual method invoked when this view is gaining focus (entering). /// The view that is leaving focus. /// , if the event is to be cancelled, otherwise. protected virtual bool OnEnter ([CanBeNull] View leavingView) { return false; } /// Raised when the view is gaining (entering) focus. Can be cancelled. /// /// Raised by . /// public event EventHandler Enter; // TODO: Leave does not need to be cancelable. That's overkill. Just make it so that it can't be canceled. /// /// Called when view is entering focus. 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. /// private bool LeaveFocus ([CanBeNull] View enteringView) { // Pre-conditions if (_hasFocus) { throw new InvalidOperationException ($"LeaveFocus should not be called if the view does not have focus."); } // Before we can leave focus, we need to make sure that all views down the subview-hierarchy have left focus. // Any of them may cancel losing focus, so we need to do that first. if (Application.Navigation?.GetFocused () != this) { // Save the most focused view in the subview-hierarchy View originalBottom = Application.Navigation?.GetFocused (); // Start at the bottom and work our way up to us View bottom = originalBottom; while (bottom is { } && bottom != this) { if (bottom.HasFocus && !bottom.SetHasFocus (false, enteringView)) { // The change was cancelled return false; } bottom = bottom.SuperView; } PreviouslyMostFocused = originalBottom; } bool previousValue = HasFocus; // Call the virtual method if (OnLeave (enteringView)) { // The event was cancelled return false; } var args = new FocusEventArgs (enteringView, this); Leave?.Invoke (this, args); if (args.Cancel) { // The event was cancelled return false; } Focused = null; if (SuperView is { }) { SuperView.Focused = null; } _hasFocus = false; if (Application.Navigation?.GetFocused () != this) { PreviouslyMostFocused = null; if (SuperView is { }) { SuperView.PreviouslyMostFocused = this; } } // Post-conditions - prove correctness if (HasFocus == previousValue) { throw new InvalidOperationException ($"LeaveFocus was not cancelled and the HasFocus value did not change."); } return true; } /// /// Caches the most focused subview when this view is losing focus. This is used by . /// [CanBeNull] internal View PreviouslyMostFocused { get; set; } /// Virtual method invoked when this view is losing focus (leaving). /// The view that is gaining focus. /// , if the event is to be cancelled, otherwise. protected virtual bool OnLeave ([CanBeNull] View enteringView) { return false; } /// Raised when the view is gaining (entering) focus. Can be cancelled. /// /// Raised by . /// public event EventHandler Leave; #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; } if (Focused is null) { FocusDeepest (behavior, direction); return Focused is { }; } if (Focused is { }) { if (Focused.AdvanceFocus (direction, behavior)) { // TODO: Temporary hack to make Application.Navigation.FocusChanged work if (Focused.Focused is null) { Application.Navigation?.SetFocused (Focused); } return true; } } var index = GetScopedTabIndexes (behavior, direction); 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 up the hierarchy and leave Focused.SetHasFocus (false, this); // TODO: Should we check the return value of SetHasFocus? return false; } } View view = index [next]; if (view.HasFocus) { return true; } // The subview does not have focus, but at least one other that can. Can this one be focused? if (view.CanFocus && view.Visible && view.Enabled) { // Make Focused Leave Focused.SetHasFocus (false, view); view.FocusDeepest (TabBehavior.TabStop, direction); // TODO: Temporary hack to make Application.Navigation.FocusChanged work if (view.Focused is null) { Application.Navigation?.SetFocused (view); } 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; } /// /// INTERNAL API to restore focus to the subview that had focus before this view lost focused. /// /// /// Returns true if focus was restored to a subview, false otherwise. /// internal bool RestoreFocus (TabBehavior? behavior) { if (Focused is null && _subviews?.Count > 0) { // TODO: Find the previous focused view and set focus to it if (PreviouslyMostFocused is { } && PreviouslyMostFocused.TabStop == behavior) { SetFocus (PreviouslyMostFocused); return true; } return true; } return false; } /// /// 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 AUTO_CANFOCUS if (!_addingViewSoCanFocusAlsoUpdatesSuperView && IsInitialized && SuperView?.CanFocus == false && value) { throw new InvalidOperationException ("Cannot set CanFocus to true if the SuperView CanFocus is false!"); } #endif if (_canFocus == value) { return; } _canFocus = value; #if AUTO_CANFOCUS 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; } #endif if (TabStop is null && _canFocus) { TabStop = TabBehavior.TabStop; } if (!_canFocus && SuperView?.Focused == this) { SuperView.Focused = null; } if (!_canFocus && HasFocus) { SetHasFocus (false, this); SuperView?.RestoreFocus (null); // If EnsureFocus () didn't set focus to a view, focus the next focusable view in the application if (SuperView is { Focused: null }) { SuperView.AdvanceFocus (NavigationDirection.Forward, null); if (SuperView.Focused is null && Application.Current is { }) { Application.Current.AdvanceFocus (NavigationDirection.Forward, null); } ApplicationOverlapped.BringOverlappedTopToFront (); } } if (_subviews is { } && IsInitialized) { #if AUTO_CANFOCUS // Change the CanFocus of all subviews to the same value as this view // if the CanFocus of the subview is different from the value being set 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; } } } #endif 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; /// Returns the currently focused Subview inside this view, or if nothing is focused. /// The currently focused Subview. [CanBeNull] public View Focused { get; private set; } /// /// Focuses the deepest focusable view in if one exists. If there are no views in /// then the focus is set to the view itself. /// /// /// public void FocusDeepest (TabBehavior? behavior, NavigationDirection direction) { if (!CanBeVisible (this)) { return; } //View deepest = FindDeepestFocusableView (behavior, direction); //if (deepest is { }) //{ // deepest.SetFocus (); //} if (_tabIndexes is null) { SuperView?.SetFocus (this); return; } SetFocus (); foreach (View view in _tabIndexes) { if (view.CanFocus && (behavior is null || view.TabStop == behavior) && view.Visible && view.Enabled) { SetFocus (view); return; } } } [CanBeNull] private View FindDeepestFocusableView (TabBehavior? behavior, NavigationDirection direction) { var indicies = GetScopedTabIndexes (behavior, direction); foreach (View v in indicies) { if (v.TabIndexes.Count == 0) { return v; } return v.FindDeepestFocusableView (behavior, direction); } return null; } /// Returns a value indicating if this View is currently on Top (Active) public bool IsCurrentTop => Application.Current == this; /// /// 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); } #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 (TabBehavior? behavior, NavigationDirection direction) { 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 }