TextValidateField.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. //
  2. // TextValidateField.cs: single-line text editor with validation through providers.
  3. //
  4. // Authors:
  5. // José Miguel Perricone ([email protected])
  6. //
  7. using System.ComponentModel;
  8. using System.Text.RegularExpressions;
  9. using Terminal.Gui.TextValidateProviders;
  10. namespace Terminal.Gui
  11. {
  12. namespace TextValidateProviders
  13. {
  14. /// <summary>TextValidateField Providers Interface. All TextValidateField are created with a ITextValidateProvider.</summary>
  15. public interface ITextValidateProvider
  16. {
  17. /// <summary>Gets the formatted string for display.</summary>
  18. string DisplayText { get; }
  19. /// <summary>Set that this provider uses a fixed width. e.g. Masked ones are fixed.</summary>
  20. bool Fixed { get; }
  21. /// <summary>True if the input is valid, otherwise false.</summary>
  22. bool IsValid { get; }
  23. /// <summary>Set the input text and get the current value.</summary>
  24. string Text { get; set; }
  25. /// <summary>Set Cursor position to <paramref name="pos"/>.</summary>
  26. /// <param name="pos"></param>
  27. /// <returns>Return first valid position.</returns>
  28. int Cursor (int pos);
  29. /// <summary>Find the last valid character position.</summary>
  30. /// <returns>New cursor position.</returns>
  31. int CursorEnd ();
  32. /// <summary>First valid position before <paramref name="pos"/>.</summary>
  33. /// <param name="pos"></param>
  34. /// <returns>New cursor position if any, otherwise returns <paramref name="pos"/></returns>
  35. int CursorLeft (int pos);
  36. /// <summary>First valid position after <paramref name="pos"/>.</summary>
  37. /// <param name="pos">Current position.</param>
  38. /// <returns>New cursor position if any, otherwise returns <paramref name="pos"/></returns>
  39. int CursorRight (int pos);
  40. /// <summary>Find the first valid character position.</summary>
  41. /// <returns>New cursor position.</returns>
  42. int CursorStart ();
  43. /// <summary>Deletes the current character in <paramref name="pos"/>.</summary>
  44. /// <param name="pos"></param>
  45. /// <returns>true if the character was successfully removed, otherwise false.</returns>
  46. bool Delete (int pos);
  47. /// <summary>Insert character <paramref name="ch"/> in position <paramref name="pos"/>.</summary>
  48. /// <param name="ch"></param>
  49. /// <param name="pos"></param>
  50. /// <returns>true if the character was successfully inserted, otherwise false.</returns>
  51. bool InsertAt (char ch, int pos);
  52. /// <summary>Method that invoke the <see cref="TextChanged"/> event if it's defined.</summary>
  53. /// <param name="oldValue">The previous text before replaced.</param>
  54. /// <returns>Returns the <see cref="EventArgs{T}"/></returns>
  55. void OnTextChanged (EventArgs<string> oldValue);
  56. /// <summary>
  57. /// Changed event, raised when the text has changed.
  58. /// <remarks>
  59. /// This event is raised when the <see cref="Text"/> changes. The passed <see cref="EventArgs"/> is a
  60. /// <see cref="string"/> containing the old value.
  61. /// </remarks>
  62. /// </summary>
  63. event EventHandler<EventArgs<string>> TextChanged;
  64. }
  65. //////////////////////////////////////////////////////////////////////////////
  66. // PROVIDERS
  67. //////////////////////////////////////////////////////////////////////////////
  68. #region NetMaskedTextProvider
  69. /// <summary>
  70. /// .Net MaskedTextProvider Provider for TextValidateField.
  71. /// <para></para>
  72. /// <para>
  73. /// <a
  74. /// href="https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.maskedtextprovider?view=net-5.0">
  75. /// Wrapper around MaskedTextProvider
  76. /// </a>
  77. /// </para>
  78. /// <para>
  79. /// <a
  80. /// href="https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.maskedtextbox.mask?view=net-5.0">
  81. /// Masking elements
  82. /// </a>
  83. /// </para>
  84. /// </summary>
  85. public class NetMaskedTextProvider : ITextValidateProvider
  86. {
  87. private MaskedTextProvider _provider;
  88. /// <summary>Empty Constructor</summary>
  89. public NetMaskedTextProvider (string mask) { Mask = mask; }
  90. /// <summary>Mask property</summary>
  91. public string Mask
  92. {
  93. get => _provider?.Mask;
  94. set
  95. {
  96. string current = _provider != null
  97. ? _provider.ToString (false, false)
  98. : string.Empty;
  99. _provider = new MaskedTextProvider (value == string.Empty ? "&&&&&&" : value);
  100. if (!string.IsNullOrEmpty (current))
  101. {
  102. _provider.Set (current);
  103. }
  104. }
  105. }
  106. /// <inheritdoc/>
  107. public event EventHandler<EventArgs<string>> TextChanged;
  108. /// <inheritdoc/>
  109. public string Text
  110. {
  111. get => _provider.ToString ();
  112. set => _provider.Set (value);
  113. }
  114. /// <inheritdoc/>
  115. public bool IsValid => _provider.MaskCompleted;
  116. /// <inheritdoc/>
  117. public bool Fixed => true;
  118. /// <inheritdoc/>
  119. public string DisplayText => _provider.ToDisplayString ();
  120. /// <inheritdoc/>
  121. public int Cursor (int pos)
  122. {
  123. if (pos < 0)
  124. {
  125. return CursorStart ();
  126. }
  127. if (pos > _provider.Length)
  128. {
  129. return CursorEnd ();
  130. }
  131. int p = _provider.FindEditPositionFrom (pos, false);
  132. if (p == -1)
  133. {
  134. p = _provider.FindEditPositionFrom (pos, true);
  135. }
  136. return p;
  137. }
  138. /// <inheritdoc/>
  139. public int CursorStart ()
  140. {
  141. return _provider.IsEditPosition (0)
  142. ? 0
  143. : _provider.FindEditPositionFrom (0, true);
  144. }
  145. /// <inheritdoc/>
  146. public int CursorEnd ()
  147. {
  148. return _provider.IsEditPosition (_provider.Length - 1)
  149. ? _provider.Length - 1
  150. : _provider.FindEditPositionFrom (_provider.Length, false);
  151. }
  152. /// <inheritdoc/>
  153. public int CursorLeft (int pos)
  154. {
  155. int c = _provider.FindEditPositionFrom (pos - 1, false);
  156. return c == -1 ? pos : c;
  157. }
  158. /// <inheritdoc/>
  159. public int CursorRight (int pos)
  160. {
  161. int c = _provider.FindEditPositionFrom (pos + 1, true);
  162. return c == -1 ? pos : c;
  163. }
  164. /// <inheritdoc/>
  165. public bool Delete (int pos)
  166. {
  167. string oldValue = Text;
  168. bool result = _provider.Replace (' ', pos); // .RemoveAt (pos);
  169. if (result)
  170. {
  171. OnTextChanged (new EventArgs<string> (in oldValue));
  172. }
  173. return result;
  174. }
  175. /// <inheritdoc/>
  176. public bool InsertAt (char ch, int pos)
  177. {
  178. string oldValue = Text;
  179. bool result = _provider.Replace (ch, pos);
  180. if (result)
  181. {
  182. OnTextChanged (new EventArgs<string> (in oldValue));
  183. }
  184. return result;
  185. }
  186. /// <inheritdoc/>
  187. public void OnTextChanged (EventArgs<string> args) { TextChanged?.Invoke (this, args); }
  188. }
  189. #endregion
  190. #region TextRegexProvider
  191. /// <summary>Regex Provider for TextValidateField.</summary>
  192. public class TextRegexProvider : ITextValidateProvider
  193. {
  194. private List<Rune> _pattern;
  195. private Regex _regex;
  196. private List<Rune> _text;
  197. /// <summary>Empty Constructor.</summary>
  198. public TextRegexProvider (string pattern) { Pattern = pattern; }
  199. /// <summary>Regex pattern property.</summary>
  200. public string Pattern
  201. {
  202. get => StringExtensions.ToString (_pattern);
  203. set
  204. {
  205. _pattern = value.ToRuneList ();
  206. CompileMask ();
  207. SetupText ();
  208. }
  209. }
  210. /// <summary>When true, validates with the regex pattern on each input, preventing the input if it's not valid.</summary>
  211. public bool ValidateOnInput { get; set; } = true;
  212. /// <inheritdoc/>
  213. public event EventHandler<EventArgs<string>> TextChanged;
  214. /// <inheritdoc/>
  215. public string Text
  216. {
  217. get => StringExtensions.ToString (_text);
  218. set
  219. {
  220. _text = value != string.Empty ? value.ToRuneList () : null;
  221. SetupText ();
  222. }
  223. }
  224. /// <inheritdoc/>
  225. public string DisplayText => Text;
  226. /// <inheritdoc/>
  227. public bool IsValid => Validate (_text);
  228. /// <inheritdoc/>
  229. public bool Fixed => false;
  230. /// <inheritdoc/>
  231. public int Cursor (int pos)
  232. {
  233. if (pos < 0)
  234. {
  235. return CursorStart ();
  236. }
  237. if (pos >= _text.Count)
  238. {
  239. return CursorEnd ();
  240. }
  241. return pos;
  242. }
  243. /// <inheritdoc/>
  244. public int CursorStart () { return 0; }
  245. /// <inheritdoc/>
  246. public int CursorEnd () { return _text.Count; }
  247. /// <inheritdoc/>
  248. public int CursorLeft (int pos)
  249. {
  250. if (pos > 0)
  251. {
  252. return pos - 1;
  253. }
  254. return pos;
  255. }
  256. /// <inheritdoc/>
  257. public int CursorRight (int pos)
  258. {
  259. if (pos < _text.Count)
  260. {
  261. return pos + 1;
  262. }
  263. return pos;
  264. }
  265. /// <inheritdoc/>
  266. public bool Delete (int pos)
  267. {
  268. if (_text.Count > 0 && pos < _text.Count)
  269. {
  270. string oldValue = Text;
  271. _text.RemoveAt (pos);
  272. OnTextChanged (new EventArgs<string> (in oldValue));
  273. }
  274. return true;
  275. }
  276. /// <inheritdoc/>
  277. public bool InsertAt (char ch, int pos)
  278. {
  279. List<Rune> aux = _text.ToList ();
  280. aux.Insert (pos, (Rune)ch);
  281. if (Validate (aux) || ValidateOnInput == false)
  282. {
  283. string oldValue = Text;
  284. _text.Insert (pos, (Rune)ch);
  285. OnTextChanged (new EventArgs<string> (in oldValue));
  286. return true;
  287. }
  288. return false;
  289. }
  290. /// <inheritdoc/>
  291. public void OnTextChanged (EventArgs<string> args) { TextChanged?.Invoke (this, args); }
  292. /// <summary>Compiles the regex pattern for validation./></summary>
  293. private void CompileMask () { _regex = new Regex (StringExtensions.ToString (_pattern), RegexOptions.Compiled); }
  294. private void SetupText ()
  295. {
  296. if (_text is { } && IsValid)
  297. {
  298. return;
  299. }
  300. _text = new List<Rune> ();
  301. }
  302. private bool Validate (List<Rune> text)
  303. {
  304. Match match = _regex.Match (StringExtensions.ToString (text));
  305. return match.Success;
  306. }
  307. }
  308. #endregion
  309. }
  310. /// <summary>Text field that validates input through a <see cref="ITextValidateProvider"/></summary>
  311. public class TextValidateField : View
  312. {
  313. private readonly int _defaultLength = 10;
  314. private int _cursorPosition;
  315. private ITextValidateProvider _provider;
  316. /// <summary>
  317. /// Initializes a new instance of the <see cref="TextValidateField"/> class.
  318. /// </summary>
  319. public TextValidateField ()
  320. {
  321. Height = Dim.Auto (minimumContentDim: 1);
  322. CanFocus = true;
  323. // Things this view knows how to do
  324. AddCommand (
  325. Command.LeftStart,
  326. () =>
  327. {
  328. HomeKeyHandler ();
  329. return true;
  330. }
  331. );
  332. AddCommand (
  333. Command.RightEnd,
  334. () =>
  335. {
  336. EndKeyHandler ();
  337. return true;
  338. }
  339. );
  340. AddCommand (
  341. Command.DeleteCharRight,
  342. () =>
  343. {
  344. DeleteKeyHandler ();
  345. return true;
  346. }
  347. );
  348. AddCommand (
  349. Command.DeleteCharLeft,
  350. () =>
  351. {
  352. BackspaceKeyHandler ();
  353. return true;
  354. }
  355. );
  356. AddCommand (
  357. Command.Left,
  358. () =>
  359. {
  360. CursorLeft ();
  361. return true;
  362. }
  363. );
  364. AddCommand (
  365. Command.Right,
  366. () =>
  367. {
  368. CursorRight ();
  369. return true;
  370. }
  371. );
  372. // Default keybindings for this view
  373. KeyBindings.Add (Key.Home, Command.LeftStart);
  374. KeyBindings.Add (Key.End, Command.RightEnd);
  375. KeyBindings.Add (Key.Delete, Command.DeleteCharRight);
  376. KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft);
  377. KeyBindings.Add (Key.CursorLeft, Command.Left);
  378. KeyBindings.Add (Key.CursorRight, Command.Right);
  379. }
  380. /// <summary>This property returns true if the input is valid.</summary>
  381. public virtual bool IsValid
  382. {
  383. get
  384. {
  385. if (_provider is null)
  386. {
  387. return false;
  388. }
  389. return _provider.IsValid;
  390. }
  391. }
  392. /// <summary>Provider</summary>
  393. public ITextValidateProvider Provider
  394. {
  395. get => _provider;
  396. set
  397. {
  398. _provider = value;
  399. if (_provider.Fixed)
  400. {
  401. Width = _provider.DisplayText == string.Empty
  402. ? _defaultLength
  403. : _provider.DisplayText.Length;
  404. }
  405. // HomeKeyHandler already call SetNeedsDisplay
  406. HomeKeyHandler ();
  407. }
  408. }
  409. /// <summary>Text</summary>
  410. public new string Text
  411. {
  412. get
  413. {
  414. if (_provider is null)
  415. {
  416. return string.Empty;
  417. }
  418. return _provider.Text;
  419. }
  420. set
  421. {
  422. if (_provider is null)
  423. {
  424. return;
  425. }
  426. _provider.Text = value;
  427. SetNeedsDisplay ();
  428. }
  429. }
  430. /// <inheritdoc/>
  431. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  432. {
  433. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  434. {
  435. int c = _provider.Cursor (mouseEvent.Position.X - GetMargins (Viewport.Width).left);
  436. if (_provider.Fixed == false && TextAlignment == Alignment.End && Text.Length > 0)
  437. {
  438. c++;
  439. }
  440. _cursorPosition = c;
  441. SetFocus ();
  442. SetNeedsDisplay ();
  443. return true;
  444. }
  445. return false;
  446. }
  447. /// <inheritdoc/>
  448. protected override bool OnDrawingContent (Rectangle viewport)
  449. {
  450. if (_provider is null)
  451. {
  452. Move (0, 0);
  453. Driver?.AddStr ("Error: ITextValidateProvider not set!");
  454. return true;
  455. }
  456. Color bgcolor = !IsValid ? new Color (Color.BrightRed) : ColorScheme.Focus.Background;
  457. var textColor = new Attribute (ColorScheme.Focus.Foreground, bgcolor);
  458. (int margin_left, int margin_right) = GetMargins (Viewport.Width);
  459. Move (0, 0);
  460. // Left Margin
  461. Driver?.SetAttribute (textColor);
  462. for (var i = 0; i < margin_left; i++)
  463. {
  464. Driver?.AddRune ((Rune)' ');
  465. }
  466. // Content
  467. Driver?.SetAttribute (textColor);
  468. // Content
  469. for (var i = 0; i < _provider.DisplayText.Length; i++)
  470. {
  471. Driver?.AddRune ((Rune)_provider.DisplayText [i]);
  472. }
  473. // Right Margin
  474. Driver?.SetAttribute (textColor);
  475. for (var i = 0; i < margin_right; i++)
  476. {
  477. Driver?.AddRune ((Rune)' ');
  478. }
  479. return true;
  480. }
  481. /// <inheritdoc/>
  482. protected override bool OnKeyDownNotHandled (Key a)
  483. {
  484. if (_provider is null)
  485. {
  486. return false;
  487. }
  488. if (a.AsRune == default (Rune))
  489. {
  490. return false;
  491. }
  492. Rune key = a.AsRune;
  493. bool inserted = _provider.InsertAt ((char)key.Value, _cursorPosition);
  494. if (inserted)
  495. {
  496. CursorRight ();
  497. }
  498. return false;
  499. }
  500. /// <inheritdoc/>
  501. public override Point? PositionCursor ()
  502. {
  503. (int left, _) = GetMargins (Viewport.Width);
  504. // Fixed = true, is for inputs that have fixed width, like masked ones.
  505. // Fixed = false, is for normal input.
  506. // When it's right-aligned and it's a normal input, the cursor behaves differently.
  507. int curPos;
  508. if (_provider?.Fixed == false && TextAlignment == Alignment.End)
  509. {
  510. curPos = _cursorPosition + left - 1;
  511. }
  512. else
  513. {
  514. curPos = _cursorPosition + left;
  515. }
  516. Move (curPos, 0);
  517. return new (curPos, 0);
  518. }
  519. /// <summary>Delete char at cursor position - 1, moving the cursor.</summary>
  520. /// <returns></returns>
  521. private bool BackspaceKeyHandler ()
  522. {
  523. if (_provider.Fixed == false && TextAlignment == Alignment.End && _cursorPosition <= 1)
  524. {
  525. return false;
  526. }
  527. _cursorPosition = _provider.CursorLeft (_cursorPosition);
  528. _provider.Delete (_cursorPosition);
  529. SetNeedsDisplay ();
  530. return true;
  531. }
  532. /// <summary>Try to move the cursor to the left.</summary>
  533. /// <returns>True if moved.</returns>
  534. private bool CursorLeft ()
  535. {
  536. if (_provider is null)
  537. {
  538. return false;
  539. }
  540. int current = _cursorPosition;
  541. _cursorPosition = _provider.CursorLeft (_cursorPosition);
  542. SetNeedsDisplay ();
  543. return current != _cursorPosition;
  544. }
  545. /// <summary>Try to move the cursor to the right.</summary>
  546. /// <returns>True if moved.</returns>
  547. private bool CursorRight ()
  548. {
  549. if (_provider is null)
  550. {
  551. return false;
  552. }
  553. int current = _cursorPosition;
  554. _cursorPosition = _provider.CursorRight (_cursorPosition);
  555. SetNeedsDisplay ();
  556. return current != _cursorPosition;
  557. }
  558. /// <summary>Deletes char at current position.</summary>
  559. /// <returns></returns>
  560. private bool DeleteKeyHandler ()
  561. {
  562. if (_provider.Fixed == false && TextAlignment == Alignment.End)
  563. {
  564. _cursorPosition = _provider.CursorLeft (_cursorPosition);
  565. }
  566. _provider.Delete (_cursorPosition);
  567. SetNeedsDisplay ();
  568. return true;
  569. }
  570. /// <summary>Moves the cursor to the last char.</summary>
  571. /// <returns></returns>
  572. private bool EndKeyHandler ()
  573. {
  574. _cursorPosition = _provider.CursorEnd ();
  575. SetNeedsDisplay ();
  576. return true;
  577. }
  578. /// <summary>Margins for text alignment.</summary>
  579. /// <param name="width">Total width</param>
  580. /// <returns>Left and right margins</returns>
  581. private (int left, int right) GetMargins (int width)
  582. {
  583. int count = Text.Length;
  584. int total = width - count;
  585. switch (TextAlignment)
  586. {
  587. case Alignment.Start:
  588. return (0, total);
  589. case Alignment.Center:
  590. return (total / 2, total / 2 + total % 2);
  591. case Alignment.End:
  592. return (total, 0);
  593. default:
  594. return (0, total);
  595. }
  596. }
  597. /// <summary>Moves the cursor to first char.</summary>
  598. /// <returns></returns>
  599. private bool HomeKeyHandler ()
  600. {
  601. _cursorPosition = _provider.CursorStart ();
  602. SetNeedsDisplay ();
  603. return true;
  604. }
  605. }
  606. }