diff --git a/Application.cs b/Application.cs
index fa6e7bd16..611094835 100644
--- a/Application.cs
+++ b/Application.cs
@@ -12,7 +12,7 @@ using System.Collections.Generic;
namespace Terminal {
public class Responder {
- public virtual bool CanFocus => true;
+ public virtual bool CanFocus { get; set; }
public bool HasFocus { get; internal set; }
// Key handling
@@ -49,23 +49,96 @@ namespace Terminal {
public View (Rect frame)
{
this.Frame = frame;
+ CanFocus = false;
}
+ ///
+ /// Invoke to flag that this view needs to be redisplayed, by any code
+ /// that alters the state of the view.
+ ///
public void SetNeedsDisplay ()
{
NeedDisplay = true;
}
- public void AddSubview (View view)
+ ///
+ /// Adds a subview to this view.
+ ///
+ ///
+ ///
+ public void Add (View view)
{
if (view == null)
return;
if (subviews == null)
subviews = new List ();
subviews.Add (view);
+ view.container = this;
+ if (view.CanFocus)
+ CanFocus = true;
}
- public void GetRealRowCol (int col, int row, out int rcol, out int rrow)
+ ///
+ /// Removes all the widgets from this container.
+ ///
+ ///
+ ///
+ public virtual void RemoveAll ()
+ {
+ if (subviews == null)
+ return;
+
+ while (subviews.Count > 0) {
+ var view = subviews [0];
+ Remove (view);
+ subviews.RemoveAt (0);
+ }
+ }
+
+ ///
+ /// Removes a widget from this container.
+ ///
+ ///
+ ///
+ public virtual void Remove (View view)
+ {
+ if (view == null)
+ return;
+
+ subviews.Remove (view);
+ view.container = null;
+
+ if (subviews.Count < 1)
+ this.CanFocus = false;
+ }
+
+ ///
+ /// Clears the view region with the current color.
+ ///
+ ///
+ ///
+ /// This clears the entire region used by this view.
+ ///
+ ///
+ public void Clear ()
+ {
+ var h = Frame.Height;
+ var w = Frame.Width;
+ for (int line = 0; line < h; line++) {
+ Move (0, line);
+ for (int col = 0; col < w; col++)
+ Driver.AddCh (' ');
+ }
+ }
+
+ ///
+ /// Converts the (col,row) position from the view into a screen (col,row). The values are clamped to (0..ScreenDim-1)
+ ///
+ /// View-based column.
+ /// View-based row.
+ /// Absolute column, display relative.
+ /// Absolute row, display relative.
+ internal void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true)
{
// Computes the real row, col relative to the screen.
rrow = row;
@@ -78,16 +151,40 @@ namespace Terminal {
}
// The following ensures that the cursor is always in the screen boundaries.
- rrow = Math.Max (0, Math.Min (rrow, Driver.Rows-1));
- rcol = Math.Max (0, Math.Min (rcol, Driver.Cols-1));
+ if (clipped) {
+ rrow = Math.Max (0, Math.Min (rrow, Driver.Rows - 1));
+ rcol = Math.Max (0, Math.Min (rcol, Driver.Cols - 1));
+ }
}
+
+
+ ///
+ /// Draws a frame in the current view, clipped by the boundary of this view
+ ///
+ /// Rectangular region for the frame to be drawn.
+ /// If set to true it fill will the contents.
+ public void DrawFrame (Rect rect, bool fill = false)
+ {
+ ViewToScreen (rect.X, rect.Y, out var x, out var y, clipped: false);
+ Driver.DrawFrame (new Rect (x, y, rect.Width, rect.Height), fill);
+ }
+
+ ///
+ /// This moves the cursor to the specified column and row in the view.
+ ///
+ /// The move.
+ /// Col.
+ /// Row.
public void Move (int col, int row)
{
- GetRealRowCol (col, row, out var rcol, out var rrow);
+ ViewToScreen (col, row, out var rcol, out var rrow);
Driver.Move (rcol, rrow);
}
+ ///
+ /// Positions the cursor in the right position based on the currently focused view in the chain.
+ ///
public virtual void PositionCursor ()
{
if (focused != null)
@@ -96,6 +193,12 @@ namespace Terminal {
Move (frame.X, frame.Y);
}
+ ///
+ /// Displays the specified character in the specified column and row.
+ ///
+ /// Col.
+ /// Row.
+ /// Ch.
public void AddCh (int col, int row, int ch)
{
if (row < 0 || col < 0)
@@ -106,21 +209,30 @@ namespace Terminal {
Driver.AddCh (ch);
}
+ ///
+ /// Performs a redraw of this view and its subviews, only redraws the views that have been flagged for a re-display.
+ ///
public virtual void Redraw ()
{
var clipRect = new Rect (offset, frame.Size);
- foreach (var view in subviews){
- if (view.NeedDisplay){
- if (view.Frame.IntersectsWith (clipRect)){
- view.Redraw ();
+ if (subviews != null) {
+ foreach (var view in subviews) {
+ if (view.NeedDisplay) {
+ if (view.Frame.IntersectsWith (clipRect)) {
+ view.Redraw ();
+ }
+ view.NeedDisplay = false;
}
- view.NeedDisplay = false;
}
}
NeedDisplay = false;
}
-
+
+ ///
+ /// Focuses the specified sub-view.
+ ///
+ /// View.
public void SetFocus (View view)
{
if (view == null)
@@ -129,6 +241,15 @@ namespace Terminal {
return;
if (focused == view)
return;
+
+ // Make sure that this view is a subview
+ View c;
+ for (c = view.container; c != null; c = c.container)
+ if (c == this)
+ break;
+ if (c == null)
+ throw new ArgumentException ("the specified view is not part of the hierarchy of this view");
+
if (focused != null)
focused.HasFocus = false;
focused = view;
@@ -138,12 +259,18 @@ namespace Terminal {
focused.PositionCursor ();
}
+ ///
+ /// Finds the first view in the hierarchy that wants to get the focus if nothing is currently focused, otherwise, it does nothing.
+ ///
public void EnsureFocus ()
{
if (focused == null)
FocusFirst ();
}
+ ///
+ /// Focuses the first focusable subview if one exists.
+ ///
public void FocusFirst ()
{
foreach (var view in subviews){
@@ -154,6 +281,9 @@ namespace Terminal {
}
}
+ ///
+ /// Focuses the last focusable subview if one exists.
+ ///
public void FocusLast ()
{
for (int i = subviews.Count; i > 0; ){
@@ -167,6 +297,10 @@ namespace Terminal {
}
}
+ ///
+ /// Focuses the previous view.
+ ///
+ /// true, if previous was focused, false otherwise.
public bool FocusPrev ()
{
if (focused == null){
@@ -200,6 +334,11 @@ namespace Terminal {
}
return false;
}
+
+ ///
+ /// Focuses the next view.
+ ///
+ /// true, if next was focused, false otherwise.
public bool FocusNext ()
{
if (focused == null){
@@ -239,7 +378,11 @@ namespace Terminal {
}
}
+ ///
+ /// Toplevel views can be modally executed.
+ ///
public class Toplevel : View {
+ public bool Running;
public Toplevel (Rect frame) : base (frame)
{
@@ -247,10 +390,13 @@ namespace Terminal {
public static Toplevel Create ()
{
- return new Window (new Rect (0, 0, Driver.Cols, Driver.Rows));
+ return new Toplevel (new Rect (0, 0, Driver.Cols, Driver.Rows));
}
}
+ ///
+ /// A toplevel view that draws a frame around its region
+ ///
public class Window : Toplevel {
View contentView;
string title;
@@ -265,15 +411,34 @@ namespace Terminal {
public Window (Rect frame, string title = null) : base (frame)
{
+ this.Title = title;
frame.Inflate (-1, -1);
contentView = new View (frame);
- AddSubview (contentView);
+ Add(contentView);
+ }
+
+ void DrawFrame ()
+ {
+ DrawFrame (Frame, true);
}
public override void Redraw ()
{
-
- base.Redraw ();
+ Driver.SetColor (Colors.Base.Normal);
+ Clear ();
+ DrawFrame ();
+ if (HasFocus)
+ Driver.SetColor (Colors.Dialog.Normal);
+ var width = Frame.Width;
+ if (Title != null && width > 4) {
+ Move (0, 1);
+ Driver.AddCh (' ');
+ var str = Title.Length > width ? Title.Substring (0, width - 4) : Title;
+ Driver.AddStr (str);
+ Driver.AddCh (' ');
+ }
+ Driver.SetColor (Colors.Dialog.Normal);
+ contentView.Redraw ();
}
}
@@ -285,6 +450,15 @@ namespace Terminal {
static Stack toplevels = new Stack ();
static Responder focus;
+ ///
+ /// This event is raised on each iteration of the
+ /// main loop.
+ ///
+ ///
+ /// See also
+ ///
+ static public event EventHandler Iteration;
+
public static void MakeFirstResponder (Responder newResponder)
{
if (newResponder == null)
@@ -293,11 +467,15 @@ namespace Terminal {
throw new NotImplementedException ();
}
+ ///
+ /// Initializes the Application
+ ///
public static void Init ()
{
if (Top != null)
return;
+ Driver.Init ();
MainLoop = new Mono.Terminal.MainLoop ();
Top = Toplevel.Create ();
focus = Top;
@@ -310,11 +488,11 @@ namespace Terminal {
}
public class RunState : IDisposable {
- internal RunState (View view)
+ internal RunState (Toplevel view)
{
- View = view;
+ Toplevel = view;
}
- internal View View;
+ internal Toplevel Toplevel;
public void Dispose ()
{
@@ -324,35 +502,29 @@ namespace Terminal {
public virtual void Dispose (bool disposing)
{
- if (View != null){
- Application.End (View);
- View = null;
+ if (Toplevel != null){
+ Application.End (Toplevel);
+ Toplevel = null;
}
}
}
- public void Run ()
+ static public RunState Begin (Toplevel toplevel)
{
- Run (Top);
- }
-
- static public RunState Begin (View view)
- {
- if (view == null)
- throw new ArgumentNullException ("view");
- var rs = new RunState (view);
+ if (toplevel == null)
+ throw new ArgumentNullException (nameof(toplevel));
+ var rs = new RunState (toplevel);
Init ();
Driver.PrepareToRun ();
- toplevels.Push (view);
+ toplevels.Push (toplevel);
- view.LayoutSubviews ();
- view.FocusFirst ();
- Redraw (view);
- view.PositionCursor ();
+ toplevel.LayoutSubviews ();
+ toplevel.FocusFirst ();
+ Redraw (toplevel);
+ toplevel.PositionCursor ();
Driver.Refresh ();
-
return rs;
}
@@ -405,9 +577,49 @@ namespace Terminal {
Refresh ();
}
- public void Run (View view)
+ ///
+ /// Runs the main loop for the created dialog
+ ///
+ ///
+ /// Use the wait parameter to control whether this is a
+ /// blocking or non-blocking call.
+ ///
+ public static void RunLoop(RunState state, bool wait = true)
{
-
+ if (state == null)
+ throw new ArgumentNullException(nameof(state));
+ if (state.Toplevel == null)
+ throw new ObjectDisposedException("state");
+
+ for (state.Toplevel.Running = true; state.Toplevel.Running;) {
+ if (MainLoop.EventsPending(wait)){
+ MainLoop.MainIteration();
+ if (Iteration != null)
+ Iteration(null, EventArgs.Empty);
+ }
+ else if (wait == false)
+ return;
+ }
+ }
+
+ public static void Run ()
+ {
+ Run (Top);
+ }
+
+ ///
+ /// Runs the main loop on the given container.
+ ///
+ ///
+ /// This method is used to start processing events
+ /// for the main application, but it is also used to
+ /// run modal dialog boxes.
+ ///
+ public static void Run (Toplevel view)
+ {
+ var runToken = Begin (view);
+ RunLoop (runToken);
+ End (runToken);
}
}
}
\ No newline at end of file
diff --git a/Terminal.csproj b/Terminal.csproj
index 97d44d2bd..13221072c 100644
--- a/Terminal.csproj
+++ b/Terminal.csproj
@@ -35,9 +35,9 @@
-
-
-
+
+
+
diff --git a/demo.cs b/demo.cs
index 17ce5b4ce..0fe06b99c 100644
--- a/demo.cs
+++ b/demo.cs
@@ -3,7 +3,9 @@ using Terminal;
class Demo {
static void Main ()
{
- var app = new Application ();
-
+ Application.Init ();
+ var top = Application.Top;
+ top.Add (new Window (new Rect (10, 10, 20, 20), "Hello"));
+ Application.Run ();
}
}
\ No newline at end of file
diff --git a/driver.cs b/driver.cs
index bd3c3f762..6fbab20c9 100644
--- a/driver.cs
+++ b/driver.cs
@@ -4,29 +4,45 @@ using Unix.Terminal;
namespace Terminal {
- public class ColorScheme {
- public int Normal;
- public int Focus;
- public int HotNormal;
- public int HotFocus;
- public int Marked => HotNormal;
- public int MarkedSelected => HotFocus;
+ public struct Color {
+ internal int value;
+ public Color (int v)
+ {
+ value = v;
+ }
+ public static implicit operator int (Color c) => c.value;
+ public static implicit operator Color (int v) => new Color (v);
+ }
+
+ public class ColorScheme {
+ public Color Normal;
+ public Color Focus;
+ public Color HotNormal;
+ public Color HotFocus;
+ public Color Marked => HotNormal;
+ public Color MarkedSelected => HotFocus;
+ }
+
+ public static class Colors {
+ public static ColorScheme Base, Dialog, Menu, Error;
}
public abstract class ConsoleDriver {
public abstract int Cols {get;}
public abstract int Rows {get;}
public abstract void Init ();
- public abstract void Move (int line, int col);
+ public abstract void Move (int col, int row);
public abstract void AddCh (int ch);
+ public abstract void AddStr (string str);
public abstract void PrepareToRun ();
public abstract void Refresh ();
public abstract void End ();
public abstract void RedrawTop ();
-
+ public abstract void SetColor (Color c);
+ public abstract void DrawFrame (Rect region, bool fill);
+
// Colors used for widgets
- public static ColorScheme ColorBase, ColorDialog, ColorMenu, ColorError;
}
public class CursesDriver : ConsoleDriver {
@@ -35,16 +51,18 @@ namespace Terminal {
public override void Move(int col, int row) => Curses.move (row, col);
public override void AddCh(int ch) => Curses.addch (ch);
+ public override void AddStr (string str) => Curses.addstr (str);
public override void Refresh() => Curses.refresh ();
public override void End() => Curses.endwin ();
public override void RedrawTop() => window.redrawwin ();
+ public override void SetColor (Color c) => Curses.attrset (c.value);
public Curses.Window window;
static short last_color_pair;
- static int MakeColor (short f, short b)
+ static Color MakeColor (short f, short b)
{
Curses.InitColorPair (++last_color_pair, f, b);
- return Curses.ColorPair (last_color_pair);
+ return new Color () { value = Curses.ColorPair (last_color_pair) };
}
public override void PrepareToRun()
@@ -52,6 +70,35 @@ namespace Terminal {
Curses.timeout (-1);
}
+ public override void DrawFrame (Rect region, bool fill)
+ {
+ int width = region.Width;
+ int height = region.Height;
+ int b;
+
+ Curses.move (region.Y, region.X);
+ Curses.addch (Curses.ACS_ULCORNER);
+ for (b = 0; b < width - 2; b++)
+ Curses.addch (Curses.ACS_HLINE);
+ Curses.addch (Curses.ACS_URCORNER);
+
+ for (b = 1; b < height - 1; b++) {
+ Curses.move (region.Y + b, region.X);
+ Curses.addch (Curses.ACS_VLINE);
+ if (fill) {
+ for (int x = 1; x < width - 1; x++)
+ Curses.addch (' ');
+ } else
+ Curses.move (region.Y + b, region.X + width - 1);
+ Curses.addch (Curses.ACS_VLINE);
+ }
+ Curses.move (region.Y + height - 1, region.X);
+ Curses.addch (Curses.ACS_LLCORNER);
+ for (b = 0; b < width - 2; b++)
+ Curses.addch (Curses.ACS_HLINE);
+ Curses.addch (Curses.ACS_LRCORNER);
+ }
+
public override void Init()
{
if (window != null)
@@ -66,53 +113,49 @@ namespace Terminal {
Curses.noecho ();
Curses.Window.Standard.keypad (true);
- ColorBase = new ColorScheme ();
- ColorDialog = new ColorScheme ();
- ColorMenu = new ColorScheme ();
- ColorError = new ColorScheme ();
+ Colors.Base = new ColorScheme ();
+ Colors.Dialog = new ColorScheme ();
+ Colors.Menu = new ColorScheme ();
+ Colors.Error = new ColorScheme ();
if (Curses.HasColors){
Curses.StartColor ();
Curses.UseDefaultColors ();
- ColorBase.Normal = MakeColor (Curses.COLOR_WHITE, Curses.COLOR_BLUE);
- ColorBase.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN);
- ColorBase.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLUE);
- ColorBase.HotFocus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN);
+ Colors.Base.Normal = MakeColor (Curses.COLOR_WHITE, Curses.COLOR_BLUE);
+ Colors.Base.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN);
+ Colors.Base.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLUE);
+ Colors.Base.HotFocus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN);
+ Colors.Menu.Normal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_CYAN);
+ Colors.Menu.Focus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN);
+ Colors.Menu.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_BLACK);
+ Colors.Menu.HotFocus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLACK);
+ Colors.Dialog.Normal = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE);
+ Colors.Dialog.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN);
+ Colors.Dialog.HotNormal = MakeColor (Curses.COLOR_BLUE, Curses.COLOR_WHITE);
+ Colors.Dialog.HotFocus = MakeColor (Curses.COLOR_BLUE, Curses.COLOR_CYAN);
- ColorMenu.Normal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_CYAN);
- ColorMenu.Focus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN);
- ColorMenu.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_BLACK);
- ColorMenu.HotFocus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLACK);
- ColorDialog.Normal = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE);
- ColorDialog.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN);
- ColorDialog.HotNormal = MakeColor (Curses.COLOR_BLUE, Curses.COLOR_WHITE);
- ColorDialog.HotFocus = MakeColor (Curses.COLOR_BLUE, Curses.COLOR_CYAN);
-
- ColorError.Normal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_RED);
- ColorError.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE);
- ColorError.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_RED);
- ColorError.HotFocus = ColorError.HotNormal;
+ Colors.Error.Normal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_RED);
+ Colors.Error.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE);
+ Colors.Error.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_RED);
+ Colors.Error.HotFocus = Colors.Error.HotNormal;
} else {
- ColorBase.Normal = Curses.A_NORMAL;
- ColorBase.Focus = Curses.A_REVERSE;
- ColorBase.HotNormal = Curses.A_BOLD;
- ColorBase.HotFocus = Curses.A_BOLD | Curses.A_REVERSE;
-
- ColorMenu.Normal = Curses.A_REVERSE;
- ColorMenu.Focus = Curses.A_NORMAL;
- ColorMenu.HotNormal = Curses.A_BOLD;
- ColorMenu.HotFocus = Curses.A_NORMAL;
-
- ColorDialog.Normal = Curses.A_REVERSE;
- ColorDialog.Focus = Curses.A_NORMAL;
- ColorDialog.HotNormal = Curses.A_BOLD;
- ColorDialog.HotFocus = Curses.A_NORMAL;
-
- ColorError.Normal = Curses.A_BOLD;
- ColorError.Focus = Curses.A_BOLD | Curses.A_REVERSE;
- ColorError.HotNormal = Curses.A_BOLD | Curses.A_REVERSE;
- ColorError.HotFocus = Curses.A_REVERSE;
+ Colors.Base.Normal = Curses.A_NORMAL;
+ Colors.Base.Focus = Curses.A_REVERSE;
+ Colors.Base.HotNormal = Curses.A_BOLD;
+ Colors.Base.HotFocus = Curses.A_BOLD | Curses.A_REVERSE;
+ Colors.Menu.Normal = Curses.A_REVERSE;
+ Colors.Menu.Focus = Curses.A_NORMAL;
+ Colors.Menu.HotNormal = Curses.A_BOLD;
+ Colors.Menu.HotFocus = Curses.A_NORMAL;
+ Colors.Dialog.Normal = Curses.A_REVERSE;
+ Colors.Dialog.Focus = Curses.A_NORMAL;
+ Colors.Dialog.HotNormal = Curses.A_BOLD;
+ Colors.Dialog.HotFocus = Curses.A_NORMAL;
+ Colors.Error.Normal = Curses.A_BOLD;
+ Colors.Error.Focus = Curses.A_BOLD | Curses.A_REVERSE;
+ Colors.Error.HotNormal = Curses.A_BOLD | Curses.A_REVERSE;
+ Colors.Error.HotFocus = Curses.A_REVERSE;
}
}
}