// // TextValidateField.cs: single-line text editor with validation through providers. // // Authors: // José Miguel Perricone (jmperricone@hotmail.com) // using NStack; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text.RegularExpressions; using Terminal.Gui.TextValidateProviders; namespace Terminal.Gui { namespace TextValidateProviders { /// /// TextValidateField Providers Interface. /// All TextValidateField are created with a ITextValidateProvider. /// public interface ITextValidateProvider { /// /// Set that this provider uses a fixed width. /// e.g. Masked ones are fixed. /// bool Fixed { get; } /// /// Set Cursor position to . /// /// /// Return first valid position. int Cursor (int pos); /// /// First valid position before . /// /// /// New cursor position if any, otherwise returns int CursorLeft (int pos); /// /// First valid position after . /// /// Current position. /// New cursor position if any, otherwise returns int CursorRight (int pos); /// /// Find the first valid character position. /// /// New cursor position. int CursorStart (); /// /// Find the last valid character position. /// /// New cursor position. int CursorEnd (); /// /// Deletes the current character in . /// /// /// true if the character was successfully removed, otherwise false. bool Delete (int pos); /// /// Insert character in position . /// /// /// /// true if the character was successfully inserted, otherwise false. bool InsertAt (char ch, int pos); /// /// True if the input is valid, otherwise false. /// bool IsValid { get; } /// /// Set the input text and get the current value. /// ustring Text { get; set; } /// /// Gets the formatted string for display. /// ustring DisplayText { get; } } ////////////////////////////////////////////////////////////////////////////// // PROVIDERS ////////////////////////////////////////////////////////////////////////////// #region NetMaskedTextProvider /// /// .Net MaskedTextProvider Provider for TextValidateField. /// /// Wrapper around MaskedTextProvider /// Masking elements /// public class NetMaskedTextProvider : ITextValidateProvider { MaskedTextProvider provider; /// /// Empty Constructor /// public NetMaskedTextProvider (string mask) { Mask = mask; } /// /// Mask property /// public ustring Mask { get { return provider?.Mask; } set { var current = provider != null ? provider.ToString (false, false) : string.Empty; provider = new MaskedTextProvider (value == ustring.Empty ? "&&&&&&" : value.ToString ()); if (string.IsNullOrEmpty (current) == false) { provider.Set (current); } } } /// public ustring Text { get { return provider.ToString (); } set { provider.Set (value.ToString ()); } } /// public bool IsValid => provider.MaskCompleted; /// public bool Fixed => true; /// public ustring DisplayText => provider.ToDisplayString (); /// public int Cursor (int pos) { if (pos < 0) { return CursorStart (); } else if (pos > provider.Length) { return CursorEnd (); } else { var p = provider.FindEditPositionFrom (pos, false); if (p == -1) p = provider.FindEditPositionFrom (pos, true); return p; } } /// public int CursorStart () { return provider.IsEditPosition (0) ? 0 : provider.FindEditPositionFrom (0, true); } /// public int CursorEnd () { return provider.IsEditPosition (provider.Length - 1) ? provider.Length - 1 : provider.FindEditPositionFrom (provider.Length, false); } /// public int CursorLeft (int pos) { var c = provider.FindEditPositionFrom (pos - 1, false); return c == -1 ? pos : c; } /// public int CursorRight (int pos) { var c = provider.FindEditPositionFrom (pos + 1, true); return c == -1 ? pos : c; } /// public bool Delete (int pos) { return provider.Replace (' ', pos);// .RemoveAt (pos); } /// public bool InsertAt (char ch, int pos) { return provider.Replace (ch, pos); } } #endregion #region TextRegexProvider /// /// Regex Provider for TextValidateField. /// public class TextRegexProvider : ITextValidateProvider { Regex regex; List text; List pattern; /// /// Empty Constructor. /// public TextRegexProvider (string pattern) { Pattern = pattern; } /// /// Regex pattern property. /// public ustring Pattern { get { return ustring.Make (pattern); } set { pattern = value.ToRuneList (); CompileMask (); SetupText (); } } /// public ustring Text { get { return ustring.Make (text); } set { text = value != ustring.Empty ? value.ToRuneList () : null; SetupText (); } } /// public ustring DisplayText => Text; /// public bool IsValid { get { return Validate (text); } } /// public bool Fixed => false; /// /// When true, validates with the regex pattern on each input, preventing the input if it's not valid. /// public bool ValidateOnInput { get; set; } = true; bool Validate (List text) { var match = regex.Match (ustring.Make (text).ToString ()); return match.Success; } /// public int Cursor (int pos) { if (pos < 0) { return CursorStart (); } else if (pos >= text.Count) { return CursorEnd (); } else { return pos; } } /// public int CursorStart () { return 0; } /// public int CursorEnd () { return text.Count; } /// public int CursorLeft (int pos) { if (pos > 0) { return pos - 1; } return pos; } /// public int CursorRight (int pos) { if (pos < text.Count) { return pos + 1; } return pos; } /// public bool Delete (int pos) { if (text.Count > 0 && pos < text.Count) { text.RemoveAt (pos); } return true; } /// public bool InsertAt (char ch, int pos) { var aux = text.ToList (); aux.Insert (pos, ch); if (Validate (aux) || ValidateOnInput == false) { text.Insert (pos, ch); return true; } return false; } void SetupText () { if (text != null && IsValid) { return; } text = new List (); } /// /// Compiles the regex pattern for validation./> /// private void CompileMask () { regex = new Regex (ustring.Make (pattern).ToString (), RegexOptions.Compiled); } } #endregion } /// /// Text field that validates input through a /// public class TextValidateField : View { ITextValidateProvider provider; int cursorPosition = 0; /// /// Initializes a new instance of the class using positioning. /// public TextValidateField () : this (null) { } /// /// Initializes a new instance of the class using positioning. /// public TextValidateField (ITextValidateProvider provider) { if (provider != null) { Provider = provider; } Initialize (); } void Initialize () { Height = 1; CanFocus = true; // Things this view knows how to do AddCommand (Command.LeftHome, () => { HomeKeyHandler (); return true; }); AddCommand (Command.RightEnd, () => { EndKeyHandler (); return true; }); AddCommand (Command.DeleteCharRight, () => { DeleteKeyHandler (); return true; }); AddCommand (Command.DeleteCharLeft, () => { BackspaceKeyHandler (); return true; }); AddCommand (Command.Left, () => { CursorLeft (); return true; }); AddCommand (Command.Right, () => { CursorRight (); return true; }); // Default keybindings for this view AddKeyBinding (Key.Home, Command.LeftHome); AddKeyBinding (Key.End, Command.RightEnd); AddKeyBinding (Key.Delete, Command.DeleteCharRight); AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight); AddKeyBinding (Key.Backspace, Command.DeleteCharLeft); AddKeyBinding (Key.CursorLeft, Command.Left); AddKeyBinding (Key.CursorRight, Command.Right); } /// /// Provider /// public ITextValidateProvider Provider { get => provider; set { provider = value; if (provider.Fixed == true) { this.Width = provider.DisplayText == ustring.Empty ? 10 : Text.Length; } HomeKeyHandler (); SetNeedsDisplay (); } } /// public override bool MouseEvent (MouseEvent mouseEvent) { if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { var c = provider.Cursor (mouseEvent.X - GetMargins (Frame.Width).left); if (provider.Fixed == false && TextAlignment == TextAlignment.Right && Text.Length > 0) { c += 1; } cursorPosition = c; SetFocus (); SetNeedsDisplay (); return true; } return false; } /// /// Text /// public new ustring Text { get { if (provider == null) { return ustring.Empty; } return provider.Text; } set { if (provider == null) { return; } provider.Text = value; SetNeedsDisplay (); } } /// public override void PositionCursor () { var (left, _) = GetMargins (Frame.Width); // Fixed = true, is for inputs thar have fixed width, like masked ones. // Fixed = false, is for normal input. // When it's right-aligned and it's a normal input, the cursor behaves differently. if (provider?.Fixed == false && TextAlignment == TextAlignment.Right) { Move (cursorPosition + left - 1, 0); } else { Move (cursorPosition + left, 0); } } /// /// Margins for text alignment. /// /// Total width /// Left and right margins (int left, int right) GetMargins (int width) { var count = Text.Length; var total = width - count; switch (TextAlignment) { case TextAlignment.Left: return (0, total); case TextAlignment.Centered: return (total / 2, (total / 2) + (total % 2)); case TextAlignment.Right: return (total, 0); default: return (0, total); } } /// public override void Redraw (Rect bounds) { if (provider == null) { Move (0, 0); Driver.AddStr ("Error: ITextValidateProvider not set!"); return; } var bgcolor = !IsValid ? Color.BrightRed : ColorScheme.Focus.Background; var textColor = new Attribute (ColorScheme.Focus.Foreground, bgcolor); var (margin_left, margin_right) = GetMargins (bounds.Width); Move (0, 0); // Left Margin Driver.SetAttribute (textColor); for (int i = 0; i < margin_left; i++) { Driver.AddRune (' '); } // Content Driver.SetAttribute (textColor); // Content for (int i = 0; i < provider.DisplayText.Length; i++) { Driver.AddRune (provider.DisplayText [i]); } // Right Margin Driver.SetAttribute (textColor); for (int i = 0; i < margin_right; i++) { Driver.AddRune (' '); } } /// /// Try to move the cursor to the left. /// /// True if moved. bool CursorLeft () { var current = cursorPosition; cursorPosition = provider.CursorLeft (cursorPosition); SetNeedsDisplay (); return current != cursorPosition; } /// /// Try to move the cursor to the right. /// /// True if moved. bool CursorRight () { var current = cursorPosition; cursorPosition = provider.CursorRight (cursorPosition); SetNeedsDisplay (); return current != cursorPosition; } /// /// Delete char at cursor position - 1, moving the cursor. /// /// bool BackspaceKeyHandler () { if (provider.Fixed == false && TextAlignment == TextAlignment.Right && cursorPosition <= 1) { return false; } cursorPosition = provider.CursorLeft (cursorPosition); provider.Delete (cursorPosition); SetNeedsDisplay (); return true; } /// /// Deletes char at current position. /// /// bool DeleteKeyHandler () { if (provider.Fixed == false && TextAlignment == TextAlignment.Right) { cursorPosition = provider.CursorLeft (cursorPosition); } provider.Delete (cursorPosition); SetNeedsDisplay (); return true; } /// /// Moves the cursor to first char. /// /// bool HomeKeyHandler () { cursorPosition = provider.CursorStart (); SetNeedsDisplay (); return true; } /// /// Moves the cursor to the last char. /// /// bool EndKeyHandler () { cursorPosition = provider.CursorEnd (); SetNeedsDisplay (); return true; } /// public override bool ProcessKey (KeyEvent kb) { if (provider == null) { return false; } var result = InvokeKeybindings (kb); if (result != null) return (bool)result; if (kb.Key < Key.Space || kb.Key > Key.CharMask) return false; var key = new Rune ((uint)kb.KeyValue); var inserted = provider.InsertAt ((char)key, cursorPosition); if (inserted) { CursorRight (); } return true; } /// /// This property returns true if the input is valid. /// public virtual bool IsValid { get { if (provider == null) { return false; } return provider.IsValid; } } } }