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