// // TextValidateField.cs: single-line text editor with validation through providers. // // Authors: // José Miguel Perricone (jmperricone@hotmail.com) // using System.ComponentModel; 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 { /// Gets the formatted string for display. string DisplayText { get; } /// Set that this provider uses a fixed width. e.g. Masked ones are fixed. bool Fixed { get; } /// True if the input is valid, otherwise false. bool IsValid { get; } /// Set the input text and get the current value. string Text { get; set; } /// Set Cursor position to . /// /// Return first valid position. int Cursor (int pos); /// Find the last valid character position. /// New cursor position. int CursorEnd (); /// 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 (); /// 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); /// Method that invoke the event if it's defined. /// The previous text before replaced. /// Returns the void OnTextChanged (EventArgs oldValue); /// /// Changed event, raised when the text has changed. /// /// This event is raised when the changes. The passed is a /// containing the old value. /// /// event EventHandler> TextChanged; } ////////////////////////////////////////////////////////////////////////////// // PROVIDERS ////////////////////////////////////////////////////////////////////////////// #region NetMaskedTextProvider /// /// .Net MaskedTextProvider Provider for TextValidateField. /// /// /// /// Wrapper around MaskedTextProvider /// /// /// /// /// Masking elements /// /// /// public class NetMaskedTextProvider : ITextValidateProvider { private MaskedTextProvider _provider; /// Empty Constructor public NetMaskedTextProvider (string mask) { Mask = mask; } /// Mask property public string Mask { get => _provider?.Mask; set { string current = _provider != null ? _provider.ToString (false, false) : string.Empty; _provider = new MaskedTextProvider (value == string.Empty ? "&&&&&&" : value); if (!string.IsNullOrEmpty (current)) { _provider.Set (current); } } } /// public event EventHandler> TextChanged; /// public string Text { get => _provider.ToString (); set => _provider.Set (value); } /// public bool IsValid => _provider.MaskCompleted; /// public bool Fixed => true; /// public string DisplayText => _provider.ToDisplayString (); /// public int Cursor (int pos) { if (pos < 0) { return CursorStart (); } if (pos > _provider.Length) { return CursorEnd (); } int 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) { int c = _provider.FindEditPositionFrom (pos - 1, false); return c == -1 ? pos : c; } /// public int CursorRight (int pos) { int c = _provider.FindEditPositionFrom (pos + 1, true); return c == -1 ? pos : c; } /// public bool Delete (int pos) { string oldValue = Text; bool result = _provider.Replace (' ', pos); // .RemoveAt (pos); if (result) { OnTextChanged (new EventArgs (in oldValue)); } return result; } /// public bool InsertAt (char ch, int pos) { string oldValue = Text; bool result = _provider.Replace (ch, pos); if (result) { OnTextChanged (new EventArgs (in oldValue)); } return result; } /// public void OnTextChanged (EventArgs args) { TextChanged?.Invoke (this, args); } } #endregion #region TextRegexProvider /// Regex Provider for TextValidateField. public class TextRegexProvider : ITextValidateProvider { private List _pattern; private Regex _regex; private List _text; /// Empty Constructor. public TextRegexProvider (string pattern) { Pattern = pattern; } /// Regex pattern property. public string Pattern { get => StringExtensions.ToString (_pattern); set { _pattern = value.ToRuneList (); CompileMask (); SetupText (); } } /// When true, validates with the regex pattern on each input, preventing the input if it's not valid. public bool ValidateOnInput { get; set; } = true; /// public event EventHandler> TextChanged; /// public string Text { get => StringExtensions.ToString (_text); set { _text = value != string.Empty ? value.ToRuneList () : null; SetupText (); } } /// public string DisplayText => Text; /// public bool IsValid => Validate (_text); /// public bool Fixed => false; /// public int Cursor (int pos) { if (pos < 0) { return CursorStart (); } if (pos >= _text.Count) { return CursorEnd (); } 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) { string oldValue = Text; _text.RemoveAt (pos); OnTextChanged (new EventArgs (in oldValue)); } return true; } /// public bool InsertAt (char ch, int pos) { List aux = _text.ToList (); aux.Insert (pos, (Rune)ch); if (Validate (aux) || ValidateOnInput == false) { string oldValue = Text; _text.Insert (pos, (Rune)ch); OnTextChanged (new EventArgs (in oldValue)); return true; } return false; } /// public void OnTextChanged (EventArgs args) { TextChanged?.Invoke (this, args); } /// Compiles the regex pattern for validation./> private void CompileMask () { _regex = new Regex (StringExtensions.ToString (_pattern), RegexOptions.Compiled); } private void SetupText () { if (_text is { } && IsValid) { return; } _text = new List (); } private bool Validate (List text) { Match match = _regex.Match (StringExtensions.ToString (text)); return match.Success; } } #endregion } /// Text field that validates input through a public class TextValidateField : View { private readonly int _defaultLength = 10; private int _cursorPosition; private ITextValidateProvider _provider; /// /// Initializes a new instance of the class. /// public TextValidateField () { Height = Dim.Auto (minimumContentDim: 1); CanFocus = true; // Things this view knows how to do AddCommand ( Command.LeftStart, () => { 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 KeyBindings.Add (Key.Home, Command.LeftStart); KeyBindings.Add (Key.End, Command.RightEnd); KeyBindings.Add (Key.Delete, Command.DeleteCharRight); KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft); KeyBindings.Add (Key.CursorLeft, Command.Left); KeyBindings.Add (Key.CursorRight, Command.Right); } /// This property returns true if the input is valid. public virtual bool IsValid { get { if (_provider is null) { return false; } return _provider.IsValid; } } /// Provider public ITextValidateProvider Provider { get => _provider; set { _provider = value; if (_provider.Fixed) { Width = _provider.DisplayText == string.Empty ? _defaultLength : _provider.DisplayText.Length; } // HomeKeyHandler already call SetNeedsDisplay HomeKeyHandler (); } } /// Text public new string Text { get { if (_provider is null) { return string.Empty; } return _provider.Text; } set { if (_provider is null) { return; } _provider.Text = value; SetNeedsDisplay (); } } /// protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { int c = _provider.Cursor (mouseEvent.Position.X - GetMargins (Viewport.Width).left); if (_provider.Fixed == false && TextAlignment == Alignment.End && Text.Length > 0) { c++; } _cursorPosition = c; SetFocus (); SetNeedsDisplay (); return true; } return false; } /// protected override bool OnDrawingContent (Rectangle viewport) { if (_provider is null) { Move (0, 0); Driver?.AddStr ("Error: ITextValidateProvider not set!"); return true; } Color bgcolor = !IsValid ? new Color (Color.BrightRed) : ColorScheme.Focus.Background; var textColor = new Attribute (ColorScheme.Focus.Foreground, bgcolor); (int margin_left, int margin_right) = GetMargins (Viewport.Width); Move (0, 0); // Left Margin Driver?.SetAttribute (textColor); for (var i = 0; i < margin_left; i++) { Driver?.AddRune ((Rune)' '); } // Content Driver?.SetAttribute (textColor); // Content for (var i = 0; i < _provider.DisplayText.Length; i++) { Driver?.AddRune ((Rune)_provider.DisplayText [i]); } // Right Margin Driver?.SetAttribute (textColor); for (var i = 0; i < margin_right; i++) { Driver?.AddRune ((Rune)' '); } return true; } /// protected override bool OnKeyDownNotHandled (Key a) { if (_provider is null) { return false; } if (a.AsRune == default (Rune)) { return false; } Rune key = a.AsRune; bool inserted = _provider.InsertAt ((char)key.Value, _cursorPosition); if (inserted) { CursorRight (); } return false; } /// public override Point? PositionCursor () { (int left, _) = GetMargins (Viewport.Width); // Fixed = true, is for inputs that 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. int curPos; if (_provider?.Fixed == false && TextAlignment == Alignment.End) { curPos = _cursorPosition + left - 1; } else { curPos = _cursorPosition + left; } Move (curPos, 0); return new (curPos, 0); } /// Delete char at cursor position - 1, moving the cursor. /// private bool BackspaceKeyHandler () { if (_provider.Fixed == false && TextAlignment == Alignment.End && _cursorPosition <= 1) { return false; } _cursorPosition = _provider.CursorLeft (_cursorPosition); _provider.Delete (_cursorPosition); SetNeedsDisplay (); return true; } /// Try to move the cursor to the left. /// True if moved. private bool CursorLeft () { if (_provider is null) { return false; } int current = _cursorPosition; _cursorPosition = _provider.CursorLeft (_cursorPosition); SetNeedsDisplay (); return current != _cursorPosition; } /// Try to move the cursor to the right. /// True if moved. private bool CursorRight () { if (_provider is null) { return false; } int current = _cursorPosition; _cursorPosition = _provider.CursorRight (_cursorPosition); SetNeedsDisplay (); return current != _cursorPosition; } /// Deletes char at current position. /// private bool DeleteKeyHandler () { if (_provider.Fixed == false && TextAlignment == Alignment.End) { _cursorPosition = _provider.CursorLeft (_cursorPosition); } _provider.Delete (_cursorPosition); SetNeedsDisplay (); return true; } /// Moves the cursor to the last char. /// private bool EndKeyHandler () { _cursorPosition = _provider.CursorEnd (); SetNeedsDisplay (); return true; } /// Margins for text alignment. /// Total width /// Left and right margins private (int left, int right) GetMargins (int width) { int count = Text.Length; int total = width - count; switch (TextAlignment) { case Alignment.Start: return (0, total); case Alignment.Center: return (total / 2, total / 2 + total % 2); case Alignment.End: return (total, 0); default: return (0, total); } } /// Moves the cursor to first char. /// private bool HomeKeyHandler () { _cursorPosition = _provider.CursorStart (); SetNeedsDisplay (); return true; } } }