mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-27 00:07:58 +01:00
542 lines
17 KiB
C#
542 lines
17 KiB
C#
#nullable enable
|
|
using System.Collections.ObjectModel;
|
|
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 = "<Pending>")]
|
|
private static readonly IReadOnlyCollection<View> _empty = [];
|
|
|
|
private readonly List<View>? _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<View> InternalSubViews => _subviews ?? [];
|
|
|
|
/// <summary>Gets the list of SubViews.</summary>
|
|
/// <remarks>
|
|
/// Use <see cref="Add(Terminal.Gui.View?)"/> and <see cref="Remove(Terminal.Gui.View?)"/> to add or remove subviews.
|
|
/// </remarks>
|
|
public IReadOnlyCollection<View> SubViews => InternalSubViews?.AsReadOnly () ?? _empty;
|
|
|
|
private View? _superView;
|
|
|
|
/// <summary>
|
|
/// Gets this Views SuperView (the View's container), or <see langword="null"/> if this view has not been added as a
|
|
/// SubView.
|
|
/// </summary>
|
|
/// <seealso cref="OnSuperViewChanged"/>
|
|
/// <seealso cref="SuperViewChanged"/>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the SuperView of this View has changed.
|
|
/// </summary>
|
|
/// <param name="e"></param>
|
|
protected virtual void OnSuperViewChanged (SuperViewChangedEventArgs e) { }
|
|
|
|
/// <summary>Raised when the SuperView of this View has changed.</summary>
|
|
public event EventHandler<SuperViewChangedEventArgs>? SuperViewChanged;
|
|
|
|
#region AddRemove
|
|
|
|
/// <summary>Adds a SubView (child) to this view.</summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The Views that have been added to this view can be retrieved via the <see cref="SubViews"/> property.
|
|
/// </para>
|
|
/// <para>
|
|
/// To check if a View has been added to this View, compare it's <see cref="SuperView"/> property to this View.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// Calls/Raises the <see cref="OnSubViewAdded"/>/<see cref="SubViewAdded"/> event.
|
|
/// </para>
|
|
/// <para>
|
|
/// The <see cref="OnSuperViewChanged"/>/<see cref="SuperViewChanged"/> event will be raised on the added View.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="view">The view to add.</param>
|
|
/// <returns>The view that was added.</returns>
|
|
/// <seealso cref="Remove(View)"/>
|
|
/// <seealso cref="RemoveAll"/>
|
|
/// <seealso cref="OnSubViewAdded"/>
|
|
/// <seealso cref="SubViewAdded"/>
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>Adds the specified SubView (children) to the view.</summary>
|
|
/// <param name="views">Array of one or more views (can be optional parameter).</param>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The Views that have been added to this view can be retrieved via the <see cref="SubViews"/> property. See also
|
|
/// <seealso cref="Remove(View)"/> and <seealso cref="RemoveAll"/>.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// </remarks>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a SubView has been added to this View.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the SubView has not been initialized, this happens before BeginInit/EndInit is called.
|
|
/// </remarks>
|
|
/// <param name="view"></param>
|
|
protected virtual void OnSubViewAdded (View view) { }
|
|
|
|
/// <summary>Raised when a SubView has been added to this View.</summary>
|
|
/// <remarks>
|
|
/// If the SubView has not been initialized, this happens before BeginInit/EndInit is called.
|
|
/// </remarks>
|
|
public event EventHandler<SuperViewChangedEventArgs>? SubViewAdded;
|
|
|
|
/// <summary>Removes a SubView added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// 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 <see cref="Dispose()"/>.
|
|
/// </para>
|
|
/// <para>
|
|
/// Calls/Raises the <see cref="OnSubViewRemoved"/>/<see cref="SubViewRemoved"/> event.
|
|
/// </para>
|
|
/// <para>
|
|
/// The <see cref="OnSuperViewChanged"/>/<see cref="SuperViewChanged"/> event will be raised on the removed View.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <returns>
|
|
/// The removed View. <see langword="null"/> if the View could not be removed.
|
|
/// </returns>
|
|
/// <seealso cref="OnSubViewRemoved"/>
|
|
/// <seealso cref="SubViewRemoved"/>"/>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a SubView has been removed from this View.
|
|
/// </summary>
|
|
/// <param name="view"></param>
|
|
protected virtual void OnSubViewRemoved (View view) { }
|
|
|
|
/// <summary>Raised when a SubView has been added to this View.</summary>
|
|
public event EventHandler<SuperViewChangedEventArgs>? SubViewRemoved;
|
|
|
|
/// <summary>
|
|
/// Removes all SubViews added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// 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 <see cref="Dispose()"/> on any Views that were
|
|
/// added.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <returns>
|
|
/// A list of removed Views.
|
|
/// </returns>
|
|
public virtual IReadOnlyCollection<View> RemoveAll ()
|
|
{
|
|
List<View> removedList = new List<View> ();
|
|
while (InternalSubViews.Count > 0)
|
|
{
|
|
View? removed = Remove (InternalSubViews [0]);
|
|
if (removed is { })
|
|
{
|
|
removedList.Add (removed);
|
|
}
|
|
}
|
|
|
|
return removedList.AsReadOnly ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all SubViews of a type added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// 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 <see cref="Dispose()"/> on any Views that were
|
|
/// added.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <returns>
|
|
/// A list of removed Views.
|
|
/// </returns>
|
|
public virtual IReadOnlyCollection<TView> RemoveAll<TView> () where TView : View
|
|
{
|
|
List<TView> removedList = new List<TView> ();
|
|
foreach (TView view in InternalSubViews.OfType<TView> ().ToList ())
|
|
{
|
|
Remove (view);
|
|
removedList.Add (view);
|
|
}
|
|
return removedList.AsReadOnly ();
|
|
}
|
|
|
|
#pragma warning disable CS0067 // The event is never used
|
|
/// <summary>Raised when a SubView has been removed from this View.</summary>
|
|
public event EventHandler<SuperViewChangedEventArgs>? 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.
|
|
/// <summary>Get the top superview of a given <see cref="View"/>.</summary>
|
|
/// <returns>The superview view.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether <paramref name="view"/> is in the SubView hierarchy of <paramref name="start"/>.
|
|
/// </summary>
|
|
/// <param name="start">The View at the start of the hierarchy.</param>
|
|
/// <param name="view">The View to test.</param>
|
|
/// <param name="includeAdornments">Will search the subview hierarchy of the adornments if true.</param>
|
|
/// <returns></returns>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Moves <paramref name="subview"/> one position towards the end of the <see cref="SubViews"/> list.
|
|
/// </summary>
|
|
/// <param name="subview">The subview to move.</param>
|
|
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);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves <paramref name="subview"/> to the end of the <see cref="SubViews"/> list.
|
|
/// </summary>
|
|
/// <param name="subview">The subview to move.</param>
|
|
public void MoveSubViewToEnd (View subview)
|
|
{
|
|
PerformActionForSubView (
|
|
subview,
|
|
x =>
|
|
{
|
|
InternalSubViews!.Remove (x);
|
|
InternalSubViews.Add (x);
|
|
}
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves <paramref name="subview"/> one position towards the start of the <see cref="SubViews"/> list.
|
|
/// </summary>
|
|
/// <param name="subview">The subview to move.</param>
|
|
public void MoveSubViewTowardsStart (View subview)
|
|
{
|
|
PerformActionForSubView (
|
|
subview,
|
|
x =>
|
|
{
|
|
int idx = InternalSubViews!.IndexOf (x);
|
|
|
|
if (idx > 0)
|
|
{
|
|
InternalSubViews.Remove (x);
|
|
InternalSubViews.Insert (idx - 1, x);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves <paramref name="subview"/> to the start of the <see cref="SubViews"/> list.
|
|
/// </summary>
|
|
/// <param name="subview">The subview to move.</param>
|
|
public void MoveSubViewToStart (View subview)
|
|
{
|
|
PerformActionForSubView (
|
|
subview,
|
|
x =>
|
|
{
|
|
InternalSubViews!.Remove (x);
|
|
InternalSubViews.Insert (0, subview);
|
|
}
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal API that runs <paramref name="action"/> on a subview if it is part of the <see cref="SubViews"/> list.
|
|
/// </summary>
|
|
/// <param name="subview"></param>
|
|
/// <param name="action"></param>
|
|
private void PerformActionForSubView (View subview, Action<View> action)
|
|
{
|
|
if (InternalSubViews.Contains (subview))
|
|
{
|
|
action (subview);
|
|
}
|
|
|
|
// BUGBUG: this is odd. Why is this needed?
|
|
SetNeedsDraw ();
|
|
subview.SetNeedsDraw ();
|
|
}
|
|
|
|
#endregion SubViewOrdering
|
|
}
|