//
// 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 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 Action Ready;
///
/// Called from after the has entered it's first iteration of the loop.
///
internal virtual void OnReady ()
{
Ready?.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 ProcessKey (KeyEvent keyEvent)
{
if (base.ProcessKey (keyEvent))
return true;
switch (keyEvent.Key) {
case Key.ControlQ:
// FIXED: stop current execution of this container
Application.RequestStop ();
break;
case Key.ControlZ:
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.ControlI: // Unix
var old = Focused;
if (!FocusNext ())
FocusNext ();
if (old != Focused) {
old?.SetNeedsDisplay ();
Focused?.SetNeedsDisplay ();
} else {
FocusNearestView (GetToplevelSubviews (true));
}
return true;
case Key.CursorLeft:
case Key.CursorUp:
case Key.BackTab:
old = Focused;
if (!FocusPrev ())
FocusPrev ();
if (old != Focused) {
old?.SetNeedsDisplay ();
Focused?.SetNeedsDisplay ();
} else {
FocusNearestView (GetToplevelSubviews (false));
}
return true;
case Key.ControlL:
Application.Refresh ();
return true;
}
return false;
}
IEnumerable GetToplevelSubviews (bool isForward)
{
if (SuperView == null) {
return null;
}
HashSet views = new HashSet ();
foreach (var v in SuperView.Subviews) {
views.Add (v);
}
return isForward ? views : views.Reverse ();
}
void FocusNearestView (IEnumerable views)
{
if (views == null) {
return;
}
bool found = false;
foreach (var v in views) {
if (v == this) {
found = true;
}
if (found && v != this) {
v.EnsureFocus ();
if (SuperView.Focused != null && SuperView.Focused != this) {
return;
}
}
}
}
///
public override void Add (View view)
{
if (this == Application.Top) {
if (view is MenuBar)
MenuBar = view as MenuBar;
if (view is StatusBar)
StatusBar = view as StatusBar;
}
base.Add (view);
}
///
public override void Remove (View view)
{
if (this is Toplevel && ((Toplevel)this).MenuBar != null) {
if (view is MenuBar) {
MenuBar?.Dispose ();
MenuBar = null;
}
if (view is StatusBar) {
StatusBar?.Dispose ();
StatusBar = null;
}
}
base.Remove (view);
}
///
public override void RemoveAll ()
{
if (this == Application.Top) {
MenuBar?.Dispose ();
MenuBar = null;
StatusBar?.Dispose ();
StatusBar = null;
}
base.RemoveAll ();
}
internal void EnsureVisibleBounds (Toplevel top, int x, int y, out int nx, out int ny)
{
nx = Math.Max (x, 0);
nx = nx + top.Frame.Width > Driver.Cols ? Math.Max (Driver.Cols - top.Frame.Width, 0) : nx;
bool m, s;
if (SuperView == null || SuperView.GetType () != typeof (Toplevel)) {
m = Application.Top.MenuBar != null;
} else {
m = ((Toplevel)SuperView).MenuBar != null;
}
int l;
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;
} else {
s = ((Toplevel)SuperView).StatusBar != null;
}
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;
}
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 is Pos.PosAbsolute && top.Bounds.X != nx) {
top.X = nx;
}
if (top.Y is Pos.PosAbsolute && top.Bounds.Y != ny) {
top.Y = ny;
}
}
if (StatusBar != null) {
if (ny + top.Frame.Height > top.Frame.Height - 1) {
if (top.Height is Dim.DimFill)
top.Height = Dim.Fill () - 1;
}
if (StatusBar.Frame.Y != Frame.Height - 1) {
StatusBar.Y = Frame.Height - 1;
SetNeedsDisplay ();
}
}
}
///
public override void Redraw (Rect bounds)
{
Application.CurrentView = this;
if (IsCurrentTop || this == Application.Top) {
if (NeedDisplay != null && !NeedDisplay.IsEmpty) {
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);
}
}
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 ();
}
}
}