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