#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui; public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.) { [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] private static readonly IReadOnlyCollection _empty = []; private readonly List? _subviews = []; // Internally, we use InternalSubViews rather than subviews, as we do not expect us // to make the same mistakes our users make when they poke at the SubViews. internal IList InternalSubViews => _subviews ?? []; /// Gets the list of SubViews. /// /// Use and to add or remove subviews. /// public IReadOnlyCollection SubViews => InternalSubViews?.AsReadOnly () ?? _empty; private View? _superView; /// /// Gets this Views SuperView (the View's container), or if this view has not been added as a /// SubView. /// /// /// public View? SuperView { get => _superView!; private set => SetSuperView (value); } private void SetSuperView (View? value) { if (_superView == value) { return; } _superView = value; RaiseSuperViewChanged (); } private void RaiseSuperViewChanged () { SuperViewChangedEventArgs args = new (SuperView, this); OnSuperViewChanged (args); SuperViewChanged?.Invoke (this, args); } /// /// Called when the SuperView of this View has changed. /// /// protected virtual void OnSuperViewChanged (SuperViewChangedEventArgs e) { } /// Raised when the SuperView of this View has changed. public event EventHandler? SuperViewChanged; #region AddRemove /// Adds a SubView (child) to this view. /// /// /// The Views that have been added to this view can be retrieved via the property. /// /// /// To check if a View has been added to this View, compare it's property to this View. /// /// /// SubViews will be disposed when this View is disposed. In other-words, calling this method causes /// the lifecycle of the subviews to be transferred to this View. /// /// /// Calls/Raises the / event. /// /// /// The / event will be raised on the added View. /// /// /// The view to add. /// The view that was added. /// /// /// /// public virtual View? Add (View? view) { if (view is null) { return null; } //Debug.Assert (view.SuperView is null, $"{view} already has a SuperView: {view.SuperView}."); if (view.SuperView is {}) { Logging.Warning ($"{view} already has a SuperView: {view.SuperView}."); } //Debug.Assert (!InternalSubViews.Contains (view), $"{view} has already been Added to {this}."); if (InternalSubViews.Contains (view)) { Logging.Warning ($"{view} has already been Added to {this}."); } // TileView likes to add views that were previously added and have HasFocus = true. No bueno. view.HasFocus = false; // TODO: Make this thread safe InternalSubViews.Add (view); view.SuperView = this; if (view is { Enabled: true, Visible: true, CanFocus: true }) { // Add will cause the newly added subview to gain focus if it's focusable if (HasFocus) { view.SetFocus (); } } if (view.Enabled && !Enabled) { view.Enabled = false; } // Raise event indicating a subview has been added // We do this before Init. RaiseSubViewAdded (view); if (IsInitialized && !view.IsInitialized) { view.BeginInit (); view.EndInit (); } SetNeedsDraw (); SetNeedsLayout (); return view; } /// Adds the specified SubView (children) to the view. /// Array of one or more views (can be optional parameter). /// /// /// The Views that have been added to this view can be retrieved via the property. See also /// and . /// /// /// SubViews will be disposed when this View is disposed. In other-words, calling this method causes /// the lifecycle of the subviews to be transferred to this View. /// /// public void Add (params View []? views) { if (views is null) { return; } foreach (View view in views) { Add (view); } } internal void RaiseSubViewAdded (View view) { OnSubViewAdded (view); SubViewAdded?.Invoke (this, new (this, view)); } /// /// Called when a SubView has been added to this View. /// /// /// If the SubView has not been initialized, this happens before BeginInit/EndInit is called. /// /// protected virtual void OnSubViewAdded (View view) { } /// Raised when a SubView has been added to this View. /// /// If the SubView has not been initialized, this happens before BeginInit/EndInit is called. /// public event EventHandler? SubViewAdded; /// Removes a SubView added via or from this View. /// /// /// Normally SubViews will be disposed when this View is disposed. Removing a SubView causes ownership of the /// SubView's /// lifecycle to be transferred to the caller; the caller must call . /// /// /// Calls/Raises the / event. /// /// /// The / event will be raised on the removed View. /// /// /// /// The removed View. if the View could not be removed. /// /// /// "/> public virtual View? Remove (View? view) { if (view is null) { return null; } if (InternalSubViews.Count == 0) { return view; } if (view.SuperView is null) { Logging.Warning ($"{view} cannot be Removed. SuperView is null."); } if (view.SuperView != this) { Logging.Warning ($"{view} cannot be Removed. SuperView is not this ({view.SuperView}."); } if (!InternalSubViews.Contains (view)) { Logging.Warning ($"{view} cannot be Removed. It has not been added to {this}."); } Rectangle touched = view.Frame; bool hadFocus = view.HasFocus; bool couldFocus = view.CanFocus; if (hadFocus) { view.CanFocus = false; // If view had focus, this will ensure it doesn't and it stays that way } Debug.Assert (!view.HasFocus); InternalSubViews.Remove (view); // Clean up focus stuff _previouslyFocused = null; if (view.SuperView is { } && view.SuperView._previouslyFocused == this) { view.SuperView._previouslyFocused = null; } view.SuperView = null; SetNeedsLayout (); SetNeedsDraw (); foreach (View v in InternalSubViews) { if (v.Frame.IntersectsWith (touched)) { view.SetNeedsDraw (); } } view.CanFocus = couldFocus; // Restore to previous value if (_previouslyFocused == view) { _previouslyFocused = null; } RaiseSubViewRemoved (view); return view; } internal void RaiseSubViewRemoved (View view) { OnSubViewRemoved (view); SubViewRemoved?.Invoke (this, new (this, view)); } /// /// Called when a SubView has been removed from this View. /// /// protected virtual void OnSubViewRemoved (View view) { } /// Raised when a SubView has been added to this View. public event EventHandler? SubViewRemoved; /// /// Removes all SubView (children) added via or from this View. /// /// /// /// Normally SubViews will be disposed when this View is disposed. Removing a SubView causes ownership of the /// SubView's /// lifecycle to be transferred to the caller; the caller must call on any Views that were /// added. /// /// public virtual void RemoveAll () { while (InternalSubViews.Count > 0) { Remove (InternalSubViews [0]); } } #pragma warning disable CS0067 // The event is never used /// Raised when a SubView has been removed from this View. public event EventHandler? Removed; #pragma warning restore CS0067 // The event is never used #endregion AddRemove // TODO: This drives a weird coupling of Application.Top and View. It's not clear why this is needed. /// Get the top superview of a given . /// The superview view. internal View? GetTopSuperView (View? view = null, View? superview = null) { View? top = superview ?? Application.Top; for (View? v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView) { top = v; if (top == superview) { break; } } return top; } /// /// Gets whether is in the SubView hierarchy of . /// /// The View at the start of the hierarchy. /// The View to test. /// Will search the subview hierarchy of the adornments if true. /// public static bool IsInHierarchy (View? start, View? view, bool includeAdornments = false) { if (view is null || start is null) { return false; } if (view == start) { return true; } foreach (View subView in start.InternalSubViews) { if (view == subView) { return true; } bool found = IsInHierarchy (subView, view, includeAdornments); if (found) { return found; } } if (includeAdornments) { bool found = IsInHierarchy (start.Padding, view, includeAdornments); if (found) { return found; } found = IsInHierarchy (start.Border, view, includeAdornments); if (found) { return found; } found = IsInHierarchy (start.Margin, view, includeAdornments); if (found) { return found; } } return false; } #region SubViewOrdering /// /// Moves one position towards the end of the list. /// /// The subview to move. public void MoveSubViewTowardsEnd (View subview) { PerformActionForSubView ( subview, x => { int idx = InternalSubViews!.IndexOf (x); if (idx + 1 < InternalSubViews.Count) { InternalSubViews.Remove (x); InternalSubViews.Insert (idx + 1, x); } } ); } /// /// Moves to the end of the list. /// /// The subview to move. public void MoveSubViewToEnd (View subview) { PerformActionForSubView ( subview, x => { InternalSubViews!.Remove (x); InternalSubViews.Add (x); } ); } /// /// Moves one position towards the start of the list. /// /// The subview to move. public void MoveSubViewTowardsStart (View subview) { PerformActionForSubView ( subview, x => { int idx = InternalSubViews!.IndexOf (x); if (idx > 0) { InternalSubViews.Remove (x); InternalSubViews.Insert (idx - 1, x); } } ); } /// /// Moves to the start of the list. /// /// The subview to move. public void MoveSubViewToStart (View subview) { PerformActionForSubView ( subview, x => { InternalSubViews!.Remove (x); InternalSubViews.Insert (0, subview); } ); } /// /// Internal API that runs on a subview if it is part of the list. /// /// /// private void PerformActionForSubView (View subview, Action action) { if (InternalSubViews.Contains (subview)) { action (subview); } // BUGBUG: this is odd. Why is this needed? SetNeedsDraw (); subview.SetNeedsDraw (); } #endregion SubViewOrdering }