DateField.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. #nullable enable
  2. //
  3. // DateField.cs: text entry for date
  4. //
  5. // Author: Barry Nolte
  6. //
  7. // Licensed under the MIT license
  8. //
  9. using System.Globalization;
  10. namespace Terminal.Gui.Views;
  11. /// <summary>Provides date editing functionality with mouse support.</summary>
  12. public class DateField : TextField
  13. {
  14. private const string RIGHT_TO_LEFT_MARK = "\u200f";
  15. private readonly int _dateFieldLength = 12;
  16. private DateTime? _date;
  17. private string? _format;
  18. private string? _separator;
  19. /// <summary>Initializes a new instance of <see cref="DateField"/>.</summary>
  20. public DateField () : this (DateTime.MinValue) { }
  21. /// <summary>Initializes a new instance of <see cref="DateField"/>.</summary>
  22. /// <param name="date"></param>
  23. public DateField (DateTime date)
  24. {
  25. Width = _dateFieldLength;
  26. SetInitialProperties (date);
  27. }
  28. private CultureInfo _culture = CultureInfo.CurrentCulture;
  29. /// <summary>CultureInfo for date. The default is CultureInfo.CurrentCulture.</summary>
  30. public CultureInfo? Culture
  31. {
  32. get => _culture;
  33. set
  34. {
  35. _culture = value ?? CultureInfo.CurrentCulture;
  36. _separator = GetDataSeparator (_culture.DateTimeFormat.DateSeparator);
  37. _format = " " + StandardizeDateFormat (_culture.DateTimeFormat.ShortDatePattern);
  38. Text = Date?.ToString (_format).Replace (RIGHT_TO_LEFT_MARK, "");
  39. }
  40. }
  41. /// <inheritdoc/>
  42. public override int CursorPosition
  43. {
  44. get => base.CursorPosition;
  45. set => base.CursorPosition = Math.Max (Math.Min (value, FormatLength), 1);
  46. }
  47. /// <summary>Gets or sets the date of the <see cref="DateField"/>.</summary>
  48. /// <remarks></remarks>
  49. public DateTime? Date
  50. {
  51. get => _date;
  52. set
  53. {
  54. if (ReadOnly)
  55. {
  56. return;
  57. }
  58. DateTime? oldData = _date;
  59. _date = value;
  60. if (_format is { })
  61. {
  62. Text = value?.ToString (" " + StandardizeDateFormat (_format.Trim ()))
  63. .Replace (RIGHT_TO_LEFT_MARK, "");
  64. EventArgs<DateTime> args = new (value!.Value);
  65. if (oldData != value)
  66. {
  67. OnDateChanged (args);
  68. DateChanged?.Invoke (this, args);
  69. }
  70. }
  71. }
  72. }
  73. private int FormatLength => StandardizeDateFormat (_format).Trim ().Length;
  74. /// <summary>DateChanged event, raised when the <see cref="Date"/> property has changed.</summary>
  75. /// <remarks>This event is raised when the <see cref="Date"/> property changes.</remarks>
  76. /// <remarks>The passed event arguments containing the old value, new value, and format string.</remarks>
  77. public event EventHandler<EventArgs<DateTime>>? DateChanged;
  78. /// <inheritdoc/>
  79. public override void DeleteCharLeft (bool useOldCursorPos = true)
  80. {
  81. if (ReadOnly)
  82. {
  83. return;
  84. }
  85. ClearAllSelection ();
  86. SetText ((Rune)'0');
  87. DecCursorPosition ();
  88. }
  89. /// <inheritdoc/>
  90. public override void DeleteCharRight ()
  91. {
  92. if (ReadOnly)
  93. {
  94. return;
  95. }
  96. ClearAllSelection ();
  97. SetText ((Rune)'0');
  98. }
  99. /// <inheritdoc/>
  100. protected override bool OnMouseEvent (MouseEventArgs ev)
  101. {
  102. if (base.OnMouseEvent (ev) || ev.Handled)
  103. {
  104. return true;
  105. }
  106. if (SelectedLength == 0 && ev.Flags.HasFlag (MouseFlags.Button1Pressed))
  107. {
  108. AdjCursorPosition (ev.Position.X);
  109. }
  110. return ev.Handled;
  111. }
  112. /// <summary>Event firing method for the <see cref="DateChanged"/> event.</summary>
  113. /// <param name="args">Event arguments</param>
  114. protected virtual void OnDateChanged (EventArgs<DateTime> args) { }
  115. /// <inheritdoc/>
  116. protected override bool OnKeyDownNotHandled (Key a)
  117. {
  118. // Ignore non-numeric characters.
  119. if (a >= Key.D0 && a <= Key.D9)
  120. {
  121. if (!ReadOnly)
  122. {
  123. if (SetText ((Rune)a))
  124. {
  125. IncCursorPosition ();
  126. }
  127. }
  128. return true;
  129. }
  130. return false;
  131. }
  132. private void AdjCursorPosition (int point, bool increment = true)
  133. {
  134. int newPoint = point;
  135. if (point > FormatLength)
  136. {
  137. newPoint = FormatLength;
  138. }
  139. if (point < 1)
  140. {
  141. newPoint = 1;
  142. }
  143. if (newPoint != point)
  144. {
  145. CursorPosition = newPoint;
  146. }
  147. while (CursorPosition < Text.GetColumns () - 1 && Text [CursorPosition].ToString () == _separator)
  148. {
  149. if (increment)
  150. {
  151. CursorPosition++;
  152. }
  153. else
  154. {
  155. CursorPosition--;
  156. }
  157. }
  158. }
  159. private void OnTextChanging (object? sender, ResultEventArgs<string> e)
  160. {
  161. if (e.Result is null)
  162. {
  163. return;
  164. }
  165. try
  166. {
  167. var spaces = 0;
  168. for (var i = 0; i < e.Result.Length; i++)
  169. {
  170. if (e.Result [i] == ' ')
  171. {
  172. spaces++;
  173. }
  174. else
  175. {
  176. break;
  177. }
  178. }
  179. spaces += FormatLength;
  180. string trimmedText = e.Result [..spaces];
  181. spaces -= FormatLength;
  182. trimmedText = trimmedText.Replace (new (' ', spaces), " ");
  183. var date = Convert.ToDateTime (trimmedText).ToString (_format!.Trim ());
  184. if ($" {date}" != e.Result)
  185. {
  186. // Change the date format to match the current culture
  187. e.Result = $" {date}".Replace (RIGHT_TO_LEFT_MARK, "");
  188. }
  189. AdjCursorPosition (CursorPosition);
  190. }
  191. catch (Exception)
  192. {
  193. e.Handled = true;
  194. }
  195. }
  196. private void DecCursorPosition ()
  197. {
  198. if (CursorPosition <= 1)
  199. {
  200. CursorPosition = 1;
  201. return;
  202. }
  203. CursorPosition--;
  204. AdjCursorPosition (CursorPosition, false);
  205. }
  206. private string GetDataSeparator (string separator)
  207. {
  208. string sepChar = separator.Trim ();
  209. if (sepChar.Length > 1 && sepChar.Contains (RIGHT_TO_LEFT_MARK))
  210. {
  211. sepChar = sepChar.Replace (RIGHT_TO_LEFT_MARK, "");
  212. }
  213. return sepChar;
  214. }
  215. private string GetDate (int month, int day, int year, string [] fm)
  216. {
  217. var date = " ";
  218. for (var i = 0; i < fm.Length; i++)
  219. {
  220. if (fm [i].Contains ('M'))
  221. {
  222. date += $"{month,2:00}";
  223. }
  224. else if (fm [i].Contains ('d'))
  225. {
  226. date += $"{day,2:00}";
  227. }
  228. else
  229. {
  230. date += $"{year,4:0000}";
  231. }
  232. if (i < 2)
  233. {
  234. date += $"{_separator}";
  235. }
  236. }
  237. return date;
  238. }
  239. private static int GetFormatIndex (string [] fm, string t)
  240. {
  241. int idx = -1;
  242. for (var i = 0; i < fm.Length; i++)
  243. {
  244. if (fm [i].Contains (t))
  245. {
  246. idx = i;
  247. break;
  248. }
  249. }
  250. return idx;
  251. }
  252. private void IncCursorPosition ()
  253. {
  254. if (CursorPosition >= FormatLength)
  255. {
  256. CursorPosition = FormatLength;
  257. return;
  258. }
  259. CursorPosition++;
  260. AdjCursorPosition (CursorPosition);
  261. }
  262. private new bool MoveEnd ()
  263. {
  264. ClearAllSelection ();
  265. CursorPosition = FormatLength;
  266. return true;
  267. }
  268. private bool MoveHome ()
  269. {
  270. // Home, C-A
  271. ClearAllSelection ();
  272. CursorPosition = 1;
  273. return true;
  274. }
  275. private bool MoveLeft ()
  276. {
  277. ClearAllSelection ();
  278. DecCursorPosition ();
  279. return true;
  280. }
  281. private bool MoveRight ()
  282. {
  283. ClearAllSelection ();
  284. IncCursorPosition ();
  285. return true;
  286. }
  287. private string NormalizeFormat (string text, string? fmt = null, string? sepChar = null)
  288. {
  289. if (string.IsNullOrEmpty (fmt))
  290. {
  291. fmt = _format;
  292. }
  293. if (string.IsNullOrEmpty (sepChar))
  294. {
  295. sepChar = _separator;
  296. }
  297. if (fmt is null || fmt.Length != text.Length)
  298. {
  299. return text;
  300. }
  301. char [] fmtText = text.ToCharArray ();
  302. for (var i = 0; i < text.Length; i++)
  303. {
  304. char c = fmt [i];
  305. if (c.ToString () == sepChar && text [i].ToString () != sepChar)
  306. {
  307. fmtText [i] = c;
  308. }
  309. }
  310. return new (fmtText);
  311. }
  312. private void SetInitialProperties (DateTime date)
  313. {
  314. _format = $" {StandardizeDateFormat (Culture!.DateTimeFormat.ShortDatePattern)}";
  315. _separator = GetDataSeparator (Culture.DateTimeFormat.DateSeparator);
  316. Date = date;
  317. CursorPosition = 1;
  318. TextChanging += OnTextChanging;
  319. // Things this view knows how to do
  320. AddCommand (
  321. Command.DeleteCharRight,
  322. () =>
  323. {
  324. DeleteCharRight ();
  325. return true;
  326. }
  327. );
  328. AddCommand (
  329. Command.DeleteCharLeft,
  330. () =>
  331. {
  332. DeleteCharLeft (false);
  333. return true;
  334. }
  335. );
  336. AddCommand (Command.LeftStart, () => MoveHome ());
  337. AddCommand (Command.Left, () => MoveLeft ());
  338. AddCommand (Command.RightEnd, () => MoveEnd ());
  339. AddCommand (Command.Right, () => MoveRight ());
  340. // Replace the commands defined in TextField
  341. KeyBindings.ReplaceCommands (Key.Delete, Command.DeleteCharRight);
  342. KeyBindings.ReplaceCommands (Key.D.WithCtrl, Command.DeleteCharRight);
  343. KeyBindings.ReplaceCommands (Key.Backspace, Command.DeleteCharLeft);
  344. KeyBindings.ReplaceCommands (Key.Home, Command.LeftStart);
  345. KeyBindings.ReplaceCommands (Key.Home.WithCtrl, Command.LeftStart);
  346. KeyBindings.ReplaceCommands (Key.CursorLeft, Command.Left);
  347. KeyBindings.ReplaceCommands (Key.B.WithCtrl, Command.Left);
  348. KeyBindings.ReplaceCommands (Key.End, Command.RightEnd);
  349. KeyBindings.ReplaceCommands (Key.E.WithCtrl, Command.RightEnd);
  350. KeyBindings.ReplaceCommands (Key.CursorRight, Command.Right);
  351. KeyBindings.ReplaceCommands (Key.F.WithCtrl, Command.Right);
  352. #if UNIX_KEY_BINDINGS
  353. KeyBindings.ReplaceCommands (Key.D.WithAlt, Command.DeleteCharLeft);
  354. #endif
  355. }
  356. private bool SetText (Rune key)
  357. {
  358. if (CursorPosition > FormatLength)
  359. {
  360. CursorPosition = FormatLength;
  361. return false;
  362. }
  363. if (CursorPosition < 1)
  364. {
  365. CursorPosition = 1;
  366. return false;
  367. }
  368. List<Rune> text = Text.EnumerateRunes ().ToList ();
  369. List<Rune> newText = text.GetRange (0, CursorPosition);
  370. newText.Add (key);
  371. if (CursorPosition < FormatLength)
  372. {
  373. newText =
  374. [
  375. .. newText,
  376. .. text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))
  377. ];
  378. }
  379. return SetText (StringExtensions.ToString (newText));
  380. }
  381. private bool SetText (string text)
  382. {
  383. if (string.IsNullOrEmpty (text))
  384. {
  385. return false;
  386. }
  387. text = NormalizeFormat (text);
  388. string [] vals = text.Split (_separator);
  389. for (var i = 0; i < vals.Length; i++)
  390. {
  391. if (vals [i].Contains (RIGHT_TO_LEFT_MARK))
  392. {
  393. vals [i] = vals [i].Replace (RIGHT_TO_LEFT_MARK, "");
  394. }
  395. }
  396. string [] frm = _format!.Split (_separator);
  397. int year;
  398. int month;
  399. int day;
  400. int idx = GetFormatIndex (frm, "y");
  401. if (int.Parse (vals [idx]) < 1)
  402. {
  403. year = 1;
  404. vals [idx] = "1";
  405. }
  406. else
  407. {
  408. year = int.Parse (vals [idx]);
  409. }
  410. idx = GetFormatIndex (frm, "M");
  411. if (int.Parse (vals [idx]) < 1)
  412. {
  413. month = 1;
  414. vals [idx] = "1";
  415. }
  416. else if (int.Parse (vals [idx]) > 12)
  417. {
  418. month = 12;
  419. vals [idx] = "12";
  420. }
  421. else
  422. {
  423. month = int.Parse (vals [idx]);
  424. }
  425. idx = GetFormatIndex (frm, "d");
  426. if (int.Parse (vals [idx]) < 1)
  427. {
  428. day = 1;
  429. vals [idx] = "1";
  430. }
  431. else if (int.Parse (vals [idx]) > 31)
  432. {
  433. day = DateTime.DaysInMonth (year, month);
  434. vals [idx] = day.ToString ();
  435. }
  436. else
  437. {
  438. day = int.Parse (vals [idx]);
  439. }
  440. string d = GetDate (month, day, year, frm);
  441. DateTime date;
  442. try
  443. {
  444. date = Convert.ToDateTime (d);
  445. }
  446. catch (Exception)
  447. {
  448. return false;
  449. }
  450. Date = date;
  451. return true;
  452. }
  453. // Converts various date formats to a uniform 10-character format.
  454. // This aids in simplifying the handling of single-digit months and days,
  455. // and reduces the number of distinct date formats to maintain.
  456. private static string StandardizeDateFormat (string? format)
  457. {
  458. return format switch
  459. {
  460. "MM/dd/yyyy" => "MM/dd/yyyy",
  461. "yyyy-MM-dd" => "yyyy-MM-dd",
  462. "yyyy/MM/dd" => "yyyy/MM/dd",
  463. "dd/MM/yyyy" => "dd/MM/yyyy",
  464. "d?/M?/yyyy" => "dd/MM/yyyy",
  465. "dd.MM.yyyy" => "dd.MM.yyyy",
  466. "dd-MM-yyyy" => "dd-MM-yyyy",
  467. "dd/MM yyyy" => "dd/MM/yyyy",
  468. "d. M. yyyy" => "dd.MM.yyyy",
  469. "yyyy.MM.dd" => "yyyy.MM.dd",
  470. "g yyyy/M/d" => "yyyy/MM/dd",
  471. "d/M/yyyy" => "dd/MM/yyyy",
  472. "d?/M?/yyyy g" => "dd/MM/yyyy",
  473. "d-M-yyyy" => "dd-MM-yyyy",
  474. "d.MM.yyyy" => "dd.MM.yyyy",
  475. "d.MM.yyyy '?'." => "dd.MM.yyyy",
  476. "M/d/yyyy" => "MM/dd/yyyy",
  477. "d. M. yyyy." => "dd.MM.yyyy",
  478. "d.M.yyyy." => "dd.MM.yyyy",
  479. "g yyyy-MM-dd" => "yyyy-MM-dd",
  480. "d.M.yyyy" => "dd.MM.yyyy",
  481. "d/MM/yyyy" => "dd/MM/yyyy",
  482. "yyyy/M/d" => "yyyy/MM/dd",
  483. "dd. MM. yyyy." => "dd.MM.yyyy",
  484. "yyyy. MM. dd." => "yyyy.MM.dd",
  485. "yyyy. M. d." => "yyyy.MM.dd",
  486. "d. MM. yyyy" => "dd.MM.yyyy",
  487. _ => "dd/MM/yyyy"
  488. };
  489. }
  490. }