diff --git a/README.md b/README.md index 028a67584..80982b08b 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,74 @@ This is a simple UI toolkit for .NET. It is an updated version of [gui.cs](https://github.com/mono/mono-curses/blob/master/gui.cs) that -I wrote for [mono-curses](https://github.com/mono/mono-curses) +I wrote for [mono-curses](https://github.com/mono/mono-curses). + +The toolkit contains various controls (labesl, text entry, buttons, +radio buttons, checkboxes, dialog boxes, windows, menus) for building +text user interfaces, a main loop, is designed to work on Curses and +the [Windows +Console](https://github.com/migueldeicaza/gui.cs/issues/27), works +well on both color and monochrome terminals and has mouse support on +terminal emulators that support it. # API Documentation Go to the [API documentation](https://migueldeicaza.github.io/gui.cs/api/Terminal.html) for details. +# Sample Usage + +``` +using Terminal.Gui; + +class Demo { + static void Main () + { + Application.Init (); + var top = Application.Top; + + // Creates the top-level window to show + var win = new Window (new Rect (0, 1, top.Frame.Width, top.Frame.Height-1), "MyApp"); + top.Add (win); + + // Creates a menubar, the item "New" has a help menu. + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_New", "Creates new file", NewFile), + new MenuItem ("_Close", "", () => Close ()), + new MenuItem ("_Quit", "", () => { if (Quit ()) top.Running = false; }) + }), + new MenuBarItem ("_Edit", new MenuItem [] { + new MenuItem ("_Copy", "", null), + new MenuItem ("C_ut", "", null), + new MenuItem ("_Paste", "", null) + }) + }); + top.Add (menu); + + // Add some controls + win.Add ( + new Label (3, 2, "Login: "), + new TextField (14, 2, 40, ""), + new Label (3, 4, "Password: "), + new TextField (14, 4, 40, "") { Secret = true }, + new CheckBox (3, 6, "Remember me"), + new RadioGroup (3, 8, new [] { "_Personal", "_Company" }), + new Button (3, 14, "Ok"), + new Button (10, 14, "Cancel"), + new Label (3, 18, "Press ESC and 9 to activate the menubar")); + + ShowEntries (win); + + Application.Run (); + } +} +``` + +This shows a UI like this: + # Running and Building -To run this, you will need a peer checkout of mono-ncurses to this -directory. I should make a NuGet package at some point. +Open the solution and run the sample program. # Input Handling diff --git a/Core.cs b/Terminal.Gui/Core.cs similarity index 100% rename from Core.cs rename to Terminal.Gui/Core.cs diff --git a/Driver.cs b/Terminal.Gui/Driver.cs similarity index 100% rename from Driver.cs rename to Terminal.Gui/Driver.cs diff --git a/Event.cs b/Terminal.Gui/Event.cs similarity index 100% rename from Event.cs rename to Terminal.Gui/Event.cs diff --git a/Terminal.Gui/MonoCurses/README.md b/Terminal.Gui/MonoCurses/README.md new file mode 100644 index 000000000..4ea82ddaf --- /dev/null +++ b/Terminal.Gui/MonoCurses/README.md @@ -0,0 +1,13 @@ +This directory contains a copy of the MonoCurses binding from: + +http://github.com/mono/mono-curses + +The source code has been exported in a way that the MonoCurses +API is kept private and does not surface to the user, this is +done with the command: + +``` + make publish-to-gui +``` + +In the MonoCurses package diff --git a/Terminal.Gui/MonoCurses/binding.cs b/Terminal.Gui/MonoCurses/binding.cs new file mode 100644 index 000000000..e08024f1e --- /dev/null +++ b/Terminal.Gui/MonoCurses/binding.cs @@ -0,0 +1,382 @@ + +// +// binding.cs.in: Core binding for curses. +// +// Authors: +// Miguel de Icaza (miguel.de.icaza@gmail.com) +// +// Copyright (C) 2007 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Unix.Terminal { + + internal partial class Curses { + + [StructLayout (LayoutKind.Sequential)] + internal struct MouseEvent { + public short ID; + public int X, Y, Z; + public Event ButtonState; + } + +#region Screen initialization + + [DllImport ("ncurses", EntryPoint="initscr")] + extern static internal IntPtr real_initscr (); + static int lines, cols; + + static Window main_window; + static IntPtr curses_handle, curscr_ptr, lines_ptr, cols_ptr; + + static void FindNCurses () + { + if (File.Exists ("/usr/lib/libncurses.dylib")) + curses_handle = dlopen ("libncurses.dylib", 0); + else + curses_handle = dlopen ("libncurses.so", 0); + + if (curses_handle == IntPtr.Zero) + throw new Exception ("Could not dlopen ncurses"); + + stdscr = read_static_ptr ("stdscr"); + curscr_ptr = get_ptr ("curscr"); + lines_ptr = get_ptr ("LINES"); + cols_ptr = get_ptr ("COLS"); + } + + static public Window initscr () + { + FindNCurses (); + + main_window = new Window (real_initscr ()); + try { + console_sharp_get_dims (out lines, out cols); + } catch (DllNotFoundException){ + endwin (); + Console.Error.WriteLine ("Unable to find the @MONO_CURSES@ native library\n" + + "this is different than the managed mono-curses.dll\n\n" + + "Typically you need to install to a LD_LIBRARY_PATH directory\n" + + "or DYLD_LIBRARY_PATH directory or run /sbin/ldconfig"); + Environment.Exit (1); + } + return main_window; + } + + public static int Lines { + get { + return lines; + } + } + + public static int Cols { + get { + return cols; + } + } + + // + // Returns true if the window changed since the last invocation, as a + // side effect, the Lines and Cols properties are updated + // + public static bool CheckWinChange () + { + int l, c; + + console_sharp_get_dims (out l, out c); + if (l != lines || c != cols){ + lines = l; + cols = c; + return true; + } + return false; + } + + [DllImport ("ncurses")] + extern static public int endwin (); + + [DllImport ("ncurses")] + extern static public bool isendwin (); + + // + // Screen operations are flagged as internal, as we need to + // catch all changes so we can update newscr, curscr, stdscr + // + [DllImport ("ncurses")] + extern static public IntPtr internal_newterm (string type, IntPtr file_outfd, IntPtr file_infd); + + [DllImport ("ncurses")] + extern static public IntPtr internal_set_term (IntPtr newscreen); + + [DllImport ("ncurses")] + extern static internal void internal_delscreen (IntPtr sp); +#endregion + +#region Input Options + [DllImport ("ncurses")] + extern static public int cbreak (); + + [DllImport ("ncurses")] + extern static public int nocbreak (); + + [DllImport ("ncurses")] + extern static public int echo (); + + [DllImport ("ncurses")] + extern static public int noecho (); + + [DllImport ("ncurses")] + extern static public int halfdelay (int t); + + [DllImport ("ncurses")] + extern static public int raw (); + + [DllImport ("ncurses")] + extern static public int noraw (); + + [DllImport ("ncurses")] + extern static public void noqiflush (); + + [DllImport ("ncurses")] + extern static public void qiflush (); + + [DllImport ("ncurses")] + extern static public int typeahead (IntPtr fd); + + [DllImport ("ncurses")] + extern static public int timeout (int delay); + + // + // Internal, as they are exposed in Window + // + [DllImport ("ncurses")] + extern static internal int wtimeout (IntPtr win, int delay); + + [DllImport ("ncurses")] + extern static internal int notimeout (IntPtr win, bool bf); + + [DllImport ("ncurses")] + extern static internal int keypad (IntPtr win, bool bf); + + [DllImport ("ncurses")] + extern static internal int meta (IntPtr win, bool bf); + + [DllImport ("ncurses")] + extern static internal int intrflush (IntPtr win, bool bf); +#endregion + +#region Output Options + [DllImport ("ncurses")] + extern internal static int clearok (IntPtr win, bool bf); + [DllImport ("ncurses")] + extern internal static int idlok (IntPtr win, bool bf); + [DllImport ("ncurses")] + extern internal static void idcok (IntPtr win, bool bf); + [DllImport ("ncurses")] + extern internal static void immedok (IntPtr win, bool bf); + [DllImport ("ncurses")] + extern internal static int leaveok (IntPtr win, bool bf); + [DllImport ("ncurses")] + extern internal static int wsetscrreg (IntPtr win, int top, int bot); + [DllImport ("ncurses")] + extern internal static int scrollok (IntPtr win, bool bf); + + [DllImport ("ncurses")] + extern public static int nl(); + [DllImport ("ncurses")] + extern public static int nonl(); + [DllImport ("ncurses")] + extern public static int setscrreg (int top, int bot); + +#endregion + +#region refresh functions + + [DllImport ("ncurses")] + extern public static int refresh (); + [DllImport ("ncurses")] + extern public static int doupdate(); + + [DllImport ("ncurses")] + extern internal static int wrefresh (IntPtr win); + [DllImport ("ncurses")] + extern internal static int redrawwin (IntPtr win); + [DllImport ("ncurses")] + extern internal static int wredrawwin (IntPtr win, int beg_line, int num_lines); + [DllImport ("ncurses")] + extern internal static int wnoutrefresh (IntPtr win); +#endregion + +#region Output + [DllImport ("ncurses")] + extern public static int move (int line, int col); + + [DllImport ("ncurses", EntryPoint="addch")] + extern internal static int _addch (int ch); + + [DllImport ("ncurses")] + extern public static int addstr (string s); + + public static int addstr (string format, params object [] args) + { + var s = string.Format (format, args); + return addstr (s); + } + + static char [] r = new char [1]; + + // + // Have to wrap the native addch, as it can not + // display unicode characters, we have to use addstr + // for that. but we need addch to render special ACS + // characters + // + public static int addch (int ch) + { + if (ch < 127 || ch > 0xffff ) + return _addch (ch); + char c = (char) ch; + return addstr (new String (c, 1)); + } + + [DllImport ("ncurses")] + extern internal static int wmove (IntPtr win, int line, int col); + + [DllImport ("ncurses")] + extern internal static int waddch (IntPtr win, int ch); +#endregion + +#region Attributes + [DllImport ("ncurses")] + extern public static int attron (int attrs); + [DllImport ("ncurses")] + extern public static int attroff (int attrs); + [DllImport ("ncurses")] + extern public static int attrset (int attrs); +#endregion + +#region Input + [DllImport ("ncurses")] + extern public static int getch (); + + [DllImport ("ncurses")] + extern public static int get_wch (out int sequence); + + [DllImport ("ncurses")] + extern public static int ungetch (int ch); + + [DllImport ("ncurses")] + extern public static int mvgetch (int y, int x); +#endregion + +#region Colors + [DllImport ("ncurses")] + extern internal static bool has_colors (); + public static bool HasColors => has_colors (); + + [DllImport ("ncurses")] + extern internal static int start_color (); + public static int StartColor () => start_color (); + + [DllImport ("ncurses")] + extern internal static int init_pair (short pair, short f, short b); + public static int InitColorPair (short pair, short foreground, short background) => init_pair (pair, foreground, background); + + [DllImport ("ncurses")] + extern internal static int use_default_colors (); + public static int UseDefaultColors () => use_default_colors (); + + [DllImport ("ncurses")] + extern internal static int COLOR_PAIRS(); + public static int ColorPairs => COLOR_PAIRS(); + + +#endregion + + [DllImport ("libc")] + extern static IntPtr dlopen (string file, int mode); + + [DllImport ("libc")] + extern static IntPtr dlsym (IntPtr handle, string symbol); + + static IntPtr stdscr; + + static IntPtr get_ptr (string key) + { + var ptr = dlsym (curses_handle, key); + if (ptr == IntPtr.Zero) + throw new Exception ("Could not load the key " + key); + return ptr; + } + + internal static IntPtr read_static_ptr (string key) + { + var ptr = get_ptr (key); + return Marshal.ReadIntPtr (ptr); + } + + internal static IntPtr console_sharp_get_stdscr () => stdscr; + + +#region Helpers + internal static IntPtr console_sharp_get_curscr () + { + return Marshal.ReadIntPtr (curscr_ptr); + } + + internal static void console_sharp_get_dims (out int lines, out int cols) + { + lines = Marshal.ReadInt32 (lines_ptr); + cols = Marshal.ReadInt32 (cols_ptr); + } + + [DllImport ("ncurses", EntryPoint="mousemask")] + extern static IntPtr call_mousemask (IntPtr newmask, out IntPtr oldmask); + + public static Event mousemask (Event newmask, out Event oldmask) + { + IntPtr e; + var ret = (Event) call_mousemask ((IntPtr) newmask, out e); + oldmask = (Event) e; + return ret; + } + + [DllImport ("ncurses")] + public extern static uint getmouse (out MouseEvent ev); + + [DllImport ("ncurses")] + public extern static uint ungetmouse (ref MouseEvent ev); +#endregion + + // We encode ESC + char (what Alt-char generates) as 0x2000 + char + public const int KeyAlt = 0x2000; + + static public int IsAlt (int key) + { + if ((key & KeyAlt) != 0) + return key & ~KeyAlt; + return 0; + } + } +} diff --git a/Terminal.Gui/MonoCurses/constants.cs b/Terminal.Gui/MonoCurses/constants.cs new file mode 100644 index 000000000..7a3864cb6 --- /dev/null +++ b/Terminal.Gui/MonoCurses/constants.cs @@ -0,0 +1,92 @@ +/* + * This file is autogenerated by the attrib.c program, do not edit + */ + +using System; + +namespace Unix.Terminal { + internal partial class Curses { + public const int A_NORMAL = unchecked((int)0x0); + public const int A_STANDOUT = unchecked((int)0x10000); + public const int A_UNDERLINE = unchecked((int)0x20000); + public const int A_REVERSE = unchecked((int)0x40000); + public const int A_BLINK = unchecked((int)0x80000); + public const int A_DIM = unchecked((int)0x100000); + public const int A_BOLD = unchecked((int)0x200000); + public const int A_PROTECT = unchecked((int)0x1000000); + public const int A_INVIS = unchecked((int)0x800000); + public const int ACS_LLCORNER = unchecked((int)0x40006d); + public const int ACS_LRCORNER = unchecked((int)0x40006a); + public const int ACS_HLINE = unchecked((int)0x400071); + public const int ACS_ULCORNER = unchecked((int)0x40006c); + public const int ACS_URCORNER = unchecked((int)0x40006b); + public const int ACS_VLINE = unchecked((int)0x400078); + public const int COLOR_BLACK = unchecked((int)0x0); + public const int COLOR_RED = unchecked((int)0x1); + public const int COLOR_GREEN = unchecked((int)0x2); + public const int COLOR_YELLOW = unchecked((int)0x3); + public const int COLOR_BLUE = unchecked((int)0x4); + public const int COLOR_MAGENTA = unchecked((int)0x5); + public const int COLOR_CYAN = unchecked((int)0x6); + public const int COLOR_WHITE = unchecked((int)0x7); + public const int KEY_CODE_YES = unchecked((int)0x100); + internal enum Event : long { + Button1Pressed = unchecked((int)0x2), + Button1Released = unchecked((int)0x1), + Button1Clicked = unchecked((int)0x4), + Button1DoubleClicked = unchecked((int)0x8), + Button1TripleClicked = unchecked((int)0x10), + Button2Pressed = unchecked((int)0x80), + Button2Released = unchecked((int)0x40), + Button2Clicked = unchecked((int)0x100), + Button2DoubleClicked = unchecked((int)0x200), + Button2TrippleClicked = unchecked((int)0x400), + Button3Pressed = unchecked((int)0x2000), + Button3Released = unchecked((int)0x1000), + Button3Clicked = unchecked((int)0x4000), + Button3DoubleClicked = unchecked((int)0x8000), + Button3TripleClicked = unchecked((int)0x10000), + Button4Pressed = unchecked((int)0x80000), + Button4Released = unchecked((int)0x40000), + Button4Clicked = unchecked((int)0x100000), + Button4DoubleClicked = unchecked((int)0x200000), + Button4TripleClicked = unchecked((int)0x400000), + ButtonShift = unchecked((int)0x2000000), + ButtonCtrl = unchecked((int)0x1000000), + ButtonAlt = unchecked((int)0x4000000), + ReportMousePosition = unchecked((int)0x8000000), + AllEvents = unchecked((int)0x7ffffff), + } + public const int ERR = unchecked((int)0xffffffff); + public const int KeyBackspace = unchecked((int)0x107); + public const int KeyUp = unchecked((int)0x103); + public const int KeyDown = unchecked((int)0x102); + public const int KeyLeft = unchecked((int)0x104); + public const int KeyRight = unchecked((int)0x105); + public const int KeyNPage = unchecked((int)0x152); + public const int KeyPPage = unchecked((int)0x153); + public const int KeyHome = unchecked((int)0x106); + public const int KeyMouse = unchecked((int)0x199); + public const int KeyEnd = unchecked((int)0x168); + public const int KeyDeleteChar = unchecked((int)0x14a); + public const int KeyInsertChar = unchecked((int)0x14b); + public const int KeyBackTab = unchecked((int)0x161); + public const int KeyF1 = unchecked((int)0x109); + public const int KeyF2 = unchecked((int)0x10a); + public const int KeyF3 = unchecked((int)0x10b); + public const int KeyF4 = unchecked((int)0x10c); + public const int KeyF5 = unchecked((int)0x10d); + public const int KeyF6 = unchecked((int)0x10e); + public const int KeyF7 = unchecked((int)0x10f); + public const int KeyF8 = unchecked((int)0x110); + public const int KeyF9 = unchecked((int)0x111); + public const int KeyF10 = unchecked((int)0x112); + public const int KeyResize = unchecked((int)0x19a); + + + static public int ColorPair(int n){ + return 0 + n * 256; + } + + } +} diff --git a/Terminal.Gui/MonoCurses/handles.cs b/Terminal.Gui/MonoCurses/handles.cs new file mode 100644 index 000000000..e91887256 --- /dev/null +++ b/Terminal.Gui/MonoCurses/handles.cs @@ -0,0 +1,172 @@ +// +// handles.cs: OO wrappers for some curses objects +// +// Authors: +// Miguel de Icaza (miguel.de.icaza@gmail.com) +// +// Copyright (C) 2007 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +using System; +using System.Runtime.InteropServices; + +namespace Unix.Terminal { + + internal partial class Curses { + internal class Window { + public readonly IntPtr Handle; + static Window curscr; + static Window stdscr; + + static Window () + { + Curses.initscr (); + stdscr = new Window (Curses.console_sharp_get_stdscr ()); + curscr = new Window (Curses.console_sharp_get_curscr ()); + } + + internal Window (IntPtr handle) + { + Handle = handle; + } + + static public Window Standard { + get { + return stdscr; + } + } + + static public Window Current { + get { + return curscr; + } + } + + + public int wtimeout (int delay) + { + return Curses.wtimeout (Handle, delay); + } + + public int notimeout (bool bf) + { + return Curses.notimeout (Handle, bf); + } + + public int keypad (bool bf) + { + return Curses.keypad (Handle, bf); + } + + public int meta (bool bf) + { + return Curses.meta (Handle, bf); + } + + public int intrflush (bool bf) + { + return Curses.intrflush (Handle, bf); + } + + public int clearok (bool bf) + { + return Curses.clearok (Handle, bf); + } + + public int idlok (bool bf) + { + return Curses.idlok (Handle, bf); + } + + public void idcok (bool bf) + { + Curses.idcok (Handle, bf); + } + + public void immedok (bool bf) + { + Curses.immedok (Handle, bf); + } + + public int leaveok (bool bf) + { + return Curses.leaveok (Handle, bf); + } + + public int setscrreg (int top, int bot) + { + return Curses.wsetscrreg (Handle, top, bot); + } + + public int scrollok (bool bf) + { + return Curses.scrollok (Handle, bf); + } + + public int wrefresh () + { + return Curses.wrefresh (Handle); + } + + public int redrawwin () + { + return Curses.redrawwin (Handle); + } + + public int wredrawwin (int beg_line, int num_lines) + { + return Curses.wredrawwin (Handle, beg_line, num_lines); + } + + public int wnoutrefresh () + { + return Curses.wnoutrefresh (Handle); + } + + public int move (int line, int col) + { + return Curses.wmove (Handle, line, col); + } + + public int addch (char ch) + { + return Curses.waddch (Handle, ch); + } + + public int refresh () + { + return Curses.wrefresh (Handle); + } + } + + // Currently unused, to do later + internal class Screen { + public readonly IntPtr Handle; + + internal Screen (IntPtr handle) + { + Handle = handle; + } + } + + } + +} diff --git a/Terminal.Gui/MonoCurses/mainloop.cs b/Terminal.Gui/MonoCurses/mainloop.cs new file mode 100644 index 000000000..442319eab --- /dev/null +++ b/Terminal.Gui/MonoCurses/mainloop.cs @@ -0,0 +1,362 @@ +// +// mainloop.cs: Simple managed mainloop implementation. +// +// Authors: +// Miguel de Icaza (miguel.de.icaza@gmail.com) +// +// Copyright (C) 2011 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +using Mono.Unix.Native; +using System.Collections.Generic; +using System; +using System.Runtime.InteropServices; + +namespace Mono.Terminal { + + /// + /// Simple main loop implementation that can be used to monitor + /// file descriptor, run timers and idle handlers. + /// + public class MainLoop { + /// + /// Condition on which to wake up from file descriptor activity + /// + [Flags] + public enum Condition { + /// + /// There is data to read + /// + PollIn = 1, + /// + /// Writing to the specified descriptor will not block + /// + PollOut = 2, + /// + /// There is urgent data to read + /// + PollPri = 4, + /// + /// Error condition on output + /// + PollErr = 8, + /// + /// Hang-up on output + /// + PollHup = 16, + /// + /// File descriptor is not open. + /// + PollNval = 32 + } + + class Watch { + public int File; + public Condition Condition; + public Func Callback; + } + + class Timeout { + public TimeSpan Span; + public Func Callback; + } + + Dictionary descriptorWatchers = new Dictionary(); + SortedList timeouts = new SortedList (); + List> idleHandlers = new List> (); + + Pollfd [] pollmap; + bool poll_dirty = true; + int [] wakeupPipes = new int [2]; + static IntPtr ignore = Marshal.AllocHGlobal (1); + + /// + /// Default constructor + /// + public MainLoop () + { + Syscall.pipe (wakeupPipes); + AddWatch (wakeupPipes [0], Condition.PollIn, ml => { + Syscall.read (wakeupPipes [0], ignore, 1); + return true; + }); + } + + void Wakeup () + { + Syscall.write (wakeupPipes [1], ignore, 1); + } + + /// + /// Runs @action on the thread that is processing events + /// + public void Invoke (Action action) + { + AddIdle (()=> { + action (); + return false; + }); + Wakeup (); + } + + /// + /// Executes the specified @idleHandler on the idle loop. The return value is a token to remove it. + /// + public Func AddIdle (Func idleHandler) + { + lock (idleHandlers) + idleHandlers.Add (idleHandler); + return idleHandler; + } + + /// + /// Removes the specified idleHandler from processing. + /// + public void RemoveIdle (Func idleHandler) + { + lock (idleHandler) + idleHandlers.Remove (idleHandler); + } + + /// + /// Watches a file descriptor for activity. + /// + /// + /// When the condition is met, the provided callback + /// is invoked. If the callback returns false, the + /// watch is automatically removed. + /// + /// The return value is a token that represents this watch, you can + /// use this token to remove the watch by calling RemoveWatch. + /// + public object AddWatch (int fileDescriptor, Condition condition, Func callback) + { + if (callback == null) + throw new ArgumentNullException ("callback"); + + var watch = new Watch () { Condition = condition, Callback = callback, File = fileDescriptor }; + descriptorWatchers [fileDescriptor] = watch; + poll_dirty = true; + return watch; + } + + /// + /// Removes an active watch from the mainloop. + /// + /// + /// The token parameter is the value returned from AddWatch + /// + public void RemoveWatch (object token) + { + var watch = token as Watch; + if (watch == null) + return; + descriptorWatchers.Remove (watch.File); + } + + void AddTimeout (TimeSpan time, Timeout timeout) + { + timeouts.Add ((DateTime.UtcNow + time).Ticks, timeout); + } + + /// + /// Adds a timeout to the mainloop. + /// + /// + /// When time time specified passes, the callback will be invoked. + /// If the callback returns true, the timeout will be reset, repeating + /// the invocation. If it returns false, the timeout will stop. + /// + /// The returned value is a token that can be used to stop the timeout + /// by calling RemoveTimeout. + /// + public object AddTimeout (TimeSpan time, Func callback) + { + if (callback == null) + throw new ArgumentNullException ("callback"); + var timeout = new Timeout () { + Span = time, + Callback = callback + }; + AddTimeout (time, timeout); + return timeout; + } + + /// + /// Removes a previously scheduled timeout + /// + /// + /// The token parameter is the value returned by AddTimeout. + /// + public void RemoveTimeout (object token) + { + var idx = timeouts.IndexOfValue (token as Timeout); + if (idx == -1) + return; + timeouts.RemoveAt (idx); + } + + static PollEvents MapCondition (Condition condition) + { + PollEvents ret = 0; + if ((condition & Condition.PollIn) != 0) + ret |= PollEvents.POLLIN; + if ((condition & Condition.PollOut) != 0) + ret |= PollEvents.POLLOUT; + if ((condition & Condition.PollPri) != 0) + ret |= PollEvents.POLLPRI; + if ((condition & Condition.PollErr) != 0) + ret |= PollEvents.POLLERR; + if ((condition & Condition.PollHup) != 0) + ret |= PollEvents.POLLHUP; + if ((condition & Condition.PollNval) != 0) + ret |= PollEvents.POLLNVAL; + return ret; + } + + void UpdatePollMap () + { + if (!poll_dirty) + return; + poll_dirty = false; + + pollmap = new Pollfd [descriptorWatchers.Count]; + int i = 0; + foreach (var fd in descriptorWatchers.Keys){ + pollmap [i].fd = fd; + pollmap [i].events = MapCondition (descriptorWatchers [fd].Condition); + } + } + + void RunTimers () + { + long now = DateTime.UtcNow.Ticks; + var copy = timeouts; + timeouts = new SortedList (); + foreach (var k in copy.Keys){ + if (k >= now) + break; + + var timeout = copy [k]; + if (timeout.Callback (this)) + AddTimeout (timeout.Span, timeout); + } + } + + void RunIdle () + { + List> iterate; + lock (idleHandlers){ + iterate = idleHandlers; + idleHandlers = new List> (); + } + + foreach (var idle in iterate){ + if (idle ()) + lock (idleHandlers) + idleHandlers.Add (idle); + } + } + + bool running; + + /// + /// Stops the mainloop. + /// + public void Stop () + { + running = false; + Wakeup (); + } + + /// + /// Determines whether there are pending events to be processed. + /// + /// + /// You can use this method if you want to probe if events are pending. + /// Typically used if you need to flush the input queue while still + /// running some of your own code in your main thread. + /// + public bool EventsPending (bool wait = false) + { + long now = DateTime.UtcNow.Ticks; + int pollTimeout, n; + if (timeouts.Count > 0) + pollTimeout = (int) ((timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); + else + pollTimeout = -1; + + if (!wait) + pollTimeout = 0; + + UpdatePollMap (); + + n = Syscall.poll (pollmap, (uint) pollmap.Length, pollTimeout); + int ic; + lock (idleHandlers) + ic = idleHandlers.Count; + return n > 0 || timeouts.Count > 0 && ((timeouts.Keys [0] - DateTime.UtcNow.Ticks) < 0) || ic > 0; + } + + /// + /// Runs one iteration of timers and file watches + /// + /// + /// You use this to process all pending events (timers, idle handlers and file watches). + /// + /// You can use it like this: + /// while (main.EvensPending ()) MainIteration (); + /// + public void MainIteration () + { + if (timeouts.Count > 0) + RunTimers (); + + foreach (var p in pollmap){ + Watch watch; + + if (p.revents == 0) + continue; + + if (!descriptorWatchers.TryGetValue (p.fd, out watch)) + continue; + if (!watch.Callback (this)) + descriptorWatchers.Remove (p.fd); + } + if (idleHandlers.Count > 0) + RunIdle (); + } + + /// + /// Runs the mainloop. + /// + public void Run () + { + bool prev = running; + running = true; + while (running){ + EventsPending (true); + MainIteration (); + } + running = prev; + } + } +} diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj new file mode 100644 index 000000000..607f5a8c1 --- /dev/null +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -0,0 +1,61 @@ + + + + Debug + AnyCPU + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5} + Library + Terminal.Gui + Terminal.Gui + v4.6.1 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + false + + + true + bin\Release + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Types/Point.cs b/Terminal.Gui/Types/Point.cs similarity index 100% rename from Types/Point.cs rename to Terminal.Gui/Types/Point.cs diff --git a/Types/Rect.cs b/Terminal.Gui/Types/Rect.cs similarity index 100% rename from Types/Rect.cs rename to Terminal.Gui/Types/Rect.cs diff --git a/Types/Size.cs b/Terminal.Gui/Types/Size.cs similarity index 100% rename from Types/Size.cs rename to Terminal.Gui/Types/Size.cs diff --git a/Views/Button.cs b/Terminal.Gui/Views/Button.cs similarity index 100% rename from Views/Button.cs rename to Terminal.Gui/Views/Button.cs diff --git a/Views/Checkbox.cs b/Terminal.Gui/Views/Checkbox.cs similarity index 100% rename from Views/Checkbox.cs rename to Terminal.Gui/Views/Checkbox.cs diff --git a/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs similarity index 100% rename from Views/Dialog.cs rename to Terminal.Gui/Views/Dialog.cs diff --git a/Views/Label.cs b/Terminal.Gui/Views/Label.cs similarity index 100% rename from Views/Label.cs rename to Terminal.Gui/Views/Label.cs diff --git a/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs similarity index 100% rename from Views/Menu.cs rename to Terminal.Gui/Views/Menu.cs diff --git a/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs similarity index 100% rename from Views/MessageBox.cs rename to Terminal.Gui/Views/MessageBox.cs diff --git a/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs similarity index 100% rename from Views/RadioGroup.cs rename to Terminal.Gui/Views/RadioGroup.cs diff --git a/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs similarity index 100% rename from Views/ScrollView.cs rename to Terminal.Gui/Views/ScrollView.cs diff --git a/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs similarity index 100% rename from Views/TextField.cs rename to Terminal.Gui/Views/TextField.cs diff --git a/Terminal.csproj b/Terminal.csproj index 030161e8f..d5005b9cf 100644 --- a/Terminal.csproj +++ b/Terminal.csproj @@ -33,30 +33,13 @@ - - - - - - - - - - - - - - - - - $(MSBuildProjectDirectory)/../mono-curses/mono-curses.dll - - - - + + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5} + Terminal.Gui + diff --git a/Terminal.sln b/Terminal.sln index 98efa6dac..d08ae8d72 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -3,6 +3,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2012 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal", "Terminal.csproj", "{B0A602CD-E176-449D-8663-64238D54F857}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui", "Terminal.Gui\Terminal.Gui.csproj", "{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x86 = Debug|x86 @@ -13,6 +15,10 @@ Global {B0A602CD-E176-449D-8663-64238D54F857}.Debug|x86.Build.0 = Debug|x86 {B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.ActiveCfg = Release|x86 {B0A602CD-E176-449D-8663-64238D54F857}.Release|x86.Build.0 = Release|x86 + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|x86.Build.0 = Debug|Any CPU + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|x86.ActiveCfg = Release|Any CPU + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution Policies = $0 diff --git a/demo.cs b/demo.cs index 763cb476b..88a391c82 100644 --- a/demo.cs +++ b/demo.cs @@ -13,15 +13,15 @@ class Demo { static void ShowEntries (View container) { container.Add ( - new Label (3, 2, "Login: "), - new TextField (14, 2, 40, ""), - new Label (3, 4, "Password: "), - new TextField (14, 4, 40, "") { Secret = true }, - new CheckBox (3, 6, "Remember me"), - new RadioGroup (3, 8, new [] { "_Personal", "_Company" }), - new Button (3, 14, "Ok"), - new Button (10, 14, "Cancel"), - new Label (3, 18, "Press ESC and 9 to activate the menubar") + new Label (3, 6, "Login: "), + new TextField (14, 6, 40, ""), + new Label (3, 8, "Password: "), + new TextField (14, 8, 40, "") { Secret = true }, + new CheckBox (3, 10, "Remember me"), + new RadioGroup (3, 12, new [] { "_Personal", "_Company" }), + new Button (3, 18, "Ok"), + new Button (10, 18, "Cancel"), + new Label (3, 22, "Press ESC and 9 to activate the menubar") ); }