DateField.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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, "") => Initialize (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. Initialize (date);
  59. }
  60. void Initialize (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.DeleteChar, Command.DeleteCharRight);
  85. KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight);
  86. KeyBindings.Add (Key.Delete, Command.DeleteCharLeft);
  87. KeyBindings.Add (Key.Backspace, 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. if (!DateTime.TryParseExact (GetDate (Text), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out var result)) {
  115. Text = e.OldValue;
  116. }
  117. } catch (Exception) {
  118. Text = e.OldValue;
  119. }
  120. }
  121. string GetInvarianteFormat () => $"MM{sepChar}dd{sepChar}yyyy";
  122. string GetLongFormat (string lf)
  123. {
  124. string [] frm = lf.Split (sepChar);
  125. for (int i = 0; i < frm.Length; i++) {
  126. if (frm [i].Contains ("M") && frm [i].GetRuneCount () < 2) {
  127. lf = lf.Replace ("M", "MM");
  128. }
  129. if (frm [i].Contains ("d") && frm [i].GetRuneCount () < 2) {
  130. lf = lf.Replace ("d", "dd");
  131. }
  132. if (frm [i].Contains ("y") && frm [i].GetRuneCount () < 4) {
  133. lf = lf.Replace ("yy", "yyyy");
  134. }
  135. }
  136. return $" {lf}";
  137. }
  138. string GetShortFormat (string lf) => lf.Replace ("yyyy", "yy");
  139. /// <summary>
  140. /// Gets or sets the date of the <see cref="DateField"/>.
  141. /// </summary>
  142. /// <remarks>
  143. /// </remarks>
  144. public DateTime Date {
  145. get => date;
  146. set {
  147. if (ReadOnly) {
  148. return;
  149. }
  150. var oldData = date;
  151. date = value;
  152. Text = value.ToString (format);
  153. var args = new DateTimeEventArgs<DateTime> (oldData, value, format);
  154. if (oldData != value) {
  155. OnDateChanged (args);
  156. }
  157. }
  158. }
  159. /// <summary>
  160. /// Get or set the date format for the widget.
  161. /// </summary>
  162. public bool IsShortFormat {
  163. get => isShort;
  164. set {
  165. isShort = value;
  166. if (isShort) {
  167. Width = 10;
  168. } else {
  169. Width = 12;
  170. }
  171. bool ro = ReadOnly;
  172. if (ro) {
  173. ReadOnly = false;
  174. }
  175. SetText (Text);
  176. ReadOnly = ro;
  177. SetNeedsDisplay ();
  178. }
  179. }
  180. /// <inheritdoc/>
  181. public override int CursorPosition {
  182. get => base.CursorPosition;
  183. set => base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1);
  184. }
  185. bool SetText (Rune key)
  186. {
  187. var text = Text.EnumerateRunes ().ToList ();
  188. var newText = text.GetRange (0, CursorPosition);
  189. newText.Add (key);
  190. if (CursorPosition < fieldLen) {
  191. newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList ();
  192. }
  193. return SetText (StringExtensions.ToString (newText));
  194. }
  195. bool SetText (string text)
  196. {
  197. if (string.IsNullOrEmpty (text)) {
  198. return false;
  199. }
  200. string [] vals = text.Split (sepChar);
  201. string [] frm = format.Split (sepChar);
  202. bool isValidDate = true;
  203. int idx = GetFormatIndex (frm, "y");
  204. int year = Int32.Parse (vals [idx]);
  205. int month;
  206. int day;
  207. idx = GetFormatIndex (frm, "M");
  208. if (Int32.Parse (vals [idx]) < 1) {
  209. isValidDate = false;
  210. month = 1;
  211. vals [idx] = "1";
  212. } else if (Int32.Parse (vals [idx]) > 12) {
  213. isValidDate = false;
  214. month = 12;
  215. vals [idx] = "12";
  216. } else {
  217. month = Int32.Parse (vals [idx]);
  218. }
  219. idx = GetFormatIndex (frm, "d");
  220. if (Int32.Parse (vals [idx]) < 1) {
  221. isValidDate = false;
  222. day = 1;
  223. vals [idx] = "1";
  224. } else if (Int32.Parse (vals [idx]) > 31) {
  225. isValidDate = false;
  226. day = DateTime.DaysInMonth (year, month);
  227. vals [idx] = day.ToString ();
  228. } else {
  229. day = Int32.Parse (vals [idx]);
  230. }
  231. string d = GetDate (month, day, year, frm);
  232. if (!DateTime.TryParseExact (d, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out var result) ||
  233. !isValidDate) {
  234. return false;
  235. }
  236. Date = result;
  237. return true;
  238. }
  239. string GetDate (int month, int day, int year, string [] fm)
  240. {
  241. string date = " ";
  242. for (int i = 0; i < fm.Length; i++) {
  243. if (fm [i].Contains ("M")) {
  244. date += $"{month,2:00}";
  245. } else if (fm [i].Contains ("d")) {
  246. date += $"{day,2:00}";
  247. } else {
  248. if (!isShort && year.ToString ().Length == 2) {
  249. string y = DateTime.Now.Year.ToString ();
  250. date += y.Substring (0, 2) + year.ToString ();
  251. } else if (isShort && year.ToString ().Length == 4) {
  252. date += $"{year.ToString ().Substring (2, 2)}";
  253. } else {
  254. date += $"{year,2:00}";
  255. }
  256. }
  257. if (i < 2) {
  258. date += $"{sepChar}";
  259. }
  260. }
  261. return date;
  262. }
  263. string GetDate (string text)
  264. {
  265. string [] vals = text.Split (sepChar);
  266. string [] frm = format.Split (sepChar);
  267. string [] date = { null, null, null };
  268. for (int i = 0; i < frm.Length; i++) {
  269. if (frm [i].Contains ("M")) {
  270. date [0] = vals [i].Trim ();
  271. } else if (frm [i].Contains ("d")) {
  272. date [1] = vals [i].Trim ();
  273. } else {
  274. string year = vals [i].Trim ();
  275. if (year.GetRuneCount () == 2) {
  276. string y = DateTime.Now.Year.ToString ();
  277. date [2] = y.Substring (0, 2) + year.ToString ();
  278. } else {
  279. date [2] = vals [i].Trim ();
  280. }
  281. }
  282. }
  283. return date [0] + sepChar + date [1] + sepChar + date [2];
  284. }
  285. int GetFormatIndex (string [] fm, string t)
  286. {
  287. int idx = -1;
  288. for (int i = 0; i < fm.Length; i++) {
  289. if (fm [i].Contains (t)) {
  290. idx = i;
  291. break;
  292. }
  293. }
  294. return idx;
  295. }
  296. void IncCursorPosition ()
  297. {
  298. if (CursorPosition == fieldLen) {
  299. return;
  300. }
  301. if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) {
  302. CursorPosition++;
  303. }
  304. }
  305. void DecCursorPosition ()
  306. {
  307. if (CursorPosition == 1) {
  308. return;
  309. }
  310. if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) {
  311. CursorPosition--;
  312. }
  313. }
  314. void AdjCursorPosition ()
  315. {
  316. if (Text [CursorPosition] == sepChar.ToCharArray () [0]) {
  317. CursorPosition++;
  318. }
  319. }
  320. bool MoveRight ()
  321. {
  322. IncCursorPosition ();
  323. return true;
  324. }
  325. new bool MoveEnd ()
  326. {
  327. CursorPosition = fieldLen;
  328. return true;
  329. }
  330. bool MoveLeft ()
  331. {
  332. DecCursorPosition ();
  333. return true;
  334. }
  335. bool MoveHome ()
  336. {
  337. // Home, C-A
  338. CursorPosition = 1;
  339. return true;
  340. }
  341. /// <inheritdoc/>
  342. public override void DeleteCharLeft (bool useOldCursorPos = true)
  343. {
  344. if (ReadOnly) {
  345. return;
  346. }
  347. SetText ((Rune)'0');
  348. DecCursorPosition ();
  349. return;
  350. }
  351. /// <inheritdoc/>
  352. public override void DeleteCharRight ()
  353. {
  354. if (ReadOnly) {
  355. return;
  356. }
  357. SetText ((Rune)'0');
  358. return;
  359. }
  360. /// <inheritdoc/>
  361. public override bool MouseEvent (MouseEvent ev)
  362. {
  363. if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) {
  364. return false;
  365. }
  366. if (!HasFocus) {
  367. SetFocus ();
  368. }
  369. int point = ev.X;
  370. if (point > fieldLen) {
  371. point = fieldLen;
  372. }
  373. if (point < 1) {
  374. point = 1;
  375. }
  376. CursorPosition = point;
  377. AdjCursorPosition ();
  378. return true;
  379. }
  380. /// <summary>
  381. /// Event firing method for the <see cref="DateChanged"/> event.
  382. /// </summary>
  383. /// <param name="args">Event arguments</param>
  384. public virtual void OnDateChanged (DateTimeEventArgs<DateTime> args) => DateChanged?.Invoke (this, args);
  385. }