diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index 688848d5e..5f66afc33 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -1536,7 +1536,8 @@ namespace Terminal.Gui { DrawBounds (state.Toplevel); state.Toplevel.PositionCursor (); Driver.Refresh (); - } + } else + Driver.UpdateCursor (); } } diff --git a/Terminal.Gui/Driver.cs b/Terminal.Gui/Driver.cs index bc337eda5..34042ed18 100644 --- a/Terminal.Gui/Driver.cs +++ b/Terminal.Gui/Driver.cs @@ -264,6 +264,11 @@ namespace Terminal.Gui { /// public abstract void Refresh (); + /// + /// Updates the location of the cursor position + /// + public abstract void UpdateCursor (); + /// /// Ends the execution of the console driver. /// @@ -495,6 +500,7 @@ namespace Terminal.Gui { } public override void Refresh () => Curses.refresh (); + public override void UpdateCursor () => Curses.refresh (); public override void End () => Curses.endwin (); public override void UpdateScreen () => window.redrawwin (); public override void SetAttribute (Attribute c) => Curses.attrset (c.value); @@ -1017,6 +1023,11 @@ namespace Terminal.Gui { Console.CursorLeft = savedCol; } + public override void UpdateCursor () + { + // + } + public override void StartReportingMouseMoves() { } diff --git a/Terminal.Gui/Event.cs b/Terminal.Gui/Event.cs index 640576c9a..a0ce2ef6a 100644 --- a/Terminal.Gui/Event.cs +++ b/Terminal.Gui/Event.cs @@ -25,6 +25,11 @@ namespace Terminal.Gui { /// /// public enum Key : uint { + /// + /// Mask that indictes that this is a character value, values outside this range + /// indicate special characters like Alt-key combinations or special keys on the + /// keyboard like function keys, arrows keys and so on. + /// CharMask = 0xfffff, /// diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs new file mode 100644 index 000000000..0f01bb206 --- /dev/null +++ b/Terminal.Gui/Views/TextView.cs @@ -0,0 +1,459 @@ +// +// TextView.cs: multi-line text editing +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NStack; + +namespace Terminal.Gui { + class TextModel { + List> lines; + List lineLength; + + public bool LoadFile (string file) + { + if (file == null) + throw new ArgumentNullException (nameof (file)); + try { + var stream = File.OpenRead (file); + if (stream == null) + return false; + } catch { + return false; + } + LoadStream (File.OpenRead (file)); + return true; + } + + List ToRunes (ustring str) + { + List runes = new List (); + foreach (var x in str.ToRunes ()) { + runes.Add (x); + } + return runes; + } + + void Append (List line) + { + var str = ustring.Make (line.ToArray ()); + lines.Add (ToRunes (str)); + } + + public void LoadStream (Stream input) + { + if (input == null) + throw new ArgumentNullException (nameof (input)); + + lines = new List> (); + var buff = new BufferedStream (input); + int v; + var line = new List (); + while ((v = buff.ReadByte ()) != -1) { + if (v == 10) { + Append (line); + line.Clear (); + continue; + } + line.Add ((byte)v); + } + if (line.Count > 0) + Append (line); + } + + public void LoadString (ustring content) + { + lines = new List> (); + int start = 0, i = 0; + for (; i < content.Length; i++) { + if (content [i] == 10) { + if (i - start > 0) + lines.Add (ToRunes (content [start, i])); + else + lines.Add (ToRunes (ustring.Empty)); + start = i + 1; + } + } + if (i - start > 0) + lines.Add (ToRunes (content [start, null])); + } + + public override string ToString () + { + var sb = new StringBuilder (); + foreach (var line in lines) { + sb.Append (line); + sb.AppendLine (); + } + return sb.ToString (); + } + + public int Count => lines.Count; + + public List GetLine (int line) => lines [line]; + } + + /// + /// Text data entry widget + /// + /// + /// The Entry widget provides Emacs-like editing + /// functionality, and mouse support. + /// + public class TextView : View { + TextModel model = new TextModel (); + int topRow; + int leftColumn; + int currentRow; + int currentColumn; + 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 TextView (Rect frame) : base (frame) + { + CanFocus = true; + } + + void ResetPosition () + { + topRow = leftColumn = currentRow = currentColumn = 0; + } + + /// + /// Sets or gets the text in the entry. + /// + /// + /// + public ustring Text { + get { + return model.ToString (); + } + + set { + ResetPosition (); + model.LoadString (value); + SetNeedsDisplay (); + } + } + + /// + /// The current cursor row. + /// + public int CurrentRow => currentRow; + + /// + /// Gets the cursor column. + /// + /// The cursor column. + public int CurrentColumn => currentColumn; + + /// + /// Sets the cursor position. + /// + public override void PositionCursor () + { + Move (CurrentColumn - leftColumn, CurrentRow - topRow); + } + + void ClearRegion (int left, int top, int right, int bottom) + { + for (int row = top; row < bottom; row++) { + Move (left, row); + for (int col = left; col < right; col++) + AddRune (col, row, ' '); + } + } + + public override void Redraw (Rect region) + { + Driver.SetAttribute (ColorScheme.Focus); + Move (0, 0); + + int bottom = region.Bottom; + int right = region.Right; + for (int row = region.Top; row < bottom; row++) { + int textLine = topRow + row; + if (textLine >= model.Count) { + ClearRegion (region.Left, row, region.Right, row + 1); + continue; + } + var line = model.GetLine (textLine); + int lineRuneCount = line.Count; + if (line.Count < region.Left){ + ClearRegion (region.Left, row, region.Right, row + 1); + continue; + } + + Move (region.Left, row); + for (int col = region.Left; col < right; col++) { + var lineCol = leftColumn + col; + var rune = lineCol >= lineRuneCount ? ' ' : line [lineCol]; + AddRune (col, row, rune); + } + } + PositionCursor (); + } + + public override bool CanFocus { + get => true; + set { base.CanFocus = value; } + } + + void SetClipboard (ustring text) + { + Clipboard.Contents = text; + } + + public void Insert (Rune rune) + { + var line = model.GetLine (currentRow); + line.Insert (currentColumn, rune); + var prow = currentRow - topRow; + + SetNeedsDisplay (new Rect (0, prow, Frame.Width, prow + 1)); + } + + public override bool ProcessKey (KeyEvent kb) + { + switch (kb.Key) { + case Key.ControlN: + case Key.CursorDown: + if (currentRow + 1 < model.Count) { + currentRow++; + if (currentRow >= topRow + Frame.Height) { + topRow++; + SetNeedsDisplay (); + } + PositionCursor (); + } + break; + + case Key.ControlP: + case Key.CursorUp: + if (currentRow > 0) { + currentRow--; + if (currentRow < topRow) { + topRow--; + SetNeedsDisplay (); + } + PositionCursor (); + } + break; + + case Key.ControlF: + case Key.CursorRight: + var currentLine = model.GetLine (currentRow); + if (currentColumn < currentLine.Count) { + currentColumn++; + if (currentColumn >= leftColumn + Frame.Width) { + leftColumn++; + SetNeedsDisplay (); + } + PositionCursor (); + } else { + if (currentRow + 1 < model.Count) { + currentRow++; + currentColumn = 0; + leftColumn = 0; + if (currentRow >= topRow + Frame.Height) { + topRow++; + } + SetNeedsDisplay (); + PositionCursor (); + } + break; + } + break; + + case Key.ControlB: + case Key.CursorLeft: + if (currentColumn > 0) { + currentColumn--; + if (currentColumn < leftColumn) { + leftColumn--; + SetNeedsDisplay (); + } + PositionCursor (); + } else { + if (currentRow > 0) { + currentRow--; + if (currentRow < topRow) { + topRow--; + + } + currentLine = model.GetLine (currentRow); + currentColumn = currentLine.Count; + int prev = leftColumn; + leftColumn = currentColumn - Frame.Width + 1; + if (leftColumn < 0) + leftColumn = 0; + if (prev != leftColumn) + SetNeedsDisplay (); + PositionCursor (); + } + } + break; + + case Key.Delete: + case Key.Backspace: + break; + + // Home, C-A + case Key.Home: + case Key.ControlA: + currentColumn = 0; + if (currentColumn < leftColumn) { + leftColumn = 0; + SetNeedsDisplay (); + } else + PositionCursor (); + break; + + case Key.ControlD: // Delete + break; + + case Key.ControlE: // End + currentLine = model.GetLine (currentRow); + currentColumn = currentLine.Count; + int pcol = leftColumn; + leftColumn = currentColumn - Frame.Width + 1; + if (leftColumn < 0) + leftColumn = 0; + if (pcol != leftColumn) + SetNeedsDisplay (); + PositionCursor (); + break; + + case Key.ControlK: // kill-to-end + break; + + case Key.ControlY: // Control-y, yank + + case (Key)((int)'b' + Key.AltMask): + break; + + case (Key)((int)'f' + Key.AltMask): + break; + + default: + // Ignore control characters and other special keys + if (kb.Key < Key.Space || kb.Key > Key.CharMask) + return false; + Insert ((uint)kb.Key); + currentColumn++; + if (currentColumn >= leftColumn + Frame.Width) { + leftColumn++; + SetNeedsDisplay (); + } + PositionCursor (); + return true; + } + return true; + } + +#if false + int WordForward (int p) + { + if (p >= text.Length) + return -1; + + int i = p; + if (Rune.IsPunctuation (text [p]) || Rune.IsWhiteSpace (text [p])) { + for (; i < text.Length; i++) { + var r = text [i]; + if (Rune.IsLetterOrDigit (r)) + break; + } + for (; i < text.Length; i++) { + var r = text [i]; + if (!Rune.IsLetterOrDigit (r)) + break; + } + } else { + for (; i < text.Length; i++) { + var r = text [i]; + if (!Rune.IsLetterOrDigit (r)) + 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; + + var ti = text [i]; + if (Rune.IsPunctuation (ti) || Rune.IsSymbol (ti) || Rune.IsWhiteSpace (ti)) { + for (; i >= 0; i--) { + if (Rune.IsLetterOrDigit (text [i])) + break; + } + for (; i >= 0; i--) { + if (!Rune.IsLetterOrDigit (text [i])) + break; + } + } else { + for (; i >= 0; i--) { + if (!Rune.IsLetterOrDigit (text [i])) + break; + } + } + i++; + + if (i != p) + return i; + + return -1; + } + + public override bool MouseEvent (MouseEvent ev) + { + if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) + return false; + + if (!HasFocus) + SuperView.SetFocus (this); + + // We could also set the cursor position. + point = first + ev.X; + if (point > text.Length) + point = text.Length; + if (point < first) + point = 0; + + SetNeedsDisplay (); + return true; + } + #endif + } + +} + diff --git a/demo.cs b/demo.cs index 139b061e8..13d73ba40 100644 --- a/demo.cs +++ b/demo.cs @@ -169,7 +169,11 @@ class Demo { }) }); - ShowEntries (win); + //ShowEntries (win); + var text = new TextView (new Rect (1, 1, 60, 14)); + text.Text = System.IO.File.ReadAllText ("/etc/passwd"); + win.Add (text); + int count = 0; ml = new Label (new Rect (3, 17, 47, 1), "Mouse: "); Application.RootMouseEvent += delegate (MouseEvent me) {