123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751 |
- //
- // TextValidateField.cs: single-line text editor with validation through providers.
- //
- // Authors:
- // José Miguel Perricone ([email protected])
- //
- using System.ComponentModel;
- using System.Text.RegularExpressions;
- using Terminal.Gui.TextValidateProviders;
- namespace Terminal.Gui
- {
- namespace TextValidateProviders
- {
- /// <summary>TextValidateField Providers Interface. All TextValidateField are created with a ITextValidateProvider.</summary>
- public interface ITextValidateProvider
- {
- /// <summary>Gets the formatted string for display.</summary>
- string DisplayText { get; }
- /// <summary>Set that this provider uses a fixed width. e.g. Masked ones are fixed.</summary>
- bool Fixed { get; }
- /// <summary>True if the input is valid, otherwise false.</summary>
- bool IsValid { get; }
- /// <summary>Set the input text and get the current value.</summary>
- string Text { get; set; }
- /// <summary>Set Cursor position to <paramref name="pos"/>.</summary>
- /// <param name="pos"></param>
- /// <returns>Return first valid position.</returns>
- int Cursor (int pos);
- /// <summary>Find the last valid character position.</summary>
- /// <returns>New cursor position.</returns>
- int CursorEnd ();
- /// <summary>First valid position before <paramref name="pos"/>.</summary>
- /// <param name="pos"></param>
- /// <returns>New cursor position if any, otherwise returns <paramref name="pos"/></returns>
- int CursorLeft (int pos);
- /// <summary>First valid position after <paramref name="pos"/>.</summary>
- /// <param name="pos">Current position.</param>
- /// <returns>New cursor position if any, otherwise returns <paramref name="pos"/></returns>
- int CursorRight (int pos);
- /// <summary>Find the first valid character position.</summary>
- /// <returns>New cursor position.</returns>
- int CursorStart ();
- /// <summary>Deletes the current character in <paramref name="pos"/>.</summary>
- /// <param name="pos"></param>
- /// <returns>true if the character was successfully removed, otherwise false.</returns>
- bool Delete (int pos);
- /// <summary>Insert character <paramref name="ch"/> in position <paramref name="pos"/>.</summary>
- /// <param name="ch"></param>
- /// <param name="pos"></param>
- /// <returns>true if the character was successfully inserted, otherwise false.</returns>
- bool InsertAt (char ch, int pos);
- /// <summary>Method that invoke the <see cref="TextChanged"/> event if it's defined.</summary>
- /// <param name="oldValue">The previous text before replaced.</param>
- /// <returns>Returns the <see cref="EventArgs{T}"/></returns>
- void OnTextChanged (EventArgs<string> oldValue);
- /// <summary>
- /// Changed event, raised when the text has changed.
- /// <remarks>
- /// This event is raised when the <see cref="Text"/> changes. The passed <see cref="EventArgs"/> is a
- /// <see cref="string"/> containing the old value.
- /// </remarks>
- /// </summary>
- event EventHandler<EventArgs<string>> TextChanged;
- }
- //////////////////////////////////////////////////////////////////////////////
- // PROVIDERS
- //////////////////////////////////////////////////////////////////////////////
- #region NetMaskedTextProvider
- /// <summary>
- /// .Net MaskedTextProvider Provider for TextValidateField.
- /// <para></para>
- /// <para>
- /// <a
- /// href="https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.maskedtextprovider?view=net-5.0">
- /// Wrapper around MaskedTextProvider
- /// </a>
- /// </para>
- /// <para>
- /// <a
- /// href="https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.maskedtextbox.mask?view=net-5.0">
- /// Masking elements
- /// </a>
- /// </para>
- /// </summary>
- public class NetMaskedTextProvider : ITextValidateProvider
- {
- private MaskedTextProvider _provider;
- /// <summary>Empty Constructor</summary>
- public NetMaskedTextProvider (string mask) { Mask = mask; }
- /// <summary>Mask property</summary>
- 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);
- }
- }
- }
- /// <inheritdoc/>
- public event EventHandler<EventArgs<string>> TextChanged;
- /// <inheritdoc/>
- public string Text
- {
- get => _provider.ToString ();
- set => _provider.Set (value);
- }
- /// <inheritdoc/>
- public bool IsValid => _provider.MaskCompleted;
- /// <inheritdoc/>
- public bool Fixed => true;
- /// <inheritdoc/>
- public string DisplayText => _provider.ToDisplayString ();
- /// <inheritdoc/>
- 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;
- }
- /// <inheritdoc/>
- public int CursorStart ()
- {
- return _provider.IsEditPosition (0)
- ? 0
- : _provider.FindEditPositionFrom (0, true);
- }
- /// <inheritdoc/>
- public int CursorEnd ()
- {
- return _provider.IsEditPosition (_provider.Length - 1)
- ? _provider.Length - 1
- : _provider.FindEditPositionFrom (_provider.Length, false);
- }
- /// <inheritdoc/>
- public int CursorLeft (int pos)
- {
- int c = _provider.FindEditPositionFrom (pos - 1, false);
- return c == -1 ? pos : c;
- }
- /// <inheritdoc/>
- public int CursorRight (int pos)
- {
- int c = _provider.FindEditPositionFrom (pos + 1, true);
- return c == -1 ? pos : c;
- }
- /// <inheritdoc/>
- public bool Delete (int pos)
- {
- string oldValue = Text;
- bool result = _provider.Replace (' ', pos); // .RemoveAt (pos);
- if (result)
- {
- OnTextChanged (new EventArgs<string> (in oldValue));
- }
- return result;
- }
- /// <inheritdoc/>
- public bool InsertAt (char ch, int pos)
- {
- string oldValue = Text;
- bool result = _provider.Replace (ch, pos);
- if (result)
- {
- OnTextChanged (new EventArgs<string> (in oldValue));
- }
- return result;
- }
- /// <inheritdoc/>
- public void OnTextChanged (EventArgs<string> args) { TextChanged?.Invoke (this, args); }
- }
- #endregion
- #region TextRegexProvider
- /// <summary>Regex Provider for TextValidateField.</summary>
- public class TextRegexProvider : ITextValidateProvider
- {
- private List<Rune> _pattern;
- private Regex _regex;
- private List<Rune> _text;
- /// <summary>Empty Constructor.</summary>
- public TextRegexProvider (string pattern) { Pattern = pattern; }
- /// <summary>Regex pattern property.</summary>
- public string Pattern
- {
- get => StringExtensions.ToString (_pattern);
- set
- {
- _pattern = value.ToRuneList ();
- CompileMask ();
- SetupText ();
- }
- }
- /// <summary>When true, validates with the regex pattern on each input, preventing the input if it's not valid.</summary>
- public bool ValidateOnInput { get; set; } = true;
- /// <inheritdoc/>
- public event EventHandler<EventArgs<string>> TextChanged;
- /// <inheritdoc/>
- public string Text
- {
- get => StringExtensions.ToString (_text);
- set
- {
- _text = value != string.Empty ? value.ToRuneList () : null;
- SetupText ();
- }
- }
- /// <inheritdoc/>
- public string DisplayText => Text;
- /// <inheritdoc/>
- public bool IsValid => Validate (_text);
- /// <inheritdoc/>
- public bool Fixed => false;
- /// <inheritdoc/>
- public int Cursor (int pos)
- {
- if (pos < 0)
- {
- return CursorStart ();
- }
- if (pos >= _text.Count)
- {
- return CursorEnd ();
- }
- return pos;
- }
- /// <inheritdoc/>
- public int CursorStart () { return 0; }
- /// <inheritdoc/>
- public int CursorEnd () { return _text.Count; }
- /// <inheritdoc/>
- public int CursorLeft (int pos)
- {
- if (pos > 0)
- {
- return pos - 1;
- }
- return pos;
- }
- /// <inheritdoc/>
- public int CursorRight (int pos)
- {
- if (pos < _text.Count)
- {
- return pos + 1;
- }
- return pos;
- }
- /// <inheritdoc/>
- public bool Delete (int pos)
- {
- if (_text.Count > 0 && pos < _text.Count)
- {
- string oldValue = Text;
- _text.RemoveAt (pos);
- OnTextChanged (new EventArgs<string> (in oldValue));
- }
- return true;
- }
- /// <inheritdoc/>
- public bool InsertAt (char ch, int pos)
- {
- List<Rune> aux = _text.ToList ();
- aux.Insert (pos, (Rune)ch);
- if (Validate (aux) || ValidateOnInput == false)
- {
- string oldValue = Text;
- _text.Insert (pos, (Rune)ch);
- OnTextChanged (new EventArgs<string> (in oldValue));
- return true;
- }
- return false;
- }
- /// <inheritdoc/>
- public void OnTextChanged (EventArgs<string> args) { TextChanged?.Invoke (this, args); }
- /// <summary>Compiles the regex pattern for validation./></summary>
- private void CompileMask () { _regex = new Regex (StringExtensions.ToString (_pattern), RegexOptions.Compiled); }
- private void SetupText ()
- {
- if (_text is { } && IsValid)
- {
- return;
- }
- _text = new List<Rune> ();
- }
- private bool Validate (List<Rune> text)
- {
- Match match = _regex.Match (StringExtensions.ToString (text));
- return match.Success;
- }
- }
- #endregion
- }
- /// <summary>Text field that validates input through a <see cref="ITextValidateProvider"/></summary>
- public class TextValidateField : View
- {
- private readonly int _defaultLength = 10;
- private int _cursorPosition;
- private ITextValidateProvider _provider;
- /// <summary>
- /// Initializes a new instance of the <see cref="TextValidateField"/> class.
- /// </summary>
- 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);
- }
- /// <summary>This property returns true if the input is valid.</summary>
- public virtual bool IsValid
- {
- get
- {
- if (_provider is null)
- {
- return false;
- }
- return _provider.IsValid;
- }
- }
- /// <summary>Provider</summary>
- 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 ();
- }
- }
- /// <summary>Text</summary>
- 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 ();
- }
- }
- /// <inheritdoc/>
- protected override bool OnMouseEvent (MouseEvent 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;
- }
- /// <inheritdoc/>
- public override void OnDrawContent (Rectangle viewport)
- {
- if (_provider is null)
- {
- Move (0, 0);
- Driver.AddStr ("Error: ITextValidateProvider not set!");
- return;
- }
- 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)' ');
- }
- }
- /// <inheritdoc/>
- 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;
- }
- /// <inheritdoc/>
- 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);
- }
- /// <summary>Delete char at cursor position - 1, moving the cursor.</summary>
- /// <returns></returns>
- private bool BackspaceKeyHandler ()
- {
- if (_provider.Fixed == false && TextAlignment == Alignment.End && _cursorPosition <= 1)
- {
- return false;
- }
- _cursorPosition = _provider.CursorLeft (_cursorPosition);
- _provider.Delete (_cursorPosition);
- SetNeedsDisplay ();
- return true;
- }
- /// <summary>Try to move the cursor to the left.</summary>
- /// <returns>True if moved.</returns>
- private bool CursorLeft ()
- {
- if (_provider is null)
- {
- return false;
- }
- int current = _cursorPosition;
- _cursorPosition = _provider.CursorLeft (_cursorPosition);
- SetNeedsDisplay ();
- return current != _cursorPosition;
- }
- /// <summary>Try to move the cursor to the right.</summary>
- /// <returns>True if moved.</returns>
- private bool CursorRight ()
- {
- if (_provider is null)
- {
- return false;
- }
- int current = _cursorPosition;
- _cursorPosition = _provider.CursorRight (_cursorPosition);
- SetNeedsDisplay ();
- return current != _cursorPosition;
- }
- /// <summary>Deletes char at current position.</summary>
- /// <returns></returns>
- private bool DeleteKeyHandler ()
- {
- if (_provider.Fixed == false && TextAlignment == Alignment.End)
- {
- _cursorPosition = _provider.CursorLeft (_cursorPosition);
- }
- _provider.Delete (_cursorPosition);
- SetNeedsDisplay ();
- return true;
- }
- /// <summary>Moves the cursor to the last char.</summary>
- /// <returns></returns>
- private bool EndKeyHandler ()
- {
- _cursorPosition = _provider.CursorEnd ();
- SetNeedsDisplay ();
- return true;
- }
- /// <summary>Margins for text alignment.</summary>
- /// <param name="width">Total width</param>
- /// <returns>Left and right margins</returns>
- 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);
- }
- }
- /// <summary>Moves the cursor to first char.</summary>
- /// <returns></returns>
- private bool HomeKeyHandler ()
- {
- _cursorPosition = _provider.CursorStart ();
- SetNeedsDisplay ();
- return true;
- }
- }
- }
|