#nullable enable
namespace Terminal.Gui;
///
/// Toplevel views are used for both an application's main view (filling the entire screen and for modal (pop-up)
/// views such as , , and ).
///
///
///
/// Toplevel views can run as modal (popup) views, started by calling
/// . They return control to the caller when
/// has been called (which sets the
/// property to false).
///
///
/// A Toplevel is created when an application initializes Terminal.Gui by calling .
/// The application Toplevel can be accessed via . Additional Toplevels can be created
/// and run (e.g. s). To run a Toplevel, create the and call
/// .
///
///
public partial class Toplevel : View
{
///
/// Initializes a new instance of the class,
/// defaulting to full screen. The and properties will be set to the
/// dimensions of the terminal using .
///
public Toplevel ()
{
CanFocus = true;
TabStop = TabBehavior.TabGroup;
Arrangement = ViewArrangement.Fixed;
Width = Dim.Fill ();
Height = Dim.Fill ();
ColorScheme = Colors.ColorSchemes ["TopLevel"];
MouseClick += Toplevel_MouseClick;
}
#region Keyboard & Mouse
// TODO: IRunnable: Re-implement - Modal means IRunnable, ViewArrangement.Overlapped where modalView.Z > allOtherViews.Max (v = v.Z), and exclusive key/mouse input.
///
/// Determines whether the is modal or not. If set to false (the default):
///
/// -
/// events will propagate keys upwards.
///
/// -
/// The Toplevel will act as an embedded view (not a modal/pop-up).
///
///
/// If set to true:
///
/// -
/// events will NOT propagate keys upwards.
///
/// -
/// The Toplevel will and look like a modal (pop-up) (e.g. see .
///
///
///
public bool Modal { get; set; }
private void Toplevel_MouseClick (object? sender, MouseEventEventArgs e) { e.Handled = InvokeCommand (Command.HotKey) == true; }
#endregion
#region Subviews
// TODO: Deprecate - Any view can host a menubar in v2
/// Gets or sets the menu for this Toplevel.
public MenuBar? MenuBar { get; set; }
// TODO: Deprecate - Any view can host a statusbar in v2
/// Gets or sets the status bar for this Toplevel.
public StatusBar? StatusBar { get; set; }
///
public override View Add (View view)
{
CanFocus = true;
AddMenuStatusBar (view);
return base.Add (view);
}
///
public override View Remove (View view)
{
if (this is Toplevel { MenuBar: { } })
{
RemoveMenuStatusBar (view);
}
return base.Remove (view);
}
///
public override void RemoveAll ()
{
if (this == Application.Top)
{
MenuBar?.Dispose ();
MenuBar = null;
StatusBar?.Dispose ();
StatusBar = null;
}
base.RemoveAll ();
}
internal void AddMenuStatusBar (View view)
{
if (view is MenuBar)
{
MenuBar = view as MenuBar;
}
if (view is StatusBar)
{
StatusBar = view as StatusBar;
}
}
internal void RemoveMenuStatusBar (View view)
{
if (view is MenuBar)
{
MenuBar?.Dispose ();
MenuBar = null;
}
if (view is StatusBar)
{
StatusBar?.Dispose ();
StatusBar = null;
}
}
// TODO: Overlapped - Rename to AllSubviewsClosed - Move to View?
///
/// Invoked when the last child of the Toplevel is closed from by
/// .
///
public event EventHandler? AllChildClosed;
// TODO: Overlapped - Rename to *Subviews* - Move to View?
///
/// Invoked when a child of the Toplevel is closed by
/// .
///
public event EventHandler? ChildClosed;
// TODO: Overlapped - Rename to *Subviews* - Move to View?
/// Invoked when a child Toplevel's has been loaded.
public event EventHandler? ChildLoaded;
// TODO: Overlapped - Rename to *Subviews* - Move to View?
/// Invoked when a cjhild Toplevel's has been unloaded.
public event EventHandler? ChildUnloaded;
#endregion
#region Life Cycle
// TODO: IRunnable: Re-implement as a property on IRunnable
/// Gets or sets whether the main loop for this is running or not.
/// Setting this property directly is discouraged. Use instead.
public bool Running { get; set; }
// TODO: IRunnable: Re-implement in IRunnable
///
/// if was already loaded by the
/// , otherwise.
///
public bool IsLoaded { get; private set; }
// TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Activating/Activate
/// Invoked when the Toplevel becomes the Toplevel.
public event EventHandler? Activate;
// TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Deactivating/Deactivate?
/// Invoked when the Toplevel ceases to be the Toplevel.
public event EventHandler? Deactivate;
/// Invoked when the Toplevel's is closed by .
public event EventHandler? Closed;
///
/// Invoked when the Toplevel's is being closed by
/// .
///
public event EventHandler? Closing;
///
/// Invoked when the has begun to be loaded. A Loaded event handler
/// is a good place to finalize initialization before calling .
///
public event EventHandler? Loaded;
///
/// Called from before the redraws for the first
/// time.
///
///
/// Overrides must call base.OnLoaded() to ensure any Toplevel subviews are initialized properly and the
/// event is raised.
///
public virtual void OnLoaded ()
{
IsLoaded = true;
foreach (var view in Subviews.Where (v => v is Toplevel))
{
var tl = (Toplevel)view;
tl.OnLoaded ();
}
Loaded?.Invoke (this, EventArgs.Empty);
}
///
/// Invoked when the main loop has started it's first iteration. Subscribe to this event to
/// perform tasks when the has been laid out and focus has been set. changes.
///
/// A Ready event handler is a good place to finalize initialization after calling
/// on this .
///
///
public event EventHandler? Ready;
///
/// Stops and closes this . If this Toplevel is the top-most Toplevel,
/// will be called, causing the application to exit.
///
public virtual void RequestStop ()
{
if (IsOverlappedContainer
&& Running
&& (Application.Current == this
|| Application.Current?.Modal == false
|| (Application.Current?.Modal == true && Application.Current?.Running == false)))
{
foreach (Toplevel child in ApplicationOverlapped.OverlappedChildren!)
{
var ev = new ToplevelClosingEventArgs (this);
if (child.OnClosing (ev))
{
return;
}
child.Running = false;
Application.RequestStop (child);
}
Running = false;
Application.RequestStop (this);
}
else if (IsOverlappedContainer && Running && Application.Current?.Modal == true && Application.Current?.Running == true)
{
var ev = new ToplevelClosingEventArgs (Application.Current);
if (OnClosing (ev))
{
return;
}
Application.RequestStop (Application.Current);
}
else if (!IsOverlappedContainer && Running && (!Modal || (Modal && Application.Current != this)))
{
var ev = new ToplevelClosingEventArgs (this);
if (OnClosing (ev))
{
return;
}
Running = false;
Application.RequestStop (this);
}
else
{
Application.RequestStop (Application.Current);
}
}
///
/// Invoked when the Toplevel has been unloaded. A Unloaded event handler is a good place
/// to dispose objects after calling .
///
public event EventHandler? Unloaded;
internal virtual void OnActivate (Toplevel deactivated) { Activate?.Invoke (this, new (deactivated)); }
///
/// Stops and closes the specified by . If is
/// the top-most Toplevel, will be called, causing the application to
/// exit.
///
/// The Toplevel to request stop.
public virtual void RequestStop (Toplevel top) { top.RequestStop (); }
internal virtual void OnAllChildClosed () { AllChildClosed?.Invoke (this, EventArgs.Empty); }
internal virtual void OnChildClosed (Toplevel top)
{
if (IsOverlappedContainer)
{
SetSubViewNeedsDisplay ();
}
ChildClosed?.Invoke (this, new (top));
}
internal virtual void OnChildLoaded (Toplevel top) { ChildLoaded?.Invoke (this, new (top)); }
internal virtual void OnChildUnloaded (Toplevel top) { ChildUnloaded?.Invoke (this, new (top)); }
internal virtual void OnClosed (Toplevel top) { Closed?.Invoke (this, new (top)); }
internal virtual bool OnClosing (ToplevelClosingEventArgs ev)
{
Closing?.Invoke (this, ev);
return ev.Cancel;
}
internal virtual void OnDeactivate (Toplevel activated) { Deactivate?.Invoke (this, new (activated)); }
///
/// Called from after the has entered the first iteration
/// of the loop.
///
internal virtual void OnReady ()
{
foreach (var view in Subviews.Where (v => v is Toplevel))
{
var tl = (Toplevel)view;
tl.OnReady ();
}
Ready?.Invoke (this, EventArgs.Empty);
}
/// Called from before the is disposed.
internal virtual void OnUnloaded ()
{
foreach (var view in Subviews.Where (v => v is Toplevel))
{
var tl = (Toplevel)view;
tl.OnUnloaded ();
}
Unloaded?.Invoke (this, EventArgs.Empty);
}
#endregion
#region Draw
///
public override void OnDrawContent (Rectangle viewport)
{
if (!Visible)
{
return;
}
if (NeedsDisplay || SubViewNeedsDisplay /*|| LayoutNeeded*/)
{
Clear ();
//LayoutSubviews ();
PositionToplevels ();
if (this == ApplicationOverlapped.OverlappedTop)
{
// This enables correct draw behavior when switching between overlapped subviews
foreach (Toplevel top in ApplicationOverlapped.OverlappedChildren!.AsEnumerable ().Reverse ())
{
if (top.Frame.IntersectsWith (Viewport))
{
if (top != this && !top.IsCurrentTop && !OutsideTopFrame (top) && top.Visible)
{
top.SetNeedsLayout ();
top.SetNeedsDisplay (top.Viewport);
top.Draw ();
top.OnRenderLineCanvas ();
}
}
}
}
// BUGBUG: This appears to be a hack to get ScrollBarViews to render correctly.
foreach (View view in Subviews)
{
if (view.Frame.IntersectsWith (Viewport) && !OutsideTopFrame (this))
{
//view.SetNeedsLayout ();
view.SetNeedsDisplay ();
view.SetSubViewNeedsDisplay ();
}
}
base.OnDrawContent (viewport);
}
}
#endregion
#region Navigation
///
public override bool OnEnter (View view) { return MostFocused?.OnEnter (view) ?? base.OnEnter (view); }
///
public override bool OnLeave (View view) { return MostFocused?.OnLeave (view) ?? base.OnLeave (view); }
#endregion
#region Size / Position Management
// TODO: Make cancelable?
internal virtual void OnSizeChanging (SizeChangedEventArgs size) { SizeChanging?.Invoke (this, size); }
///
public override Point? PositionCursor ()
{
if (!IsOverlappedContainer)
{
if (Focused is null)
{
RestoreFocus ();
}
return null;
}
// This code path only happens when the Toplevel is an Overlapped container
if (Focused is null)
{
// TODO: this is an Overlapped hack
foreach (Toplevel top in ApplicationOverlapped.OverlappedChildren!)
{
if (top != this && top.Visible)
{
top.SetFocus ();
return null;
}
}
}
Point? cursor2 = base.PositionCursor ();
return null;
}
///
/// Adjusts the location and size of within this Toplevel. Virtual method enabling
/// implementation of specific positions for inherited views.
///
/// The Toplevel to adjust.
public virtual void PositionToplevel (Toplevel? top)
{
if (top is null)
{
return;
}
View? superView = GetLocationEnsuringFullVisibility (
top,
top.Frame.X,
top.Frame.Y,
out int nx,
out int ny,
out StatusBar? sb
);
if (superView is null)
{
return;
}
var layoutSubviews = false;
var maxWidth = 0;
if (superView.Margin is { } && superView == top.SuperView)
{
maxWidth -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right;
}
if ((superView != top || top?.SuperView is { } || (top != Application.Top && top!.Modal) || (top?.SuperView is null && ApplicationOverlapped.IsOverlapped (top)))
&& (top!.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y))
{
if (top?.X is null or PosAbsolute && top?.Frame.X != nx)
{
top!.X = nx;
layoutSubviews = true;
}
if (top?.Y is null or PosAbsolute && top?.Frame.Y != ny)
{
top!.Y = ny;
layoutSubviews = true;
}
}
// TODO: v2 - This is a hack to get the StatusBar to be positioned correctly.
if (sb != null
&& !top!.Subviews.Contains (sb)
&& ny + top.Frame.Height != superView.Frame.Height - (sb.Visible ? 1 : 0)
&& top.Height is DimFill
&& -top.Height.GetAnchor (0) < 1)
{
top.Height = Dim.Fill (sb.Visible ? 1 : 0);
layoutSubviews = true;
}
if (superView.LayoutNeeded || layoutSubviews)
{
superView.LayoutSubviews ();
}
if (LayoutNeeded)
{
LayoutSubviews ();
}
}
/// Invoked when the terminal has been resized. The new of the terminal is provided.
public event EventHandler? SizeChanging;
private bool OutsideTopFrame (Toplevel top)
{
if (top.Frame.X > Driver.Cols || top.Frame.Y > Driver.Rows)
{
return true;
}
return false;
}
// TODO: v2 - Not sure this is needed anymore.
internal void PositionToplevels ()
{
PositionToplevel (this);
foreach (View top in Subviews)
{
if (top is Toplevel)
{
PositionToplevel ((Toplevel)top);
}
}
}
#endregion
}
///
/// Implements the for comparing two s used by
/// .
///
public class ToplevelEqualityComparer : IEqualityComparer
{
/// Determines whether the specified objects are equal.
/// The first object of type to compare.
/// The second object of type to compare.
/// if the specified objects are equal; otherwise, .
public bool Equals (Toplevel? x, Toplevel? y)
{
if (y is null && x is null)
{
return true;
}
if (x is null || y is null)
{
return false;
}
if (x.Id == y.Id)
{
return true;
}
return false;
}
/// Returns a hash code for the specified object.
/// The for which a hash code is to be returned.
/// A hash code for the specified object.
///
/// The type of is a reference type and
/// is .
///
public int GetHashCode (Toplevel obj)
{
if (obj is null)
{
throw new ArgumentNullException ();
}
var hCode = 0;
if (int.TryParse (obj.Id, out int result))
{
hCode = result;
}
return hCode.GetHashCode ();
}
}
///
/// Implements the to sort the from the
/// if needed.
///
public sealed class ToplevelComparer : IComparer
{
///
/// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the
/// other.
///
/// The first object to compare.
/// The second object to compare.
///
/// A signed integer that indicates the relative values of and , as shown
/// in the following table.Value Meaning Less than zero is less than .Zero
/// equals .Greater than zero is greater than
/// .
///
public int Compare (Toplevel? x, Toplevel? y)
{
if (ReferenceEquals (x, y))
{
return 0;
}
if (x is null)
{
return -1;
}
if (y is null)
{
return 1;
}
return string.CompareOrdinal (x.Id, y.Id);
}
}