// // Toplevel.cs: Toplevel views can be modally executed // // Authors: // Miguel de Icaza (miguel@gnome.org) // using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; namespace Terminal.Gui { /// /// Toplevel views can be modally executed. /// /// /// /// Toplevels can be modally executing 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 initialzies Terminal.Gui by callling . /// 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 . /// /// /// Toplevels can also opt-in to more sophisticated initialization /// by implementing . When they do /// so, the and /// methods will be called /// before running the view. /// If first-run-only initialization is preferred, the /// can be implemented too, in which case the /// methods will only be called if /// is . This allows proper inheritance hierarchies /// to override base class layout code optimally by doing so only on first run, /// instead of on every run. /// /// public class Toplevel : View { /// /// Gets or sets whether the for this is running or not. /// /// /// Setting this property directly is discouraged. Use instead. /// public bool Running { get; set; } /// /// Fired once the Toplevel's has begin loaded. /// A Loaded event handler is a good place to finalize initialization before calling `. /// public event Action Loaded; /// /// Fired once the Toplevel's 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 `(topLevel)`. /// public event Action Ready; /// /// Fired once the Toplevel's has begin unloaded. /// A Unloaded event handler is a good place to disposing after calling `. /// public event Action Unloaded; /// /// Called from before the is redraws for the first time. /// internal virtual void OnLoaded () { Loaded?.Invoke (); } /// /// Called from after the has entered it's first iteration of the loop. /// internal virtual void OnReady () { Ready?.Invoke (); } /// /// Called from before the is disposed. /// internal virtual void OnUnloaded () { Unloaded?.Invoke (); } /// /// Initializes a new instance of the class with the specified absolute layout. /// /// A superview-relative rectangle specifying the location and size for the new Toplevel public Toplevel (Rect frame) : base (frame) { Initialize (); } /// /// Initializes a new instance of the class with layout, defaulting to full screen. /// public Toplevel () : base () { Initialize (); Width = Dim.Fill (); Height = Dim.Fill (); } void Initialize () { ColorScheme = Colors.Base; } /// /// Convenience factory method that creates a new Toplevel with the current terminal dimensions. /// /// The create. public static Toplevel Create () { return new Toplevel (new Rect (0, 0, Driver.Cols, Driver.Rows)); } /// /// Gets or sets a value indicating whether this can focus. /// /// true if can focus; otherwise, false. public override bool CanFocus { get => true; } /// /// Determines whether the is modal or not. /// Causes to propagate keys upwards /// by default unless set to . /// public bool Modal { get; set; } /// /// Gets or sets the menu for this Toplevel /// public MenuBar MenuBar { get; set; } /// /// Gets or sets the status bar for this Toplevel /// public StatusBar StatusBar { get; set; } /// public override bool OnKeyDown (KeyEvent keyEvent) { if (base.OnKeyDown (keyEvent)) { return true; } switch (keyEvent.Key) { case Key.AltMask: case Key.AltMask | Key.Space: case Key.CtrlMask | Key.Space: if (MenuBar != null && MenuBar.OnKeyDown (keyEvent)) { return true; } break; } return false; } /// public override bool OnKeyUp (KeyEvent keyEvent) { if (base.OnKeyUp (keyEvent)) { return true; } switch (keyEvent.Key) { case Key.AltMask: case Key.AltMask | Key.Space: case Key.CtrlMask | Key.Space: if (MenuBar != null && MenuBar.OnKeyUp (keyEvent)) { return true; } break; } return false; } /// public override bool ProcessKey (KeyEvent keyEvent) { if (base.ProcessKey (keyEvent)) return true; switch (ShortcutHelper.GetModifiersKey (keyEvent)) { case Key.Q | Key.CtrlMask: // FIXED: stop current execution of this container Application.RequestStop (); break; case Key.Z | Key.CtrlMask: Driver.Suspend (); return true; #if false case Key.F5: Application.DebugDrawBounds = !Application.DebugDrawBounds; SetNeedsDisplay (); return true; #endif case Key.Tab: case Key.CursorRight: case Key.CursorDown: case Key.I | Key.CtrlMask: // Unix var old = GetDeepestFocusedSubview (Focused); if (!FocusNext ()) FocusNext (); if (old != Focused && old != Focused?.Focused) { old?.SetNeedsDisplay (); Focused?.SetNeedsDisplay (); } else { FocusNearestView (SuperView?.TabIndexes, Direction.Forward); } return true; case Key.BackTab | Key.ShiftMask: case Key.CursorLeft: case Key.CursorUp: old = GetDeepestFocusedSubview (Focused); if (!FocusPrev ()) FocusPrev (); if (old != Focused && old != Focused?.Focused) { old?.SetNeedsDisplay (); Focused?.SetNeedsDisplay (); } else { FocusNearestView (SuperView?.TabIndexes?.Reverse(), Direction.Backward); } return true; case Key.Tab | Key.CtrlMask: Application.Top.FocusNext (); return true; case Key.Tab | Key.ShiftMask | Key.CtrlMask: Application.Top.FocusPrev (); return true; case Key.L | Key.CtrlMask: Application.Refresh (); return true; } return false; } /// public override bool ProcessColdKey (KeyEvent keyEvent) { if (base.ProcessColdKey (keyEvent)) { return true; } if (ShortcutHelper.FindAndOpenByShortcut(keyEvent, this)) { return true; } return false; } View GetDeepestFocusedSubview (View view) { if (view == null) { return null; } foreach (var v in view.Subviews) { if (v.HasFocus) { return GetDeepestFocusedSubview (v); } } return view; } void FocusNearestView (IEnumerable views, Direction direction) { if (views == null) { return; } bool found = false; bool focusProcessed = false; int idx = 0; foreach (var v in views) { if (v == this) { found = true; } if (found && v != this) { if (direction == Direction.Forward) { SuperView?.FocusNext (); } else { SuperView?.FocusPrev (); } focusProcessed = true; if (SuperView.Focused != null && SuperView.Focused != this) { return; } } else if (found && !focusProcessed && idx == views.Count () - 1) { views.ToList () [0].SetFocus (); } idx++; } } /// public override void Add (View view) { if (this == Application.Top) { AddMenuStatusBar (view); } base.Add (view); } internal void AddMenuStatusBar (View view) { if (view is MenuBar) { MenuBar = view as MenuBar; } if (view is StatusBar) { StatusBar = view as StatusBar; } } /// public override void Remove (View view) { if (this is Toplevel toplevel && toplevel.MenuBar != null) { RemoveMenuStatusBar (view); } base.Remove (view); } /// public override void RemoveAll () { if (this == Application.Top) { MenuBar?.Dispose (); MenuBar = null; StatusBar?.Dispose (); StatusBar = null; } base.RemoveAll (); } internal void RemoveMenuStatusBar (View view) { if (view is MenuBar) { MenuBar?.Dispose (); MenuBar = null; } if (view is StatusBar) { StatusBar?.Dispose (); StatusBar = null; } } internal void EnsureVisibleBounds (Toplevel top, int x, int y, out int nx, out int ny) { nx = Math.Max (x, 0); int l; if (SuperView == null || SuperView is Toplevel) { l = Driver.Cols; } else { l = SuperView.Frame.Width; } nx = nx + top.Frame.Width > l ? Math.Max (l - top.Frame.Width, 0) : nx; SetWidth (top.Frame.Width, out int rWidth); if (rWidth < 0 && nx >= top.Frame.X) { nx = Math.Max (top.Frame.Right - 2, 0); } //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); bool m, s; if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) { m = Application.Top.MenuBar != null; } else { m = ((Toplevel)SuperView).MenuBar != null; } if (SuperView == null || SuperView is Toplevel) { l = m ? 1 : 0; } else { l = 0; } ny = Math.Max (y, l); if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) { s = Application.Top.StatusBar != null && Application.Top.StatusBar.Visible; } else { s = ((Toplevel)SuperView).StatusBar != null && ((Toplevel)SuperView).StatusBar.Visible; } if (SuperView == null || SuperView is Toplevel) { l = s ? Driver.Rows - 1 : Driver.Rows; } else { l = s ? SuperView.Frame.Height - 1 : SuperView.Frame.Height; } ny = Math.Min (ny, l); ny = ny + top.Frame.Height > l ? Math.Max (l - top.Frame.Height, m ? 1 : 0) : ny; SetHeight (top.Frame.Height, out int rHeight); if (rHeight < 0 && ny >= top.Frame.Y) { ny = Math.Max (top.Frame.Bottom - 2, 0); } //System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}"); } internal void PositionToplevels () { PositionToplevel (this); foreach (var top in Subviews) { if (top is Toplevel) { PositionToplevel ((Toplevel)top); } } } private void PositionToplevel (Toplevel top) { EnsureVisibleBounds (top, top.Frame.X, top.Frame.Y, out int nx, out int ny); if ((nx != top.Frame.X || ny != top.Frame.Y) && top.LayoutStyle == LayoutStyle.Computed) { if ((top.X == null || top.X is Pos.PosAbsolute) && top.Bounds.X != nx) { top.X = nx; } if ((top.Y == null || top.Y is Pos.PosAbsolute) && top.Bounds.Y != ny) { top.Y = ny; } } if (top.StatusBar != null) { if (ny + top.Frame.Height > top.Frame.Height - (top.StatusBar.Visible ? 1 : 0)) { if (top.Height is Dim.DimFill) top.Height = Dim.Fill () - (top.StatusBar.Visible ? 1 : 0); } if (top.StatusBar.Frame.Y != top.Frame.Height - (top.StatusBar.Visible ? 1 : 0)) { top.StatusBar.Y = top.Frame.Height - (top.StatusBar.Visible ? 1 : 0); top.LayoutSubviews (); } top.BringSubviewToFront (top.StatusBar); } } /// public override void Redraw (Rect bounds) { if (IsCurrentTop || this == Application.Top) { if (!NeedDisplay.IsEmpty || LayoutNeeded) { Driver.SetAttribute (Colors.TopLevel.Normal); // This is the Application.Top. Clear just the region we're being asked to redraw // (the bounds passed to us). Clear (bounds); Driver.SetAttribute (Colors.Base.Normal); PositionToplevels (); foreach (var view in Subviews) { if (view.Frame.IntersectsWith (bounds)) { view.SetNeedsLayout (); view.SetNeedsDisplay (view.Bounds); } } ClearLayoutNeeded (); ClearNeedsDisplay (); } } base.Redraw (base.Bounds); } /// /// Invoked by as part of the after /// the views have been laid out, and before the views are drawn for the first time. /// public virtual void WillPresent () { FocusFirst (); } } }