#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); } }