Browse Source

Feature TextValidateField (#1230)

* Feature TextValidateField

* Fix Mouse Click

* Fix Mouse click on TextRegexProvider

* UiCatalog fields with text alginment centered

* Fix Mouse click on TextRegexProvider when right aligned

* added newline to text.cs catalog

* NetMaskedTextProvider - changing the mask, try to use current input.
NetMaskedTextProvider - Left and Right Cursor dosen't wrap around.

* Add Some TextValidateField Tests.

* Add TextRegexProvider Tests

* Remove unnecessary using

* Tests namespace to Terminal.Gui.Views

* Regex Parse exception handling

* remove textmaskprovider in favor of .net maskedtextprovider

* refactoring and cleaning
José Miguel Perricone 4 năm trước cách đây
mục cha
commit
819dc291bc

+ 610 - 0
Terminal.Gui/Views/TextValidateField.cs

@@ -0,0 +1,610 @@
+//
+// TextValidateField.cs: single-line text editor with validation through providers.
+//
+// Authors:
+//	José Miguel Perricone ([email protected])
+//
+
+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 {
+		/// <summary>
+		/// TextValidateField Providers Interface.
+		/// All TextValidateField are created with a ITextValidateProvider.
+		/// </summary>
+		public interface ITextValidateProvider {
+			/// <summary>
+			/// Set that this provider uses a fixed width.
+			/// e.g. Masked ones are fixed.
+			/// </summary>
+			bool Fixed { get; }
+
+			/// <summary>
+			/// Set Cursor position to <paramref name="pos"/>.
+			/// </summary>
+			/// <param name="pos"></param>
+			/// <returns>Return first valid position.</returns>
+			int Cursor (int pos);
+
+			/// <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>
+			/// Find the last valid character position.
+			/// </summary>
+			/// <returns>New cursor position.</returns>
+			int CursorEnd ();
+
+			/// <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>
+			/// True if the input is valid, otherwise false.
+			/// </summary>
+			bool IsValid { get; }
+
+			/// <summary>
+			/// Set the input text, and get the formatted string for display.
+			/// </summary>
+			ustring Text { get; set; }
+
+			/// <summary>
+			/// Mask used for validation.
+			/// Not always a mask, can by a regex expression.
+			/// TODO: Maybe we can change the name.
+			/// </summary>
+			ustring Mask { get; set; }
+		}
+
+		//////////////////////////////////////////////////////////////////////////////
+		// 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 {
+			MaskedTextProvider provider;
+			string text;
+
+			/// <summary>
+			/// Empty Constructor
+			/// </summary>
+			public NetMaskedTextProvider () { }
+
+			///<inheritdoc/>
+			public ustring Mask {
+				get {
+					return provider?.Mask;
+				}
+				set {
+					provider = new MaskedTextProvider (value == ustring.Empty ? "&&&&&&" : value.ToString ());
+					if (string.IsNullOrEmpty (text) == false) {
+						provider.Set (text);
+					}
+				}
+			}
+
+			///<inheritdoc/>
+			public ustring Text {
+				get {
+					return provider.ToDisplayString ();
+				}
+				set {
+					text = value.ToString ();
+					provider.Set (value.ToString ());
+				}
+			}
+
+			///<inheritdoc/>
+			public bool IsValid => provider.MaskCompleted;
+
+			///<inheritdoc/>
+			public bool Fixed => true;
+
+			///<inheritdoc/>
+			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;
+				}
+			}
+
+			///<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)
+			{
+				var c = provider.FindEditPositionFrom (pos - 1, false);
+				return c == -1 ? pos : c;
+			}
+
+			///<inheritdoc/>
+			public int CursorRight (int pos)
+			{
+				var c = provider.FindEditPositionFrom (pos + 1, true);
+				return c == -1 ? pos : c;
+			}
+
+			///<inheritdoc/>
+			public bool Delete (int pos)
+			{
+				return provider.Replace (' ', pos);// .RemoveAt (pos);
+			}
+
+			///<inheritdoc/>
+			public bool InsertAt (char ch, int pos)
+			{
+				return provider.Replace (ch, pos);
+			}
+		}
+		#endregion
+
+		#region TextRegexProvider
+
+		/// <summary>
+		/// Regex Provider for TextValidateField.
+		/// </summary>
+		public class TextRegexProvider : ITextValidateProvider {
+			Regex regex;
+			List<Rune> text;
+			List<Rune> mask;
+
+			/// <summary>
+			/// Empty Constructor
+			/// </summary>
+			public TextRegexProvider () { }
+
+			///<inheritdoc/>
+			public ustring Mask {
+				get {
+					return ustring.Make (mask);
+				}
+				set {
+					mask = value.ToRuneList ();
+					CompileMask ();
+					SetupText ();
+				}
+			}
+
+			///<inheritdoc/>
+			public ustring Text {
+				get {
+					return ustring.Make (text);
+				}
+				set {
+					text = value != ustring.Empty ? value.ToRuneList () : null;
+					SetupText ();
+				}
+			}
+
+			///<inheritdoc/>
+			public bool IsValid {
+				get {
+					return Validate (text);
+				}
+			}
+
+			///<inheritdoc/>
+			public bool Fixed => false;
+
+			/// <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;
+
+			bool Validate (List<Rune> text)
+			{
+				var match = regex.Match (ustring.Make (text).ToString ());
+				return match.Success;
+			}
+
+			///<inheritdoc/>
+			public int Cursor (int pos)
+			{
+				if (pos < 0) {
+					return CursorStart ();
+				} else if (pos >= text.Count) {
+					return CursorEnd ();
+				} else {
+					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) {
+					text.RemoveAt (pos);
+				}
+				return true;
+			}
+
+			///<inheritdoc/>
+			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<Rune> ();
+			}
+
+			/// <summary>
+			/// Compiles the regex pattern for validation./>
+			/// </summary>
+			private void CompileMask ()
+			{
+				regex = new Regex (ustring.Make (mask).ToString (), RegexOptions.Compiled);
+			}
+		}
+		#endregion
+	}
+
+	/// <summary>
+	/// Text field that validates input through a  <see cref="ITextValidateProvider"/>
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	public class TextValidateField<T> : View where T : ITextValidateProvider {
+
+		ITextValidateProvider provider;
+		int cursorPosition = 0;
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="TextValidateField{T}"/> class using <see cref="LayoutStyle.Computed"/> positioning.
+		/// </summary>
+		public TextValidateField () : this (ustring.Empty)
+		{
+		}
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="TextValidateField{T}"></see>  class using <see cref="LayoutStyle.Computed"/> positioning.
+		/// </summary>
+		/// <param name="mask">Mask</param>
+		public TextValidateField (ustring mask) : this (mask, ustring.Empty) { }
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="TextValidateField{T}"/> class using <see cref="LayoutStyle.Computed"/> positioning.
+		/// </summary>
+		/// <param name="mask"></param>
+		/// <param name="text">Initial Value</param>
+		public TextValidateField (ustring mask, ustring text) : base ()
+		{
+			provider = Activator.CreateInstance (typeof (T)) as ITextValidateProvider;
+
+			Mask = mask;
+			Text = text;
+
+			this.Width = text == ustring.Empty ? 20 : Text.Length;
+			this.Height = 1;
+			this.CanFocus = true;
+		}
+
+		/// <summary>
+		/// Get the Provider
+		/// </summary>
+		public T Provider => (T)provider;
+
+		///<inheritdoc/>
+		public override bool MouseEvent (MouseEvent mouseEvent)
+		{
+			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;
+		}
+
+		/// <summary>
+		/// Text
+		/// </summary>
+		public new ustring Text {
+			get {
+				return provider.Text;
+			}
+			set {
+				provider.Text = value;
+
+				SetNeedsDisplay ();
+			}
+		}
+
+		/// <summary>
+		/// Mask
+		/// </summary>
+		public ustring Mask {
+			get {
+				return provider.Mask;
+			}
+			set {
+				provider.Mask = value;
+
+				cursorPosition = provider.CursorStart ();
+
+				SetNeedsDisplay ();
+			}
+		}
+
+		///inheritdoc/>
+		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);
+			}
+		}
+
+		/// <summary>
+		/// Margins for text alignment.
+		/// </summary>
+		/// <param name="width">Total width</param>
+		/// <returns>Left and right margins</returns>
+		(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);
+			}
+		}
+
+		///<inheritdoc/>
+		public override void Redraw (Rect bounds)
+		{
+			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.Text.Length; i++) {
+				Driver.AddRune (provider.Text [i]);
+			}
+
+			// Right Margin
+			Driver.SetAttribute (textColor);
+			for (int i = 0; i < margin_right; i++) {
+				Driver.AddRune (' ');
+			}
+		}
+
+		/// <summary>
+		/// Try to move the cursor to the left.
+		/// </summary>
+		/// <returns>True if moved.</returns>
+		bool CursorLeft ()
+		{
+			var current = cursorPosition;
+			cursorPosition = provider.CursorLeft (cursorPosition);
+			return current != cursorPosition;
+		}
+
+		/// <summary>
+		/// Try to move the cursor to the right.
+		/// </summary>
+		/// <returns>True if moved.</returns>
+		bool CursorRight ()
+		{
+			var current = cursorPosition;
+			cursorPosition = provider.CursorRight (cursorPosition);
+			return current != cursorPosition;
+		}
+
+		/// <summary>
+		/// Delete char at cursor position - 1, moving the cursor.
+		/// </summary>
+		/// <returns></returns>
+		bool BackspaceKeyHandler ()
+		{
+			if (provider.Fixed == false && TextAlignment == TextAlignment.Right && cursorPosition <= 1) {
+				return false;
+			}
+			cursorPosition = provider.CursorLeft (cursorPosition);
+			provider.Delete (cursorPosition);
+			return true;
+		}
+
+		/// <summary>
+		/// Deletes char at current position.
+		/// </summary>
+		/// <returns></returns>
+		bool DeleteKeyHandler ()
+		{
+			if (provider.Fixed == false && TextAlignment == TextAlignment.Right) {
+				cursorPosition = provider.CursorLeft (cursorPosition);
+			}
+			provider.Delete (cursorPosition);
+			return true;
+		}
+
+		/// <summary>
+		/// Moves the cursor to first char.
+		/// </summary>
+		/// <returns></returns>
+		bool HomeKeyHandler ()
+		{
+			cursorPosition = provider.CursorStart ();
+			return true;
+		}
+
+		/// <summary>
+		/// Moves the cursor to the last char.
+		/// </summary>
+		/// <returns></returns>
+		bool EndKeyHandler ()
+		{
+			cursorPosition = provider.CursorEnd ();
+			return true;
+		}
+
+		///<inheritdoc/>
+		public override bool ProcessKey (KeyEvent kb)
+		{
+			switch (kb.Key) {
+			case Key.Home: HomeKeyHandler (); break;
+			case Key.End: EndKeyHandler (); break;
+			case Key.Delete:
+			case Key.DeleteChar: DeleteKeyHandler (); break;
+			case Key.Backspace: BackspaceKeyHandler (); break;
+			case Key.CursorLeft: CursorLeft (); break;
+			case Key.CursorRight: CursorRight (); break;
+			default:
+				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 ();
+				}
+
+				break;
+			}
+
+			SetNeedsDisplay ();
+			return true;
+		}
+
+		/// <summary>
+		/// This property returns true if the input is valid.
+		/// </summary>
+		public virtual bool IsValid {
+			get {
+				return provider.IsValid;
+			}
+		}
+	}
+}

+ 36 - 2
UICatalog/Scenarios/Text.cs

@@ -1,6 +1,9 @@
 using System;
 using System;
 using System.Text;
 using System.Text;
 using Terminal.Gui;
 using Terminal.Gui;
+using Terminal.Gui.TextValidateProviders;
+
+
 
 
 namespace UICatalog {
 namespace UICatalog {
 	[ScenarioMetadata (Name: "Text Input Controls", Description: "Tests all text input controls")]
 	[ScenarioMetadata (Name: "Text Input Controls", Description: "Tests all text input controls")]
@@ -101,6 +104,38 @@ namespace UICatalog {
 
 
 			_timeField.TimeChanged += TimeChanged;
 			_timeField.TimeChanged += TimeChanged;
 
 
+			// MaskedTextProvider
+			var netProvider = new Label (".Net MaskedTextProvider [ 999 000 LLL >LLL| AAA aaa ]") {
+				X = Pos.Left (dateField),
+				Y = Pos.Bottom (dateField) + 1
+			};
+			Win.Add (netProvider);
+
+			var netProviderField = new TextValidateField<NetMaskedTextProvider> ("999 000 LLL >LLL| AAA aaa") {
+				X = Pos.Right (netProvider) + 1,
+				Y = Pos.Y (netProvider),
+				Width = 40,
+				TextAlignment = TextAlignment.Centered
+			};
+			Win.Add (netProviderField);
+
+			// TextRegexProvider
+			var regexProvider = new Label ("Gui.cs TextRegexProvider [ ^([0-9]?[0-9]?[0-9]|1000)$ ]") {
+				X = Pos.Left (netProvider),
+				Y = Pos.Bottom (netProvider) + 1
+			};
+			Win.Add (regexProvider);
+
+			var regexProviderField = new TextValidateField<TextRegexProvider> ("^([0-9]?[0-9]?[0-9]|1000)$") {
+				X = Pos.Right (regexProvider) + 1,
+				Y = Pos.Y (regexProvider),
+				Width = 40,
+				TextAlignment = TextAlignment.Centered
+			};
+			// Access the inner Provider to configure.
+			regexProviderField.Provider.ValidateOnInput = false;
+
+			Win.Add (regexProviderField);
 		}
 		}
 
 
 		TimeField _timeField;
 		TimeField _timeField;
@@ -109,7 +144,6 @@ namespace UICatalog {
 		private void TimeChanged (DateTimeEventArgs<TimeSpan> e)
 		private void TimeChanged (DateTimeEventArgs<TimeSpan> e)
 		{
 		{
 			_labelMirroringTimeField.Text = _timeField.Text;
 			_labelMirroringTimeField.Text = _timeField.Text;
-
 		}
 		}
 	}
 	}
-}
+}

+ 560 - 0
UnitTests/TextValidateFieldTests.cs

@@ -0,0 +1,560 @@
+using System.Text.RegularExpressions;
+using Terminal.Gui.TextValidateProviders;
+
+using Xunit;
+
+namespace Terminal.Gui.Views {
+	public class TextValidateField_NET_Provider_Tests {
+		public TextValidateField_NET_Provider_Tests ()
+		{
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+		}
+
+		[Fact]
+		public void Initialized_With_Cursor_On_First_Editable_Character ()
+		{
+			//                                                            *
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+
+			Assert.Equal ("--(1___)--", field.Text);
+		}
+
+		[Fact]
+		public void Input_Ilegal_Character ()
+		{
+			//                                                            *
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			field.ProcessKey (new KeyEvent (Key.A, new KeyModifiers { }));
+
+			Assert.Equal ("--(____)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Home_Key_First_Editable_Character ()
+		{
+			//                                                            *
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			field.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers { }));
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+
+			Assert.Equal ("--(1___)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void End_Key_Last_Editable_Character ()
+		{
+			//                                                               *
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			field.ProcessKey (new KeyEvent (Key.End, new KeyModifiers { }));
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+			Assert.Equal ("--(___1)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Right_Key_Stops_In_Last_Editable_Character ()
+		{
+			//                                                               *
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			for (int i = 0; i < 10; i++) {
+				field.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers { }));
+			}
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+
+			Assert.Equal ("--(___1)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Left_Key_Stops_In_First_Editable_Character ()
+		{
+			//                                                            *
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			for (int i = 0; i < 10; i++) {
+				field.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers { }));
+			}
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+
+			Assert.Equal ("--(1___)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void When_Valid_Is_Valid_True ()
+		{
+			//                                                            ****
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+			Assert.Equal ("--(1___)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.D2, new KeyModifiers { }));
+			Assert.Equal ("--(12__)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.D3, new KeyModifiers { }));
+			Assert.Equal ("--(123_)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers { }));
+			Assert.Equal ("--(1234)--", field.Text);
+			Assert.True (field.IsValid);
+		}
+
+		[Fact]
+		public void Insert_Skips_Non_Editable_Characters ()
+		{
+			//                                                            ** **
+			//                                                         01234567890
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(00-00)--") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+			Assert.Equal ("--(1_-__)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.D2, new KeyModifiers { }));
+			Assert.Equal ("--(12-__)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.D3, new KeyModifiers { }));
+			Assert.Equal ("--(12-3_)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers { }));
+			Assert.Equal ("--(12-34)--", field.Text);
+			Assert.True (field.IsValid);
+		}
+
+
+		[Fact]
+		public void Initial_Value_Exact_Valid ()
+		{
+			//                                                            ****
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--", "1234") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			Assert.Equal ("--(1234)--", field.Text);
+			Assert.True (field.IsValid);
+		}
+
+		[Fact]
+		public void Initial_Value_Bigger_Than_Mask_Discarded ()
+		{
+			//                                                            ****
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--", "12345") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			Assert.Equal ("--(____)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Initial_Value_Smaller_Than_Mask_Accepted ()
+		{
+			//                                                            ****
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--", "123") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			Assert.Equal ("--(123_)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Delete_Key_Dosent_Move_Cursor ()
+		{
+			//                                                            ****
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--", "1234") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			Assert.Equal ("--(1234)--", field.Text);
+			Assert.True (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers { }));
+
+			Assert.Equal ("--(_234)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers { }));
+
+			field.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers { }));
+
+			Assert.Equal ("--(_2_4)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Backspace_Key_Deletes_Previous_Character ()
+		{
+			//                                                            ****
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--", "1234") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+
+			// Go to the end.
+			field.ProcessKey (new KeyEvent (Key.End, new KeyModifiers { }));
+
+			field.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers { }));
+			Assert.Equal ("--(12_4)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers { }));
+			Assert.Equal ("--(1__4)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers { }));
+			Assert.Equal ("--(___4)--", field.Text);
+			Assert.False (field.IsValid);
+
+			// One more
+			field.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers { }));
+			Assert.Equal ("--(___4)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+
+		[Fact]
+		public void Set_Text_After_Initialization ()
+		{
+			//                                                            ****
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Left,
+				Width = 30
+			};
+
+			field.Text = "1234";
+
+			Assert.Equal ("--(1234)--", field.Text);
+			Assert.True (field.IsValid);
+		}
+
+		[Fact]
+		public void Changing_The_Mask_Tries_To_Keep_The_Previous_Text ()
+		{
+			//                                                            ****
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Left,
+				Width = 30
+			};
+
+			field.Text = "1234";
+			Assert.Equal ("--(1234)--", field.Text);
+			Assert.True (field.IsValid);
+
+			field.Mask = "--------(00000000)--------";
+			Assert.Equal ("--------(1234____)--------", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void MouseClick_Right_X_Greater_Than_Text_Width_Goes_To_Last_Editable_Position ()
+		{
+			//                                                            ****
+			//                                                         0123456789
+			var field = new TextValidateField<NetMaskedTextProvider> ("--(0000)--") {
+				TextAlignment = TextAlignment.Left,
+				Width = 30
+			};
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+
+			Assert.Equal ("--(1___)--", field.Text);
+			Assert.False (field.IsValid);
+
+			field.MouseEvent (new MouseEvent () { X = 25, Flags = MouseFlags.Button1Clicked });
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+
+			Assert.Equal ("--(1__1)--", field.Text);
+			Assert.False (field.IsValid);
+		}
+	}
+
+	public class TextValidateField_Regex_Provider_Tests {
+		public TextValidateField_Regex_Provider_Tests ()
+		{
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+		}
+
+		[Fact]
+		public void Input_Without_Validate_On_Input ()
+		{
+			var field = new TextValidateField<TextRegexProvider> ("^[0-9][0-9][0-9]$") {
+				Width = 20
+			};
+
+			// Let you input
+			field.Provider.ValidateOnInput = false;
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+			Assert.Equal ("1", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.D2, new KeyModifiers { }));
+			Assert.Equal ("12", field.Text);
+			Assert.False (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.D3, new KeyModifiers { }));
+			Assert.Equal ("123", field.Text);
+			Assert.True (field.IsValid);
+
+			field.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers { }));
+			Assert.Equal ("1234", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Input_With_Validate_On_Input_Set_Text ()
+		{
+			var field = new TextValidateField<TextRegexProvider> ("^[0-9][0-9][0-9]$") {
+				Width = 20
+			};
+
+			// Input dosen't validates the pattern.
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+			Assert.Equal ("", field.Text);
+			Assert.False (field.IsValid);
+
+			// Dosen't match
+			field.Text = "12356";
+			Assert.Equal ("", field.Text);
+			Assert.False (field.IsValid);
+
+			// Yes.
+			field.Text = "123";
+			Assert.Equal ("123", field.Text);
+			Assert.True (field.IsValid);
+		}
+
+		[Fact]
+		public void Empty_Mask_Validates_Everything ()
+		{
+			// Maybe it's not the right behaviour.
+
+			var field = new TextValidateField<TextRegexProvider> () {
+				Width = 20
+			};
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+			Assert.Equal ("1", field.Text);
+			Assert.True (field.IsValid);
+		}
+
+		[Fact]
+		public void Text_With_All_Charset ()
+		{
+			var field = new TextValidateField<TextRegexProvider> ("^[0-9][0-9][0-9]$") {
+				Width = 20
+			};
+
+			var text = "";
+			for (int i = 0; i < 255; i++) {
+				text += (char)i;
+			}
+
+			field.Text = text;
+
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Mask_With_Invalid_Pattern_Exception ()
+		{
+			// Regex Exception
+			// Maybe it's not the right behaviour.
+
+			var mask = "";
+			for (int i = 0; i < 255; i++) {
+				mask += (char)i;
+			}
+
+			try {
+				var field = new TextValidateField<TextRegexProvider> (mask) {
+					Width = 20
+				};
+			} catch (RegexParseException ex) {
+				Assert.True (true, ex.Message);
+				return;
+			}
+			Assert.True (false);
+		}
+
+		[Fact]
+		public void Home_Key_First_Editable_Character ()
+		{
+			// Range 0 to 1000
+			// Accepts 001 too.
+			var field = new TextValidateField<TextRegexProvider> ("^[0-9]?[0-9]?[0-9]|1000$") {
+				Width = 20
+			};
+
+			field.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.D0, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.D0, new KeyModifiers { }));
+			field.ProcessKey (new KeyEvent (Key.D0, new KeyModifiers { }));
+
+			Assert.Equal ("1000", field.Text);
+			Assert.True (field.IsValid);
+
+			// HOME KEY
+			field.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers { }));
+
+			// DELETE
+			field.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers { }));
+
+			Assert.Equal ("000", field.Text);
+			Assert.True (field.IsValid);
+		}
+
+		[Fact]
+		public void End_Key_End_Of_Input ()
+		{
+			// Exactly 5 numbers
+			var field = new TextValidateField<TextRegexProvider> ("^[0-9]{5}$") {
+				Width = 20
+			};
+
+			field.Provider.ValidateOnInput = false;
+
+			for (int i = 0; i < 4; i++) {
+				field.ProcessKey (new KeyEvent (Key.D0, new KeyModifiers { }));
+			}
+
+			Assert.Equal ("0000", field.Text);
+			Assert.False (field.IsValid);
+
+			// HOME KEY
+			field.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers { }));
+
+			// END KEY
+			field.ProcessKey (new KeyEvent (Key.End, new KeyModifiers { }));
+
+			// Insert 9
+			field.ProcessKey (new KeyEvent (Key.D9, new KeyModifiers { }));
+
+			Assert.Equal ("00009", field.Text);
+			Assert.True (field.IsValid);
+
+			// Insert 9
+			field.ProcessKey (new KeyEvent (Key.D9, new KeyModifiers { }));
+
+			Assert.Equal ("000099", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Right_Key_Stops_At_End_And_Insert ()
+		{
+			var field = new TextValidateField<TextRegexProvider> ("^[0-9][0-9][0-9]$") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+			field.Provider.ValidateOnInput = false;
+
+			field.Text = "123";
+
+			for (int i = 0; i < 10; i++) {
+				field.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers { }));
+			}
+
+			Assert.Equal ("123", field.Text);
+			Assert.True (field.IsValid);
+
+			// Insert 4
+			field.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers { }));
+
+			Assert.Equal ("1234", field.Text);
+			Assert.False (field.IsValid);
+		}
+
+		[Fact]
+		public void Left_Key_Stops_At_Start_And_Insert ()
+		{
+			var field = new TextValidateField<TextRegexProvider> ("^[0-9][0-9][0-9]$") {
+				TextAlignment = TextAlignment.Centered,
+				Width = 20
+			};
+			field.Provider.ValidateOnInput = false;
+
+			field.Text = "123";
+
+			for (int i = 0; i < 10; i++) {
+				field.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers { }));
+			}
+
+			Assert.Equal ("123", field.Text);
+			Assert.True (field.IsValid);
+
+			// Insert 4
+			field.ProcessKey (new KeyEvent (Key.D4, new KeyModifiers { }));
+
+			Assert.Equal ("4123", field.Text);
+			Assert.False (field.IsValid);
+		}
+	}
+}