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