TextValidateField.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  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="StringEventArgs"/></returns>
  55. void OnTextChanged (StringEventArgs 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<StringEventArgs> 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<StringEventArgs> 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 StringEventArgs { NewValue = 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 StringEventArgs { NewValue = oldValue });
  183. }
  184. return result;
  185. }
  186. /// <inheritdoc/>
  187. public void OnTextChanged (StringEventArgs oldValue) { TextChanged?.Invoke (this, oldValue); }
  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<StringEventArgs> 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 StringEventArgs { NewValue = Text, OldValue = 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 StringEventArgs { NewValue = Text, OldValue = oldValue });
  286. return true;
  287. }
  288. return false;
  289. }
  290. /// <inheritdoc/>
  291. public void OnTextChanged (StringEventArgs oldValue) { TextChanged?.Invoke (this, oldValue); }
  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 using
  318. /// <see cref="LayoutStyle.Computed"/> positioning.
  319. /// </summary>
  320. public TextValidateField ()
  321. {
  322. Height = Dim.Auto (minimumContentDim: 1);
  323. CanFocus = true;
  324. // Things this view knows how to do
  325. AddCommand (
  326. Command.LeftHome,
  327. () =>
  328. {
  329. HomeKeyHandler ();
  330. return true;
  331. }
  332. );
  333. AddCommand (
  334. Command.RightEnd,
  335. () =>
  336. {
  337. EndKeyHandler ();
  338. return true;
  339. }
  340. );
  341. AddCommand (
  342. Command.DeleteCharRight,
  343. () =>
  344. {
  345. DeleteKeyHandler ();
  346. return true;
  347. }
  348. );
  349. AddCommand (
  350. Command.DeleteCharLeft,
  351. () =>
  352. {
  353. BackspaceKeyHandler ();
  354. return true;
  355. }
  356. );
  357. AddCommand (
  358. Command.Left,
  359. () =>
  360. {
  361. CursorLeft ();
  362. return true;
  363. }
  364. );
  365. AddCommand (
  366. Command.Right,
  367. () =>
  368. {
  369. CursorRight ();
  370. return true;
  371. }
  372. );
  373. // Default keybindings for this view
  374. KeyBindings.Add (Key.Home, Command.LeftHome);
  375. KeyBindings.Add (Key.End, Command.RightEnd);
  376. KeyBindings.Add (Key.Delete, Command.DeleteCharRight);
  377. KeyBindings.Add (Key.Delete, Command.DeleteCharRight);
  378. KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft);
  379. KeyBindings.Add (Key.CursorLeft, Command.Left);
  380. KeyBindings.Add (Key.CursorRight, Command.Right);
  381. }
  382. /// <summary>This property returns true if the input is valid.</summary>
  383. public virtual bool IsValid
  384. {
  385. get
  386. {
  387. if (_provider is null)
  388. {
  389. return false;
  390. }
  391. return _provider.IsValid;
  392. }
  393. }
  394. /// <summary>Provider</summary>
  395. public ITextValidateProvider Provider
  396. {
  397. get => _provider;
  398. set
  399. {
  400. _provider = value;
  401. if (_provider.Fixed)
  402. {
  403. Width = _provider.DisplayText == string.Empty
  404. ? _defaultLength
  405. : _provider.DisplayText.Length;
  406. }
  407. // HomeKeyHandler already call SetNeedsDisplay
  408. HomeKeyHandler ();
  409. }
  410. }
  411. /// <summary>Text</summary>
  412. public new string Text
  413. {
  414. get
  415. {
  416. if (_provider is null)
  417. {
  418. return string.Empty;
  419. }
  420. return _provider.Text;
  421. }
  422. set
  423. {
  424. if (_provider is null)
  425. {
  426. return;
  427. }
  428. _provider.Text = value;
  429. SetNeedsDisplay ();
  430. }
  431. }
  432. /// <inheritdoc/>
  433. protected internal override bool OnMouseEvent (MouseEvent mouseEvent)
  434. {
  435. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  436. {
  437. int c = _provider.Cursor (mouseEvent.Position.X - GetMargins (Viewport.Width).left);
  438. if (_provider.Fixed == false && TextAlignment == Alignment.End && Text.Length > 0)
  439. {
  440. c++;
  441. }
  442. _cursorPosition = c;
  443. SetFocus ();
  444. SetNeedsDisplay ();
  445. return true;
  446. }
  447. return false;
  448. }
  449. /// <inheritdoc/>
  450. public override void OnDrawContent (Rectangle viewport)
  451. {
  452. if (_provider is null)
  453. {
  454. Move (0, 0);
  455. Driver.AddStr ("Error: ITextValidateProvider not set!");
  456. return;
  457. }
  458. Color bgcolor = !IsValid ? new Color (Color.BrightRed) : ColorScheme.Focus.Background;
  459. var textColor = new Attribute (ColorScheme.Focus.Foreground, bgcolor);
  460. (int margin_left, int margin_right) = GetMargins (Viewport.Width);
  461. Move (0, 0);
  462. // Left Margin
  463. Driver.SetAttribute (textColor);
  464. for (var i = 0; i < margin_left; i++)
  465. {
  466. Driver.AddRune ((Rune)' ');
  467. }
  468. // Content
  469. Driver.SetAttribute (textColor);
  470. // Content
  471. for (var i = 0; i < _provider.DisplayText.Length; i++)
  472. {
  473. Driver.AddRune ((Rune)_provider.DisplayText [i]);
  474. }
  475. // Right Margin
  476. Driver.SetAttribute (textColor);
  477. for (var i = 0; i < margin_right; i++)
  478. {
  479. Driver.AddRune ((Rune)' ');
  480. }
  481. }
  482. /// <inheritdoc/>
  483. public override bool OnProcessKeyDown (Key a)
  484. {
  485. if (_provider is null)
  486. {
  487. return false;
  488. }
  489. if (a.AsRune == default (Rune))
  490. {
  491. return false;
  492. }
  493. Rune key = a.AsRune;
  494. bool inserted = _provider.InsertAt ((char)key.Value, _cursorPosition);
  495. if (inserted)
  496. {
  497. CursorRight ();
  498. }
  499. return false;
  500. }
  501. /// <inheritdoc/>
  502. public override Point? PositionCursor ()
  503. {
  504. (int left, _) = GetMargins (Viewport.Width);
  505. // Fixed = true, is for inputs that have fixed width, like masked ones.
  506. // Fixed = false, is for normal input.
  507. // When it's right-aligned and it's a normal input, the cursor behaves differently.
  508. int curPos;
  509. if (_provider?.Fixed == false && TextAlignment == Alignment.End)
  510. {
  511. curPos = _cursorPosition + left - 1;
  512. }
  513. else
  514. {
  515. curPos = _cursorPosition + left;
  516. }
  517. Move (curPos, 0);
  518. return new (curPos, 0);
  519. }
  520. /// <summary>Delete char at cursor position - 1, moving the cursor.</summary>
  521. /// <returns></returns>
  522. private bool BackspaceKeyHandler ()
  523. {
  524. if (_provider.Fixed == false && TextAlignment == Alignment.End && _cursorPosition <= 1)
  525. {
  526. return false;
  527. }
  528. _cursorPosition = _provider.CursorLeft (_cursorPosition);
  529. _provider.Delete (_cursorPosition);
  530. SetNeedsDisplay ();
  531. return true;
  532. }
  533. /// <summary>Try to move the cursor to the left.</summary>
  534. /// <returns>True if moved.</returns>
  535. private bool CursorLeft ()
  536. {
  537. int current = _cursorPosition;
  538. _cursorPosition = _provider.CursorLeft (_cursorPosition);
  539. SetNeedsDisplay ();
  540. return current != _cursorPosition;
  541. }
  542. /// <summary>Try to move the cursor to the right.</summary>
  543. /// <returns>True if moved.</returns>
  544. private bool CursorRight ()
  545. {
  546. int current = _cursorPosition;
  547. _cursorPosition = _provider.CursorRight (_cursorPosition);
  548. SetNeedsDisplay ();
  549. return current != _cursorPosition;
  550. }
  551. /// <summary>Deletes char at current position.</summary>
  552. /// <returns></returns>
  553. private bool DeleteKeyHandler ()
  554. {
  555. if (_provider.Fixed == false && TextAlignment == Alignment.End)
  556. {
  557. _cursorPosition = _provider.CursorLeft (_cursorPosition);
  558. }
  559. _provider.Delete (_cursorPosition);
  560. SetNeedsDisplay ();
  561. return true;
  562. }
  563. /// <summary>Moves the cursor to the last char.</summary>
  564. /// <returns></returns>
  565. private bool EndKeyHandler ()
  566. {
  567. _cursorPosition = _provider.CursorEnd ();
  568. SetNeedsDisplay ();
  569. return true;
  570. }
  571. /// <summary>Margins for text alignment.</summary>
  572. /// <param name="width">Total width</param>
  573. /// <returns>Left and right margins</returns>
  574. private (int left, int right) GetMargins (int width)
  575. {
  576. int count = Text.Length;
  577. int total = width - count;
  578. switch (TextAlignment)
  579. {
  580. case Alignment.Start:
  581. return (0, total);
  582. case Alignment.Center:
  583. return (total / 2, total / 2 + total % 2);
  584. case Alignment.End:
  585. return (total, 0);
  586. default:
  587. return (0, total);
  588. }
  589. }
  590. /// <summary>Moves the cursor to first char.</summary>
  591. /// <returns></returns>
  592. private bool HomeKeyHandler ()
  593. {
  594. _cursorPosition = _provider.CursorStart ();
  595. SetNeedsDisplay ();
  596. return true;
  597. }
  598. }
  599. }