From afb6fbf300e2b0fdf5da28d7907ef0bad4d2a934 Mon Sep 17 00:00:00 2001 From: Miguel de Icaza Date: Mon, 18 Dec 2017 23:53:23 -0500 Subject: [PATCH] Start event handling --- Core.cs | 158 ++++++++++++++++++++-- Driver.cs | 99 ++++++++++++-- Event.cs | 88 ++++++++++++- Terminal.csproj | 1 + Views/TextField.cs | 321 +++++++++++++++++++++++++++++++++++++++++++++ demo.cs | 4 +- 6 files changed, 643 insertions(+), 28 deletions(-) create mode 100644 Views/TextField.cs diff --git a/Core.cs b/Core.cs index 757fa36b1..fe05c7ac0 100644 --- a/Core.cs +++ b/Core.cs @@ -3,6 +3,8 @@ // Pending: // - Check for NeedDisplay on the hierarchy and repaint // - Layout support +// - "Colors" type or "Attributes" type? +// - What to surface as "BackgroundCOlor" when clearing a window, an attribute or colors? // // Optimziations // - Add rendering limitation to the exposed area @@ -17,7 +19,79 @@ namespace Terminal { public bool HasFocus { get; internal set; } // Key handling - public virtual void KeyDown (Event.Key kb) { } + /// + /// This method can be overwritten by view that + /// want to provide accelerator functionality + /// (Alt-key for example). + /// + /// + /// + /// Before keys are sent to the subview on the + /// current view, all the views are + /// processed and the key is passed to the widgets + /// to allow some of them to process the keystroke + /// as a hot-key. + /// + /// For example, if you implement a button that + /// has a hotkey ok "o", you would catch the + /// combination Alt-o here. If the event is + /// caught, you must return true to stop the + /// keystroke from being dispatched to other + /// views. + /// + /// + + public virtual bool ProcessHotKey (KeyEvent kb) + { + return false; + } + + /// + /// If the view is focused, gives the view a + /// chance to process the keystroke. + /// + /// + /// + /// Views can override this method if they are + /// interested in processing the given keystroke. + /// If they consume the keystroke, they must + /// return true to stop the keystroke from being + /// processed by other widgets or consumed by the + /// widget engine. If they return false, the + /// keystroke will be passed using the ProcessColdKey + /// method to other views to process. + /// + /// + public virtual bool ProcessKey (KeyEvent kb) + { + return false; + } + + /// + /// This method can be overwritten by views that + /// want to provide accelerator functionality + /// (Alt-key for example), but without + /// interefering with normal ProcessKey behavior. + /// + /// + /// + /// After keys are sent to the subviews on the + /// current view, all the view are + /// processed and the key is passed to the views + /// to allow some of them to process the keystroke + /// as a cold-key. + /// + /// This functionality is used, for example, by + /// default buttons to act on the enter key. + /// Processing this as a hot-key would prevent + /// non-default buttons from consuming the enter + /// keypress when they have the focus. + /// + /// + public virtual bool ProcessColdKey (KeyEvent kb) + { + return false; + } // Mouse events public virtual void MouseEvent (Event.Mouse me) { } @@ -57,6 +131,8 @@ namespace Terminal { } } + public View SuperView => container; + public View (Rect frame) { this.Frame = frame; @@ -223,6 +299,12 @@ namespace Terminal { Move (frame.X, frame.Y); } + /// + /// Returns the currently focused view inside this view, or null if nothing is focused. + /// + /// The focused. + public View Focused => focused; + /// /// Displays the specified character in the specified column and row. /// @@ -305,6 +387,9 @@ namespace Terminal { /// public void FocusFirst () { + if (subviews == null) + return; + foreach (var view in subviews) { if (view.CanFocus) { SetFocus (view); @@ -318,6 +403,9 @@ namespace Terminal { /// public void FocusLast () { + if (subviews == null) + return; + for (int i = subviews.Count; i > 0;) { i--; @@ -360,6 +448,7 @@ namespace Terminal { return true; } } + if (focused != null) { focused.HasFocus = false; focused = null; @@ -425,6 +514,48 @@ namespace Terminal { return new Toplevel (new Rect (0, 0, Driver.Cols, Driver.Rows)); } + public override bool CanFocus { + get => true; + } + + public override bool ProcessKey (KeyEvent kb) + { + if (ProcessHotKey (kb)) + return true; + + // Process the key normally + if (Focused?.ProcessKey (kb) == true) + return true; + + if (ProcessColdKey (kb)) + return true; + + switch (kb.Key) { + case Key.ControlC: + // TODO: stop current execution of this container + break; + case Key.ControlZ: + // TODO: should suspend + // console_csharp_send_sigtstp (); + break; + case Key.Tab: + var old = Focused; + if (!FocusNext ()) + FocusNext (); + old?.SetNeedsDisplay (); + Focused?.SetNeedsDisplay (); + break; + case Key.BackTab: + old = Focused; + if (!FocusPrev ()) + FocusPrev (); + old?.SetNeedsDisplay (); + Focused?.SetNeedsDisplay (); + break; + } + return false; + } + #if false public override void Redraw () { @@ -460,7 +591,7 @@ namespace Terminal { base.Add(contentView); } - public IEnumerator GetEnumerator () + public new IEnumerator GetEnumerator () { return contentView.GetEnumerator (); } @@ -527,16 +658,10 @@ namespace Terminal { if (Top != null) return; - Driver.Init (); + Driver.Init (TerminalResized); MainLoop = new Mono.Terminal.MainLoop (); Top = Toplevel.Create (); focus = Top; - - MainLoop.AddWatch (0, Mono.Terminal.MainLoop.Condition.PollIn, x => { - //ProcessChar (); - - return true; - }); } public class RunState : IDisposable { @@ -561,6 +686,10 @@ namespace Terminal { } } + static void KeyEvent (Key key) + { + } + static public RunState Begin (Toplevel toplevel) { if (toplevel == null) @@ -568,10 +697,8 @@ namespace Terminal { var rs = new RunState (toplevel); Init (); - Driver.PrepareToRun (); - toplevels.Push (toplevel); - + Driver.PrepareToRun (MainLoop, toplevel); toplevel.LayoutSubviews (); toplevel.FocusFirst (); Redraw (toplevel); @@ -674,5 +801,12 @@ namespace Terminal { RunLoop (runToken); End (runToken); } + + static void TerminalResized () + { + foreach (var t in toplevels) { + t.Frame = new Rect (0, 0, Driver.Cols, Driver.Rows); + } + } } } \ No newline at end of file diff --git a/Driver.cs b/Driver.cs index 4e4058549..e43cbded8 100644 --- a/Driver.cs +++ b/Driver.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; +using Mono.Terminal; using Unix.Terminal; namespace Terminal { - public enum Color - { + + /// + /// Basic colors that can be used to set the foreground and background colors in console applications. These can only be + /// + public enum Color { Black, Blue, Green, @@ -49,13 +53,13 @@ namespace Terminal { } public abstract class ConsoleDriver { - public abstract int Cols {get;} - public abstract int Rows {get;} - public abstract void Init (); + public abstract int Cols { get; } + public abstract int Rows { get; } + public abstract void Init (Action terminalResized); 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 PrepareToRun (MainLoop mainLoop, Responder target); public abstract void Refresh (); public abstract void End (); public abstract void RedrawTop (); @@ -78,6 +82,8 @@ namespace Terminal { } public class CursesDriver : ConsoleDriver { + Action terminalResized; + public override int Cols => Curses.Cols; public override int Rows => Curses.Lines; @@ -115,12 +121,12 @@ namespace Terminal { { // TODO; optimize this to determine if the str fits in the clip region, and if so, use Curses.addstr directly foreach (var c in str) - AddCh ((int) c); + AddCh ((int)c); } - public override void Refresh() => Curses.refresh (); - public override void End() => Curses.endwin (); - public override void RedrawTop() => window.redrawwin (); + public override void Refresh () => Curses.refresh (); + public override void End () => Curses.endwin (); + public override void RedrawTop () => window.redrawwin (); public override void SetAttribute (Attribute c) => Curses.attrset (c.value); public Curses.Window window; @@ -135,7 +141,7 @@ namespace Terminal { public override void SetColors (ConsoleColor foreground, ConsoleColor background) { - int f = (short) foreground; + int f = (short)foreground; int b = (short)background; var v = colorPairs [f, b]; if ((v & 0x10000) == 0) { @@ -152,16 +158,80 @@ namespace Terminal { Dictionary rawPairs = new Dictionary (); public override void SetColors (short foreColorId, short backgroundColorId) { - int key = (((ushort)foreColorId << 16)) | (ushort) backgroundColorId; + int key = (((ushort)foreColorId << 16)) | (ushort)backgroundColorId; if (!rawPairs.TryGetValue (key, out var v)) { v = MakeColor (foreColorId, backgroundColorId); rawPairs [key] = v; } SetAttribute (v); } - public override void PrepareToRun() + + static Key MapCursesKey (int cursesKey) + { + switch (cursesKey) { + case Curses.KeyF1: return Key.F1; + case Curses.KeyF2: return Key.F2; + case Curses.KeyF3: return Key.F3; + case Curses.KeyF4: return Key.F4; + case Curses.KeyF5: return Key.F5; + case Curses.KeyF6: return Key.F6; + case Curses.KeyF7: return Key.F7; + case Curses.KeyF8: return Key.F8; + case Curses.KeyF9: return Key.F9; + case Curses.KeyF10: return Key.F10; + case Curses.KeyUp: return Key.CursorUp; + case Curses.KeyDown: return Key.CursorDown; + case Curses.KeyLeft: return Key.CursorLeft; + case Curses.KeyRight: return Key.CursorRight; + case Curses.KeyHome: return Key.Home; + case Curses.KeyEnd: return Key.End; + case Curses.KeyNPage: return Key.PageDown; + case Curses.KeyPPage: return Key.PageUp; + case Curses.KeyDeleteChar: return Key.DeleteChar; + case Curses.KeyInsertChar: return Key.InsertChar; + case Curses.KeyBackTab: return Key.BackTab; + default: return Key.Unknown; + } + } + + void ProcessInput (Responder handler) + { + var code = Curses.getch (); + if ((code == -1) || (code == Curses.KeyResize)) { + if (Curses.CheckWinChange ()) { + terminalResized (); + } + } + if (code == Curses.KeyMouse) { + // TODO + // Curses.MouseEvent ev; + // Curses.getmouse (out ev); + // handler.HandleMouse (); + return; + } + + // ESC+letter is Alt-Letter. + if (code == 27) { + Curses.timeout (100); + int k = Curses.getch (); + if (k != Curses.ERR && k != 27) { + var mapped = MapCursesKey (k) | Key.AltMask; + handler.ProcessKey (new KeyEvent (mapped)); + } + } else { + handler.ProcessKey (new KeyEvent (MapCursesKey (code))); + } + } + + public override void PrepareToRun (MainLoop mainLoop, Responder handler) { Curses.timeout (-1); + + mainLoop.AddWatch (0, Mono.Terminal.MainLoop.Condition.PollIn, x => { + ProcessInput (handler); + return true; + }); + } public override void DrawFrame (Rect region, bool fill) @@ -192,7 +262,7 @@ namespace Terminal { AddCh (Curses.ACS_LRCORNER); } - public override void Init() + public override void Init(Action terminalResized) { if (window != null) return; @@ -205,6 +275,7 @@ namespace Terminal { Curses.raw (); Curses.noecho (); Curses.Window.Standard.keypad (true); + this.terminalResized = terminalResized; Colors.Base = new ColorScheme (); Colors.Dialog = new ColorScheme (); diff --git a/Event.cs b/Event.cs index b59801f5b..b6a350f31 100644 --- a/Event.cs +++ b/Event.cs @@ -1,9 +1,95 @@ namespace Terminal { + /// + /// The Key enumeration contains special encoding for some keys, but can also + /// encode all the unicode values that can be passed. + /// + /// + /// + /// If the SpecialMask is set, then the value is that of the special mask, + /// otherwise, the value is the one of the lower bits (as extracted by CharMask) + /// + /// + /// Control keys are the values between 1 and 26 corresponding to Control-A to Control-Z + /// + /// + public enum Key : uint { + CharMask = 0xfffff, + SpecialMask = 0xfff00000, + ControlA = 1, + ControlB, + ControlC, + ControlD, + ControlE, + ControlF, + ControlG, + ControlH, + ControlI, + Tab = ControlI, + ControlJ, + ControlK, + ControlL, + ControlM, + ControlN, + ControlO, + ControlP, + ControlQ, + ControlR, + ControlS, + ControlT, + ControlU, + ControlV, + ControlW, + ControlX, + ControlY, + ControlZ, + Esc = 27, + Space = 32, + Delete = 127, + + AltMask = 0x80000000, + + Backspace = 0x100000, + CursorUp, + CursorDown, + CursorLeft, + CursorRight, + PageUp, + PageDown, + Home, + End, + DeleteChar, + InsertChar, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + BackTab, + Unknown + } + + public struct KeyEvent { + public Key Key; + public int KeyValue => (int)KeyValue; + public bool IsAlt => (Key & Key.AltMask) != 0; + public bool IsCtrl => ((uint)Key >= 1) && ((uint)Key <= 26); + + public KeyEvent (Key k) + { + Key = k; + } + } + public class Event { public class Key : Event { public int Code { get; private set; } - + public bool Alt { get; private set; } public Key (int code) { Code = code; diff --git a/Terminal.csproj b/Terminal.csproj index 28bad5a41..0f79098e1 100644 --- a/Terminal.csproj +++ b/Terminal.csproj @@ -40,6 +40,7 @@ + diff --git a/Views/TextField.cs b/Views/TextField.cs new file mode 100644 index 000000000..afc9d8112 --- /dev/null +++ b/Views/TextField.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Terminal { + /// + /// Text data entry widget + /// + /// + /// The Entry widget provides Emacs-like editing + /// functionality, and mouse support. + /// + public class TextField : View { + string text, kill; + int first, point; + bool used; + + /// + /// Changed event, raised when the text has clicked. + /// + /// + /// Client code can hook up to this event, it is + /// raised when the text in the entry changes. + /// + public event EventHandler Changed; + + /// + /// Public constructor. + /// + /// + /// + public TextField (int x, int y, int w, string s) : base (new Rect (x, y, w, 1)) + { + if (s == null) + s = ""; + + text = s; + point = s.Length; + first = point > w ? point - w : 0; + CanFocus = true; + Color = Colors.Dialog.Focus; + } + + /// + /// Sets or gets the text in the entry. + /// + /// + /// + public string Text { + get { + return text; + } + + set { + text = value; + if (point > text.Length) + point = text.Length; + first = point > Frame.Width ? point - Frame.Width : 0; + SetNeedsDisplay (); + } + } + + /// + /// Sets the secret property. + /// + /// + /// This makes the text entry suitable for entering passwords. + /// + public bool Secret { get; set; } + + Attribute color; + /// + /// Sets the color attribute to use (includes foreground and background). + /// + /// The color. + public Attribute Color { + get => color; + set { + color = value; + SetNeedsDisplay (); + } + } + + /// + /// The current cursor position. + /// + public int CursorPosition { get { return point; } } + + /// + /// Sets the cursor position. + /// + public override void PositionCursor () + { + Move (point - first, 0); + } + + public override void Redraw (Rect region) + { + Driver.SetAttribute (Color); + Move (0, 0); + + for (int i = 0; i < Frame.Width; i++) { + int p = first + i; + + if (p < text.Length) { + Driver.AddCh (Secret ? '*' : text [p]); + } else + Driver.AddCh (' '); + } + PositionCursor (); + } + + void Adjust () + { + if (point < first) + first = point; + else if (first + point >= Frame.Width) + first = point - (Frame.Width / 3); + Redraw (Bounds); + Driver.Refresh (); + } + + void SetText (string new_text) + { + text = new_text; + if (Changed != null) + Changed (this, EventArgs.Empty); + } + + public override bool CanFocus { + get => true; + set { base.CanFocus = value; } + } + + public override bool ProcessKey (KeyEvent kb) + { + switch (kb.Key) { + case Key.Delete: + case Key.Backspace: + if (point == 0) + return true; + + SetText (text.Substring (0, point - 1) + text.Substring (point)); + point--; + Adjust (); + break; + + // Home, C-A + case Key.Home: + case Key.ControlA: + point = 0; + Adjust (); + break; + + case Key.CursorLeft: + case Key.ControlB: + if (point > 0) { + point--; + Adjust (); + } + break; + + case Key.ControlD: // Delete + if (point == text.Length) + break; + SetText (text.Substring (0, point) + text.Substring (point + 1)); + Adjust (); + break; + + case Key.ControlE: // End + point = text.Length; + Adjust (); + break; + + case Key.CursorRight: + case Key.ControlF: + if (point == text.Length) + break; + point++; + Adjust (); + break; + + case Key.ControlK: // kill-to-end + kill = text.Substring (point); + SetText (text.Substring (0, point)); + Adjust (); + break; + + case Key.ControlY: // Control-y, yank + if (kill == null) + return true; + + if (point == text.Length) { + SetText (text + kill); + point = text.Length; + } else { + SetText (text.Substring (0, point) + kill + text.Substring (point)); + point += kill.Length; + } + Adjust (); + break; + + case (Key)((int)'b' + Key.AltMask): + int bw = WordBackward (point); + if (bw != -1) + point = bw; + Adjust (); + break; + + case (Key)((int)'f' + Key.AltMask): + int fw = WordForward (point); + if (fw != -1) + point = fw; + Adjust (); + break; + + default: + // Ignore other control characters. + if (kb.Key < Key.Space || kb.Key > Key.CharMask) + return false; + + if (used) { + if (point == text.Length) { + SetText (text + (char)kb.Key); + } else { + SetText (text.Substring (0, point) + (char)kb.Key + text.Substring (point)); + } + point++; + } else { + SetText ("" + (char)kb.Key); + first = 0; + point = 1; + } + used = true; + Adjust (); + return true; + } + used = true; + return true; + } + + int WordForward (int p) + { + if (p >= text.Length) + return -1; + + int i = p; + if (Char.IsPunctuation (text [p]) || Char.IsWhiteSpace (text [p])) { + for (; i < text.Length; i++) { + if (Char.IsLetterOrDigit (text [i])) + break; + } + for (; i < text.Length; i++) { + if (!Char.IsLetterOrDigit (text [i])) + break; + } + } else { + for (; i < text.Length; i++) { + if (!Char.IsLetterOrDigit (text [i])) + break; + } + } + if (i != p) + return i; + return -1; + } + + int WordBackward (int p) + { + if (p == 0) + return -1; + + int i = p - 1; + if (i == 0) + return 0; + + if (Char.IsPunctuation (text [i]) || Char.IsSymbol (text [i]) || Char.IsWhiteSpace (text [i])) { + for (; i >= 0; i--) { + if (Char.IsLetterOrDigit (text [i])) + break; + } + for (; i >= 0; i--) { + if (!Char.IsLetterOrDigit (text [i])) + break; + } + } else { + for (; i >= 0; i--) { + if (!Char.IsLetterOrDigit (text [i])) + break; + } + } + i++; + + if (i != p) + return i; + + return -1; + } + +#if false + public override void ProcessMouse (Curses.MouseEvent ev) + { + if ((ev.ButtonState & Curses.Event.Button1Clicked) == 0) + return; + + .SetFocus (this); + + // We could also set the cursor position. + point = first + (ev.X - x); + if (point > text.Length) + point = text.Length; + if (point < first) + point = 0; + + SetNeedsDisplay (); + } +#endif + } + + +} diff --git a/demo.cs b/demo.cs index 243257280..748744721 100644 --- a/demo.cs +++ b/demo.cs @@ -9,7 +9,9 @@ class Demo { new Label (new Rect (0, 0, 40, 3), "1-Hello world, how are you doing today") { TextAlignment = TextAlignment.Left }, new Label (new Rect (0, 4, 40, 3), "2-Hello world, how are you doing today") { TextAlignment = TextAlignment.Right}, new Label (new Rect (0, 8, 40, 3), "3-Hello world, how are you doing today") { TextAlignment = TextAlignment.Centered }, - new Label (new Rect (0, 12, 40, 3), "4-Hello world, how are you doing today") { TextAlignment = TextAlignment.Justified} + new Label (new Rect (0, 12, 40, 3), "4-Hello world, how are you doing today") { TextAlignment = TextAlignment.Justified}, + new Label (3, 14, "Login: "), + new TextField (10, 14, 40, "") }; top.Add (win); Application.Run ();