DateField.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. //
  2. // DateField.cs: text entry for date
  3. //
  4. // Author: Barry Nolte
  5. //
  6. // Licensed under the MIT license
  7. //
  8. using System;
  9. using System.Globalization;
  10. using System.Linq;
  11. using System.Text;
  12. namespace Terminal.Gui;
  13. /// <summary>
  14. /// Simple Date editing <see cref="View"/>
  15. /// </summary>
  16. /// <remarks>
  17. /// The <see cref="DateField"/> <see cref="View"/> provides date editing functionality with mouse support.
  18. /// </remarks>
  19. public class DateField : TextField {
  20. DateTime _date;
  21. bool _isShort;
  22. int _longFieldLen = 10;
  23. int _shortFieldLen = 8;
  24. string _sepChar;
  25. string _longFormat;
  26. string _shortFormat;
  27. int _fieldLen => _isShort ? _shortFieldLen : _longFieldLen;
  28. string _format => _isShort ? _shortFormat : _longFormat;
  29. /// <summary>
  30. /// DateChanged event, raised when the <see cref="Date"/> property has changed.
  31. /// </summary>
  32. /// <remarks>
  33. /// This event is raised when the <see cref="Date"/> property changes.
  34. /// </remarks>
  35. /// <remarks>
  36. /// The passed event arguments containing the old value, new value, and format string.
  37. /// </remarks>
  38. public event EventHandler<DateTimeEventArgs<DateTime>> DateChanged;
  39. /// <summary>
  40. /// Initializes a new instance of <see cref="DateField"/> using <see cref="LayoutStyle.Absolute"/> layout.
  41. /// </summary>
  42. /// <param name="x">The x coordinate.</param>
  43. /// <param name="y">The y coordinate.</param>
  44. /// <param name="date">Initial date contents.</param>
  45. /// <param name="isShort">If true, shows only two digits for the year.</param>
  46. public DateField (int x, int y, DateTime date, bool isShort = false) : base (x, y, isShort ? 10 : 12, "") => SetInitialProperties (date, isShort);
  47. /// <summary>
  48. /// Initializes a new instance of <see cref="DateField"/> using <see cref="LayoutStyle.Computed"/> layout.
  49. /// </summary>
  50. public DateField () : this (DateTime.MinValue) { }
  51. /// <summary>
  52. /// Initializes a new instance of <see cref="DateField"/> using <see cref="LayoutStyle.Computed"/> layout.
  53. /// </summary>
  54. /// <param name="date"></param>
  55. public DateField (DateTime date) : base ("")
  56. {
  57. Width = _fieldLen + 2;
  58. SetInitialProperties (date);
  59. }
  60. void SetInitialProperties (DateTime date, bool isShort = false)
  61. {
  62. var cultureInfo = CultureInfo.CurrentCulture;
  63. _sepChar = cultureInfo.DateTimeFormat.DateSeparator;
  64. _longFormat = GetLongFormat (cultureInfo.DateTimeFormat.ShortDatePattern);
  65. _shortFormat = GetShortFormat (_longFormat);
  66. this._isShort = isShort;
  67. Date = date;
  68. CursorPosition = 1;
  69. TextChanged += DateField_Changed;
  70. // Things this view knows how to do
  71. AddCommand (Command.DeleteCharRight, () => {
  72. DeleteCharRight ();
  73. return true;
  74. });
  75. AddCommand (Command.DeleteCharLeft, () => {
  76. DeleteCharLeft (false);
  77. return true;
  78. });
  79. AddCommand (Command.LeftHome, () => MoveHome ());
  80. AddCommand (Command.Left, () => MoveLeft ());
  81. AddCommand (Command.RightEnd, () => MoveEnd ());
  82. AddCommand (Command.Right, () => MoveRight ());
  83. // Default keybindings for this view
  84. KeyBindings.Add (KeyCode.Delete, Command.DeleteCharRight);
  85. KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight);
  86. KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft);
  87. KeyBindings.Add (Key.D.WithAlt, Command.DeleteCharLeft);
  88. KeyBindings.Add (Key.Home, Command.LeftHome);
  89. KeyBindings.Add (Key.A.WithCtrl, Command.LeftHome);
  90. KeyBindings.Add (Key.CursorLeft, Command.Left);
  91. KeyBindings.Add (Key.B.WithCtrl, Command.Left);
  92. KeyBindings.Add (Key.End, Command.RightEnd);
  93. KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd);
  94. KeyBindings.Add (Key.CursorRight, Command.Right);
  95. KeyBindings.Add (Key.F.WithCtrl, Command.Right);
  96. }
  97. /// <inheritdoc />
  98. public override bool OnProcessKeyDown (Key a)
  99. {
  100. // Ignore non-numeric characters.
  101. if (a >= Key.D0 && a <= Key.D9) {
  102. if (!ReadOnly) {
  103. if (SetText ((Rune)a)) {
  104. IncCursorPosition ();
  105. }
  106. }
  107. return true;
  108. }
  109. return false;
  110. }
  111. void DateField_Changed (object sender, TextChangedEventArgs e)
  112. {
  113. try {
  114. var date = GetInvarianteDate (Text, _isShort);
  115. if ($" {date}" != Text) {
  116. Text = $" {date}";
  117. }
  118. if (_isShort) {
  119. date = GetInvarianteDate (Text, false);
  120. }
  121. if (!DateTime.TryParseExact (date, GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out var result)) {
  122. Text = e.OldValue;
  123. }
  124. } catch (Exception) {
  125. Text = e.OldValue;
  126. }
  127. }
  128. string GetInvarianteFormat () => $"MM{_sepChar}dd{_sepChar}yyyy";
  129. string GetLongFormat (string lf)
  130. {
  131. string [] frm = lf.Split (_sepChar);
  132. for (int i = 0; i < frm.Length; i++) {
  133. if (frm [i].Contains ("M") && frm [i].GetRuneCount () < 2) {
  134. lf = lf.Replace ("M", "MM");
  135. }
  136. if (frm [i].Contains ("d") && frm [i].GetRuneCount () < 2) {
  137. lf = lf.Replace ("d", "dd");
  138. }
  139. if (frm [i].Contains ("y") && frm [i].GetRuneCount () < 4) {
  140. lf = lf.Replace ("yy", "yyyy");
  141. }
  142. }
  143. return $" {lf}";
  144. }
  145. string GetShortFormat (string lf) => lf.Replace ("yyyy", "yy");
  146. /// <summary>
  147. /// Gets or sets the date of the <see cref="DateField"/>.
  148. /// </summary>
  149. /// <remarks>
  150. /// </remarks>
  151. public DateTime Date {
  152. get => _date;
  153. set {
  154. if (ReadOnly) {
  155. return;
  156. }
  157. var oldData = _date;
  158. _date = value;
  159. Text = value.ToString (_format);
  160. var args = new DateTimeEventArgs<DateTime> (oldData, value, _format);
  161. if (oldData != value) {
  162. OnDateChanged (args);
  163. }
  164. }
  165. }
  166. /// <summary>
  167. /// Get or set the date format for the widget.
  168. /// </summary>
  169. public bool IsShortFormat {
  170. get => _isShort;
  171. set {
  172. _isShort = value;
  173. if (_isShort) {
  174. Width = 10;
  175. } else {
  176. Width = 12;
  177. }
  178. bool ro = ReadOnly;
  179. if (ro) {
  180. ReadOnly = false;
  181. }
  182. SetText (Text);
  183. ReadOnly = ro;
  184. SetNeedsDisplay ();
  185. }
  186. }
  187. /// <inheritdoc/>
  188. public override int CursorPosition {
  189. get => base.CursorPosition;
  190. set => base.CursorPosition = Math.Max (Math.Min (value, _fieldLen), 1);
  191. }
  192. bool SetText (Rune key)
  193. {
  194. if (CursorPosition > _fieldLen) {
  195. CursorPosition = _fieldLen;
  196. return false;
  197. } else if (CursorPosition < 1) {
  198. CursorPosition = 1;
  199. return false;
  200. }
  201. var text = Text.EnumerateRunes ().ToList ();
  202. var newText = text.GetRange (0, CursorPosition);
  203. newText.Add (key);
  204. if (CursorPosition < _fieldLen) {
  205. newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList ();
  206. }
  207. return SetText (StringExtensions.ToString (newText));
  208. }
  209. bool SetText (string text)
  210. {
  211. if (string.IsNullOrEmpty (text)) {
  212. return false;
  213. }
  214. text = NormalizeFormat (text);
  215. string [] vals = text.Split (_sepChar);
  216. string [] frm = _format.Split (_sepChar);
  217. int year;
  218. int month;
  219. int day;
  220. int idx = GetFormatIndex (frm, "y");
  221. if (Int32.Parse (vals [idx]) < 1) {
  222. year = 1;
  223. vals [idx] = "1";
  224. } else {
  225. year = Int32.Parse (vals [idx]);
  226. }
  227. idx = GetFormatIndex (frm, "M");
  228. if (Int32.Parse (vals [idx]) < 1) {
  229. month = 1;
  230. vals [idx] = "1";
  231. } else if (Int32.Parse (vals [idx]) > 12) {
  232. month = 12;
  233. vals [idx] = "12";
  234. } else {
  235. month = Int32.Parse (vals [idx]);
  236. }
  237. idx = GetFormatIndex (frm, "d");
  238. if (Int32.Parse (vals [idx]) < 1) {
  239. day = 1;
  240. vals [idx] = "1";
  241. } else if (Int32.Parse (vals [idx]) > 31) {
  242. day = DateTime.DaysInMonth (year, month);
  243. vals [idx] = day.ToString ();
  244. } else {
  245. day = Int32.Parse (vals [idx]);
  246. }
  247. string d = GetDate (month, day, year, frm);
  248. if (!DateTime.TryParseExact (d, _format, CultureInfo.CurrentCulture, DateTimeStyles.None, out var result)) {
  249. return false;
  250. }
  251. Date = result;
  252. return true;
  253. }
  254. string NormalizeFormat (string text, string fmt = null, string sepChar = null)
  255. {
  256. if (string.IsNullOrEmpty (fmt)) {
  257. fmt = _format;
  258. }
  259. if (string.IsNullOrEmpty (sepChar)) {
  260. sepChar = _sepChar;
  261. }
  262. if (fmt.Length != text.Length) {
  263. return text;
  264. }
  265. var fmtText = text.ToCharArray ();
  266. for (int i = 0; i < text.Length; i++) {
  267. var c = fmt [i];
  268. if (c.ToString () == sepChar && text [i].ToString () != sepChar) {
  269. fmtText [i] = c;
  270. }
  271. }
  272. return new string (fmtText);
  273. }
  274. string GetDate (int month, int day, int year, string [] fm)
  275. {
  276. string date = " ";
  277. for (int i = 0; i < fm.Length; i++) {
  278. if (fm [i].Contains ("M")) {
  279. date += $"{month,2:00}";
  280. } else if (fm [i].Contains ("d")) {
  281. date += $"{day,2:00}";
  282. } else {
  283. if (_isShort && year.ToString ().Length == 4) {
  284. date += $"{year.ToString ().Substring (2, 2)}";
  285. } else if (_isShort) {
  286. date += $"{year,2:00}";
  287. } else {
  288. date += $"{year,4:0000}";
  289. }
  290. }
  291. if (i < 2) {
  292. date += $"{_sepChar}";
  293. }
  294. }
  295. return date;
  296. }
  297. string GetInvarianteDate (string text, bool isShort)
  298. {
  299. string [] vals = text.Split (_sepChar);
  300. string [] frm = (isShort ? $"MM{_sepChar}dd{_sepChar}yy" : GetInvarianteFormat ()).Split (_sepChar);
  301. string [] date = { null, null, null };
  302. for (int i = 0; i < frm.Length; i++) {
  303. if (frm [i].Contains ("M")) {
  304. date [0] = vals [i].Trim ();
  305. } else if (frm [i].Contains ("d")) {
  306. date [1] = vals [i].Trim ();
  307. } else {
  308. string yearString;
  309. if (isShort && vals [i].Length > 2) {
  310. yearString = vals [i].Substring (0, 2);
  311. } else if (!isShort && vals [i].Length > 4) {
  312. yearString = vals [i].Substring (0, 4);
  313. } else {
  314. yearString = vals [i].Trim ();
  315. }
  316. var year = int.Parse (yearString);
  317. if (isShort && year.ToString ().Length == 4) {
  318. date [2] = year.ToString ().Substring (2, 2);
  319. } else if (isShort) {
  320. date [2] = year.ToString ();
  321. } else {
  322. date [2] = $"{year,4:0000}";
  323. }
  324. }
  325. }
  326. return $"{date [0]}{_sepChar}{date [1]}{_sepChar}{date [2]}";
  327. }
  328. int GetFormatIndex (string [] fm, string t)
  329. {
  330. int idx = -1;
  331. for (int i = 0; i < fm.Length; i++) {
  332. if (fm [i].Contains (t)) {
  333. idx = i;
  334. break;
  335. }
  336. }
  337. return idx;
  338. }
  339. void IncCursorPosition ()
  340. {
  341. if (CursorPosition >= _fieldLen) {
  342. CursorPosition = _fieldLen;
  343. return;
  344. }
  345. if (Text [++CursorPosition] == _sepChar.ToCharArray () [0]) {
  346. CursorPosition++;
  347. }
  348. }
  349. void DecCursorPosition ()
  350. {
  351. if (CursorPosition <= 1) {
  352. CursorPosition = 1;
  353. return;
  354. }
  355. if (Text [--CursorPosition] == _sepChar.ToCharArray () [0]) {
  356. CursorPosition--;
  357. }
  358. }
  359. void AdjCursorPosition ()
  360. {
  361. if (Text [CursorPosition] == _sepChar.ToCharArray () [0]) {
  362. CursorPosition++;
  363. }
  364. }
  365. bool MoveRight ()
  366. {
  367. ClearAllSelection ();
  368. IncCursorPosition ();
  369. return true;
  370. }
  371. new bool MoveEnd ()
  372. {
  373. ClearAllSelection ();
  374. CursorPosition = _fieldLen;
  375. return true;
  376. }
  377. bool MoveLeft ()
  378. {
  379. ClearAllSelection ();
  380. DecCursorPosition ();
  381. return true;
  382. }
  383. bool MoveHome ()
  384. {
  385. // Home, C-A
  386. ClearAllSelection ();
  387. CursorPosition = 1;
  388. return true;
  389. }
  390. /// <inheritdoc/>
  391. public override void DeleteCharLeft (bool useOldCursorPos = true)
  392. {
  393. if (ReadOnly) {
  394. return;
  395. }
  396. ClearAllSelection ();
  397. SetText ((Rune)'0');
  398. DecCursorPosition ();
  399. return;
  400. }
  401. /// <inheritdoc/>
  402. public override void DeleteCharRight ()
  403. {
  404. if (ReadOnly) {
  405. return;
  406. }
  407. ClearAllSelection ();
  408. SetText ((Rune)'0');
  409. return;
  410. }
  411. /// <inheritdoc/>
  412. public override bool MouseEvent (MouseEvent ev)
  413. {
  414. if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) {
  415. return false;
  416. }
  417. if (!HasFocus) {
  418. SetFocus ();
  419. }
  420. int point = ev.X;
  421. if (point > _fieldLen) {
  422. point = _fieldLen;
  423. }
  424. if (point < 1) {
  425. point = 1;
  426. }
  427. CursorPosition = point;
  428. AdjCursorPosition ();
  429. return true;
  430. }
  431. /// <summary>
  432. /// Event firing method for the <see cref="DateChanged"/> event.
  433. /// </summary>
  434. /// <param name="args">Event arguments</param>
  435. public virtual void OnDateChanged (DateTimeEventArgs<DateTime> args) => DateChanged?.Invoke (this, args);
  436. }