using System.Net.Mime;
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 ).
///
///
///
/// Toplevels 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 with layout,
/// defaulting to full screen. The and properties will be set to the
/// dimensions of the terminal using .
///
public Toplevel ()
{
Arrangement = ViewArrangement.Movable;
Width = Dim.Fill ();
Height = Dim.Fill ();
ColorScheme = Colors.ColorSchemes ["TopLevel"];
// Things this view knows how to do
AddCommand (
Command.QuitToplevel,
() =>
{
QuitToplevel ();
return true;
}
);
AddCommand (
Command.Suspend,
() =>
{
Driver.Suspend ();
;
return true;
}
);
AddCommand (
Command.NextView,
() =>
{
MoveNextView ();
return true;
}
);
AddCommand (
Command.PreviousView,
() =>
{
MovePreviousView ();
return true;
}
);
AddCommand (
Command.NextViewOrTop,
() =>
{
MoveNextViewOrTop ();
return true;
}
);
AddCommand (
Command.PreviousViewOrTop,
() =>
{
MovePreviousViewOrTop ();
return true;
}
);
AddCommand (
Command.Refresh,
() =>
{
Application.Refresh ();
return true;
}
);
// Default keybindings for this view
KeyBindings.Add (Application.QuitKey, Command.QuitToplevel);
KeyBindings.Add (Key.CursorRight, Command.NextView);
KeyBindings.Add (Key.CursorDown, Command.NextView);
KeyBindings.Add (Key.CursorLeft, Command.PreviousView);
KeyBindings.Add (Key.CursorUp, Command.PreviousView);
KeyBindings.Add (Key.Tab, Command.NextView);
KeyBindings.Add (Key.Tab.WithShift, Command.PreviousView);
KeyBindings.Add (Key.Tab.WithCtrl, Command.NextViewOrTop);
KeyBindings.Add (Key.Tab.WithShift.WithCtrl, Command.PreviousViewOrTop);
KeyBindings.Add (Key.F5, Command.Refresh);
KeyBindings.Add (Application.AlternateForwardKey, Command.NextViewOrTop); // Needed on Unix
KeyBindings.Add (Application.AlternateBackwardKey, Command.PreviousViewOrTop); // Needed on Unix
#if UNIX_KEY_BINDINGS
KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend);
KeyBindings.Add (Key.L.WithCtrl, Command.Refresh); // Unix
KeyBindings.Add (Key.F.WithCtrl, Command.NextView); // Unix
KeyBindings.Add (Key.I.WithCtrl, Command.NextView); // Unix
KeyBindings.Add (Key.B.WithCtrl, Command.PreviousView); // Unix
#endif
MouseClick += Toplevel_MouseClick;
CanFocus = true;
}
private void Toplevel_MouseClick (object sender, MouseEventEventArgs e)
{
e.Handled = InvokeCommand (Command.HotKey) == true;
}
///
/// if was already loaded by the
/// , otherwise.
///
public bool IsLoaded { get; private set; }
/// Gets or sets the menu for this Toplevel.
public virtual MenuBar MenuBar { get; set; }
///
/// 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; }
/// 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; }
/// Gets or sets the status bar for this Toplevel.
public virtual StatusBar StatusBar { get; set; }
/// Invoked when the Toplevel becomes the Toplevel.
public event EventHandler Activate;
///
public override void Add (View view)
{
CanFocus = true;
AddMenuStatusBar (view);
base.Add (view);
}
///
/// Invoked when the last child of the Toplevel is closed from by
/// .
///
public event EventHandler AllChildClosed;
/// Invoked when the is changed.
public event EventHandler AlternateBackwardKeyChanged;
/// Invoked when the is changed.
public event EventHandler AlternateForwardKeyChanged;
///
/// Invoked when a child of the Toplevel is closed by
/// .
///
public event EventHandler ChildClosed;
/// Invoked when a child Toplevel's has been loaded.
public event EventHandler ChildLoaded;
/// Invoked when a cjhild Toplevel's has been unloaded.
public event EventHandler ChildUnloaded;
/// 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 Toplevel ceases to be the Toplevel.
public event EventHandler Deactivate;
///
/// 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;
/// Virtual method to invoke the event.
///
public virtual void OnAlternateBackwardKeyChanged (KeyChangedEventArgs e)
{
KeyBindings.Replace (e.OldKey, e.NewKey);
AlternateBackwardKeyChanged?.Invoke (this, e);
}
/// Virtual method to invoke the event.
///
public virtual void OnAlternateForwardKeyChanged (KeyChangedEventArgs e)
{
KeyBindings.Replace (e.OldKey, e.NewKey);
AlternateForwardKeyChanged?.Invoke (this, e);
}
///
public override void OnDrawContent (Rectangle viewport)
{
if (!Visible)
{
return;
}
if (NeedsDisplay || SubViewNeedsDisplay || LayoutNeeded)
{
//Driver.SetAttribute (GetNormalColor ());
// TODO: It's bad practice for views to always clear. Defeats the purpose of clipping etc...
Clear ();
LayoutSubviews ();
PositionToplevels ();
if (this == Application.OverlappedTop)
{
foreach (Toplevel top in Application.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 ();
}
}
}
}
// This should not be here, but in base
foreach (View view in Subviews)
{
if (view.Frame.IntersectsWith (Viewport) && !OutsideTopFrame (this))
{
//view.SetNeedsLayout ();
view.SetNeedsDisplay (view.Viewport);
view.SetSubViewNeedsDisplay ();
}
}
base.OnDrawContent (viewport);
// This is causing the menus drawn incorrectly if UseSubMenusSingleFrame is true
//if (this.MenuBar is { } && this.MenuBar.IsMenuOpen && this.MenuBar.openMenu is { }) {
// // TODO: Hack until we can get compositing working right.
// this.MenuBar.openMenu.Redraw (this.MenuBar.openMenu.Viewport);
//}
}
}
///
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); }
///
/// Called from before the redraws for the first
/// time.
///
public virtual void OnLoaded ()
{
IsLoaded = true;
foreach (Toplevel tl in Subviews.Where (v => v is Toplevel))
{
tl.OnLoaded ();
}
Loaded?.Invoke (this, EventArgs.Empty);
}
/// Virtual method to invoke the event.
///
public virtual void OnQuitKeyChanged (KeyChangedEventArgs e)
{
KeyBindings.Replace (e.OldKey, e.NewKey);
QuitKeyChanged?.Invoke (this, e);
}
///
public override void PositionCursor ()
{
if (!IsOverlappedContainer)
{
base.PositionCursor ();
if (Focused is null)
{
EnsureFocus ();
if (Focused is null)
{
Driver.SetCursorVisibility (CursorVisibility.Invisible);
}
}
return;
}
if (Focused is null)
{
foreach (Toplevel top in Application.OverlappedChildren)
{
if (top != this && top.Visible)
{
top.SetFocus ();
return;
}
}
}
base.PositionCursor ();
if (Focused is null)
{
Driver.SetCursorVisibility (CursorVisibility.Invisible);
}
}
///
/// 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)
{
View superView = GetLocationEnsuringFullVisibility (
top,
top.Frame.X,
top.Frame.Y,
out int nx,
out int ny,
out StatusBar sb
);
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 && top.IsOverlapped))
// BUGBUG: Prevously PositionToplevel required LayotuStyle.Computed
&& (top.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y) /*&& top.LayoutStyle == LayoutStyle.Computed*/)
{
if ((top.X is null || top.X is Pos.PosAbsolute) && top.Frame.X != nx)
{
top.X = nx;
layoutSubviews = true;
}
if ((top.Y is null || top.Y is Pos.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 Dim.DimFill
&& -top.Height.Anchor (0) < 1)
{
top.Height = Dim.Fill (sb.Visible ? 1 : 0);
layoutSubviews = true;
}
if (superView.LayoutNeeded || layoutSubviews)
{
superView.LayoutSubviews ();
}
if (LayoutNeeded)
{
LayoutSubviews ();
}
}
/// Invoked when the is changed.
public event EventHandler QuitKeyChanged;
///
/// 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;
///
public override void Remove (View view)
{
if (this is Toplevel Toplevel && Toplevel.MenuBar is { })
{
RemoveMenuStatusBar (view);
}
base.Remove (view);
}
///
public override void RemoveAll ()
{
if (this == Application.Top)
{
MenuBar?.Dispose ();
MenuBar = null;
StatusBar?.Dispose ();
StatusBar = null;
}
base.RemoveAll ();
}
///
/// 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 Application.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);
}
}
///
/// 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 (); }
/// Invoked when the terminal has been resized. The new of the terminal is provided.
public event EventHandler SizeChanging;
///
/// 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 void AddMenuStatusBar (View view)
{
if (view is MenuBar)
{
MenuBar = view as MenuBar;
}
if (view is StatusBar)
{
StatusBar = view as StatusBar;
}
}
internal virtual void OnActivate (Toplevel deactivated) { Activate?.Invoke (this, new ToplevelEventArgs (deactivated)); }
internal virtual void OnAllChildClosed () { AllChildClosed?.Invoke (this, EventArgs.Empty); }
internal virtual void OnChildClosed (Toplevel top)
{
if (IsOverlappedContainer)
{
SetSubViewNeedsDisplay ();
}
ChildClosed?.Invoke (this, new ToplevelEventArgs (top));
}
internal virtual void OnChildLoaded (Toplevel top) { ChildLoaded?.Invoke (this, new ToplevelEventArgs (top)); }
internal virtual void OnChildUnloaded (Toplevel top) { ChildUnloaded?.Invoke (this, new ToplevelEventArgs (top)); }
internal virtual void OnClosed (Toplevel top) { Closed?.Invoke (this, new ToplevelEventArgs (top)); }
internal virtual bool OnClosing (ToplevelClosingEventArgs ev)
{
Closing?.Invoke (this, ev);
return ev.Cancel;
}
internal virtual void OnDeactivate (Toplevel activated) { Deactivate?.Invoke (this, new ToplevelEventArgs (activated)); }
///
/// Called from after the has entered the first iteration
/// of the loop.
///
internal virtual void OnReady ()
{
foreach (Toplevel tl in Subviews.Where (v => v is Toplevel))
{
tl.OnReady ();
}
Ready?.Invoke (this, EventArgs.Empty);
}
// TODO: Make cancelable?
internal virtual void OnSizeChanging (SizeChangedEventArgs size) { SizeChanging?.Invoke (this, size); }
/// Called from before the is disposed.
internal virtual void OnUnloaded ()
{
foreach (Toplevel tl in Subviews.Where (v => v is Toplevel))
{
tl.OnUnloaded ();
}
Unloaded?.Invoke (this, EventArgs.Empty);
}
// 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);
}
}
}
internal void RemoveMenuStatusBar (View view)
{
if (view is MenuBar)
{
MenuBar?.Dispose ();
MenuBar = null;
}
if (view is StatusBar)
{
StatusBar?.Dispose ();
StatusBar = null;
}
}
private void FocusNearestView (IEnumerable views, NavigationDirection direction)
{
if (views is null)
{
return;
}
var found = false;
var focusProcessed = false;
var idx = 0;
foreach (View v in views)
{
if (v == this)
{
found = true;
}
if (found && v != this)
{
if (direction == NavigationDirection.Forward)
{
SuperView?.FocusNext ();
}
else
{
SuperView?.FocusPrev ();
}
focusProcessed = true;
if (SuperView.Focused is { } && SuperView.Focused != this)
{
return;
}
}
else if (found && !focusProcessed && idx == views.Count () - 1)
{
views.ToList () [0].SetFocus ();
}
idx++;
}
}
private View GetDeepestFocusedSubview (View view)
{
if (view is null)
{
return null;
}
foreach (View v in view.Subviews)
{
if (v.HasFocus)
{
return GetDeepestFocusedSubview (v);
}
}
return view;
}
private void MoveNextView ()
{
View old = GetDeepestFocusedSubview (Focused);
if (!FocusNext ())
{
FocusNext ();
}
if (old != Focused && old != Focused?.Focused)
{
old?.SetNeedsDisplay ();
Focused?.SetNeedsDisplay ();
}
else
{
FocusNearestView (SuperView?.TabIndexes, NavigationDirection.Forward);
}
}
private void MoveNextViewOrTop ()
{
if (Application.OverlappedTop is null)
{
Toplevel top = Modal ? this : Application.Top;
top.FocusNext ();
if (top.Focused is null)
{
top.FocusNext ();
}
top.SetNeedsDisplay ();
Application.BringOverlappedTopToFront ();
}
else
{
Application.OverlappedMoveNext ();
}
}
private void MovePreviousView ()
{
View old = GetDeepestFocusedSubview (Focused);
if (!FocusPrev ())
{
FocusPrev ();
}
if (old != Focused && old != Focused?.Focused)
{
old?.SetNeedsDisplay ();
Focused?.SetNeedsDisplay ();
}
else
{
FocusNearestView (SuperView?.TabIndexes?.Reverse (), NavigationDirection.Backward);
}
}
private void MovePreviousViewOrTop ()
{
if (Application.OverlappedTop is null)
{
Toplevel top = Modal ? this : Application.Top;
top.FocusPrev ();
if (top.Focused is null)
{
top.FocusPrev ();
}
top.SetNeedsDisplay ();
Application.BringOverlappedTopToFront ();
}
else
{
Application.OverlappedMovePrevious ();
}
}
private bool OutsideTopFrame (Toplevel top)
{
if (top.Frame.X > Driver.Cols || top.Frame.Y > Driver.Rows)
{
return true;
}
return false;
}
private void QuitToplevel ()
{
if (Application.OverlappedTop is { })
{
Application.OverlappedTop.RequestStop ();
}
else
{
Application.RequestStop ();
}
}
}
///
/// 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.Compare (x.Id, y.Id);
}
}