// // TextField.cs: single-line text editor with Emacs keybindings // // Authors: // Miguel de Icaza (miguel@gnome.org) // using System; using System.Collections.Generic; using System.Linq; using NStack; namespace Terminal.Gui { /// /// Text data entry widget /// /// /// The Entry widget provides Emacs-like editing /// functionality, and mouse support. /// public class TextField : View { ustring text; 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 that creates a text field, with layout controlled with X, Y, Width and Height. /// /// Initial text contents. public TextField (ustring text) { if (text == null) text = ""; this.text = text; point = text.Length; CanFocus = true; } /// /// Public constructor that creates a text field at an absolute position and size. /// /// The x coordinate. /// The y coordinate. /// The width. /// Initial text contents. public TextField (int x, int y, int w, ustring text) : base (new Rect (x, y, w, 1)) { if (text == null) text = ""; this.text = text; point = text.Length; first = point > w ? point - w : 0; CanFocus = true; } public override Rect Frame { get => base.Frame; set { base.Frame = value; var w = base.Frame.Width; first = point > w ? point - w : 0; } } /// /// Sets or gets the text in the entry. /// /// /// public ustring 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; } /// /// The current cursor position. /// public int CursorPosition { get { return point; } } /// /// Sets the cursor position. /// public override void PositionCursor () { var x = first; var col = 0; foreach ((var idx, var rune) in text [first, null].Range ()) { if (x == point) break; var cols = Rune.ColumnWidth (rune); col += cols; x++; } Move (col, 0); } public override void Redraw (Rect region) { Driver.SetAttribute (ColorScheme.Focus); Move (0, 0); int p = first; int col = 0; int width = Frame.Width; foreach ((var idx, var rune) in text.Range ()) { if (idx < first) continue; var cols = Rune.ColumnWidth (rune); if (col + cols < width) Driver.AddRune ((Rune)(Secret ? '*' : rune)); col += cols; } for (int i = col; i < Frame.Width; i++) Driver.AddRune (' '); PositionCursor (); } void Adjust () { if (point < first) first = point; else if (first + point >= Frame.Width) first = point - (Frame.Width / 3); SetNeedsDisplay (); } void SetText (ustring new_text) { text = new_text; if (Changed != null) Changed (this, EventArgs.Empty); } public override bool CanFocus { get => true; set { base.CanFocus = value; } } void SetClipboard (ustring text) { if (!Secret) Clipboard.Contents = text; } // Maps a rune index to the byte index inside the utf8 string int RuneIndexToByteIndex (int index) { var blen = text.Length; for (int byteIndex = 0, runeIndex = 0; byteIndex < blen; runeIndex++) { if (index == runeIndex) return byteIndex; (var rune, var size) = Utf8.DecodeRune (text, byteIndex, byteIndex - blen); byteIndex += size; } throw new InvalidOperationException (); } public override bool ProcessKey (KeyEvent kb) { switch (kb.Key) { case Key.DeleteChar: if (text.Length == 0 || text.Length == point) return true; SetText (text [0, RuneIndexToByteIndex (point)] + text [RuneIndexToByteIndex (point + 1), null]); Adjust (); break; case Key.Delete: case Key.Backspace: if (point == 0) return true; SetText (text [0, RuneIndexToByteIndex (point - 1)] + text [RuneIndexToByteIndex (point), null]); 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 [0, RuneIndexToByteIndex (point)] + text [RuneIndexToByteIndex (point + 1), null]); Adjust (); break; case Key.End: case Key.ControlE: // End point = text.RuneCount; Adjust (); break; case Key.CursorRight: case Key.ControlF: if (point == text.RuneCount) break; point++; Adjust (); break; case Key.ControlK: // kill-to-end SetClipboard (text.Substring (RuneIndexToByteIndex (point))); SetText (text [0, RuneIndexToByteIndex (point)]); Adjust (); break; case Key.ControlY: // Control-y, yank var clip = Clipboard.Contents; if (clip== null) return true; if (point == text.RuneCount) { SetText (text + clip); point = text.RuneCount; } else { SetText (text [0, RuneIndexToByteIndex (point)] + clip + text.Substring (RuneIndexToByteIndex (point))); point += clip.RuneCount; } 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; // MISSING: // Alt-D, Alt-backspace // Alt-Y // Delete adding to kill buffer default: // Ignore other control characters. if (kb.Key < Key.Space || kb.Key > Key.CharMask) return false; var kbstr = ustring.Make ((uint)kb.Key); if (used) { if (point == text.RuneCount) { SetText (text + kbstr); } else { SetText (text [0, RuneIndexToByteIndex (point)] + kbstr + text [RuneIndexToByteIndex (point), null]); } point++; } else { SetText (kbstr); 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 (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; } } }