DateField.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  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 NStack;
  12. namespace Terminal.Gui {
  13. /// <summary>
  14. /// 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 { get { return isShort ? shortFieldLen : longFieldLen; } }
  28. string Format { get { return isShort ? shortFormat : longFormat; } }
  29. /// <summary>
  30. /// DateChanged event, raised when the Date has changed.
  31. /// </summary>
  32. /// <remarks>
  33. /// This event is raised when the <see cref="Date"/> changes.
  34. /// </remarks>
  35. /// <remarks>
  36. /// The passed <see cref="EventArgs"/> is a <see cref="DateTimeEventArgs"/> containing the old, new value and format.
  37. /// </remarks>
  38. public event Action<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, "")
  47. {
  48. this.isShort = isShort;
  49. Initialize (date);
  50. }
  51. /// <summary>
  52. /// Initializes a new instance of <see cref="DateField"/> using <see cref="LayoutStyle.Computed"/> layout.
  53. /// </summary>
  54. public DateField () : this (DateTime.MinValue) { }
  55. /// <summary>
  56. /// Initializes a new instance of <see cref="DateField"/> using <see cref="LayoutStyle.Computed"/> layout.
  57. /// </summary>
  58. /// <param name="date"></param>
  59. public DateField (DateTime date) : base ("")
  60. {
  61. this.isShort = true;
  62. Width = FieldLen + 2;
  63. Initialize (date);
  64. }
  65. void Initialize (DateTime date)
  66. {
  67. CultureInfo cultureInfo = CultureInfo.CurrentCulture;
  68. sepChar = cultureInfo.DateTimeFormat.DateSeparator;
  69. longFormat = GetLongFormat (cultureInfo.DateTimeFormat.ShortDatePattern);
  70. shortFormat = GetShortFormat (longFormat);
  71. CursorPosition = 1;
  72. Date = date;
  73. Changed += DateField_Changed;
  74. }
  75. void DateField_Changed (ustring e)
  76. {
  77. try {
  78. if (!DateTime.TryParseExact (GetDate (Text).ToString (), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result))
  79. Text = e;
  80. } catch (Exception) {
  81. Text = e;
  82. }
  83. }
  84. string GetInvarianteFormat ()
  85. {
  86. return $"MM{sepChar}dd{sepChar}yyyy";
  87. }
  88. string GetLongFormat (string lf)
  89. {
  90. ustring [] frm = ustring.Make (lf).Split (ustring.Make (sepChar));
  91. for (int i = 0; i < frm.Length; i++) {
  92. if (frm [i].Contains ("M") && frm [i].Length < 2)
  93. lf = lf.Replace ("M", "MM");
  94. if (frm [i].Contains ("d") && frm [i].Length < 2)
  95. lf = lf.Replace ("d", "dd");
  96. if (frm [i].Contains ("y") && frm [i].Length < 4)
  97. lf = lf.Replace ("yy", "yyyy");
  98. }
  99. return $" {lf}";
  100. }
  101. string GetShortFormat (string lf)
  102. {
  103. return lf.Replace ("yyyy", "yy");
  104. }
  105. /// <summary>
  106. /// Gets or sets the date of the <see cref="DateField"/>.
  107. /// </summary>
  108. /// <remarks>
  109. /// </remarks>
  110. public DateTime Date {
  111. get {
  112. return date;
  113. }
  114. set {
  115. if (ReadOnly)
  116. return;
  117. var oldData = date;
  118. date = value;
  119. this.Text = value.ToString (Format);
  120. var args = new DateTimeEventArgs<DateTime> (oldData, value, Format);
  121. if (oldData != value) {
  122. OnDateChanged (args);
  123. }
  124. }
  125. }
  126. /// <summary>
  127. /// Get or set the data format for the widget.
  128. /// </summary>
  129. public bool IsShortFormat {
  130. get => isShort;
  131. set {
  132. isShort = value;
  133. if (isShort)
  134. Width = 10;
  135. else
  136. Width = 12;
  137. var ro = ReadOnly;
  138. if (ro)
  139. ReadOnly = false;
  140. SetText (Text);
  141. ReadOnly = ro;
  142. SetNeedsDisplay ();
  143. }
  144. }
  145. bool SetText (Rune key)
  146. {
  147. var text = TextModel.ToRunes (Text);
  148. var newText = text.GetRange (0, CursorPosition);
  149. newText.Add (key);
  150. if (CursorPosition < FieldLen)
  151. newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList ();
  152. return SetText (ustring.Make (newText));
  153. }
  154. bool SetText (ustring text)
  155. {
  156. if (text.IsEmpty) {
  157. return false;
  158. }
  159. ustring [] vals = text.Split (ustring.Make (sepChar));
  160. ustring [] frm = ustring.Make (Format).Split (ustring.Make (sepChar));
  161. bool isValidDate = true;
  162. int idx = GetFormatIndex (frm, "y");
  163. int year = Int32.Parse (vals [idx].ToString ());
  164. int month;
  165. int day;
  166. idx = GetFormatIndex (frm, "M");
  167. if (Int32.Parse (vals [idx].ToString ()) < 1) {
  168. isValidDate = false;
  169. month = 1;
  170. vals [idx] = "1";
  171. } else if (Int32.Parse (vals [idx].ToString ()) > 12) {
  172. isValidDate = false;
  173. month = 12;
  174. vals [idx] = "12";
  175. } else
  176. month = Int32.Parse (vals [idx].ToString ());
  177. idx = GetFormatIndex (frm, "d");
  178. if (Int32.Parse (vals [idx].ToString ()) < 1) {
  179. isValidDate = false;
  180. day = 1;
  181. vals [idx] = "1";
  182. } else if (Int32.Parse (vals [idx].ToString ()) > 31) {
  183. isValidDate = false;
  184. day = DateTime.DaysInMonth (year, month);
  185. vals [idx] = day.ToString ();
  186. } else
  187. day = Int32.Parse (vals [idx].ToString ());
  188. string d = GetDate (month, day, year, frm);
  189. if (!DateTime.TryParseExact (d, Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) ||
  190. !isValidDate)
  191. return false;
  192. Date = result;
  193. return true;
  194. }
  195. string GetDate (int month, int day, int year, ustring [] fm)
  196. {
  197. string date = " ";
  198. for (int i = 0; i < fm.Length; i++) {
  199. if (fm [i].Contains ("M")) {
  200. date += $"{month,2:00}";
  201. } else if (fm [i].Contains ("d")) {
  202. date += $"{day,2:00}";
  203. } else {
  204. if (!isShort && year.ToString ().Length == 2) {
  205. var y = DateTime.Now.Year.ToString ();
  206. date += y.Substring (0, 2) + year.ToString ();
  207. } else if (isShort && year.ToString ().Length == 4) {
  208. date += $"{year.ToString ().Substring (2, 2)}";
  209. } else {
  210. date += $"{year,2:00}";
  211. }
  212. }
  213. if (i < 2)
  214. date += $"{sepChar}";
  215. }
  216. return date;
  217. }
  218. ustring GetDate (ustring text)
  219. {
  220. ustring [] vals = text.Split (ustring.Make (sepChar));
  221. ustring [] frm = ustring.Make (Format).Split (ustring.Make (sepChar));
  222. ustring [] date = { null, null, null };
  223. for (int i = 0; i < frm.Length; i++) {
  224. if (frm [i].Contains ("M")) {
  225. date [0] = vals [i].TrimSpace ();
  226. } else if (frm [i].Contains ("d")) {
  227. date [1] = vals [i].TrimSpace ();
  228. } else {
  229. var year = vals [i].TrimSpace ();
  230. if (year.Length == 2) {
  231. var y = DateTime.Now.Year.ToString ();
  232. date [2] = y.Substring (0, 2) + year.ToString ();
  233. } else {
  234. date [2] = vals [i].TrimSpace ();
  235. }
  236. }
  237. }
  238. return date [0] + ustring.Make (sepChar) + date [1] + ustring.Make (sepChar) + date [2];
  239. }
  240. int GetFormatIndex (ustring [] fm, string t)
  241. {
  242. int idx = -1;
  243. for (int i = 0; i < fm.Length; i++) {
  244. if (fm [i].Contains (t)) {
  245. idx = i;
  246. break;
  247. }
  248. }
  249. return idx;
  250. }
  251. void IncCursorPosition ()
  252. {
  253. if (CursorPosition == FieldLen)
  254. return;
  255. if (Text [++CursorPosition] == sepChar.ToCharArray () [0])
  256. CursorPosition++;
  257. }
  258. void DecCursorPosition ()
  259. {
  260. if (CursorPosition == 1)
  261. return;
  262. if (Text [--CursorPosition] == sepChar.ToCharArray () [0])
  263. CursorPosition--;
  264. }
  265. void AdjCursorPosition ()
  266. {
  267. if (Text [CursorPosition] == sepChar.ToCharArray () [0])
  268. CursorPosition++;
  269. }
  270. /// <inheritdoc/>
  271. public override bool ProcessKey (KeyEvent kb)
  272. {
  273. switch (kb.Key) {
  274. case Key.DeleteChar:
  275. case Key.ControlD:
  276. if (ReadOnly)
  277. return true;
  278. SetText ('0');
  279. break;
  280. case Key.Delete:
  281. case Key.Backspace:
  282. if (ReadOnly)
  283. return true;
  284. SetText ('0');
  285. DecCursorPosition ();
  286. break;
  287. // Home, C-A
  288. case Key.Home:
  289. case Key.ControlA:
  290. CursorPosition = 1;
  291. break;
  292. case Key.CursorLeft:
  293. case Key.ControlB:
  294. DecCursorPosition ();
  295. break;
  296. case Key.End:
  297. case Key.ControlE: // End
  298. CursorPosition = FieldLen;
  299. break;
  300. case Key.CursorRight:
  301. case Key.ControlF:
  302. IncCursorPosition ();
  303. break;
  304. default:
  305. // Ignore non-numeric characters.
  306. if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9'))
  307. return false;
  308. if (ReadOnly)
  309. return true;
  310. if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ()))
  311. IncCursorPosition ();
  312. return true;
  313. }
  314. return true;
  315. }
  316. /// <inheritdoc/>
  317. public override bool MouseEvent (MouseEvent ev)
  318. {
  319. if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked))
  320. return false;
  321. if (!HasFocus)
  322. SuperView.SetFocus (this);
  323. var point = ev.X;
  324. if (point > FieldLen)
  325. point = FieldLen;
  326. if (point < 1)
  327. point = 1;
  328. CursorPosition = point;
  329. AdjCursorPosition ();
  330. return true;
  331. }
  332. /// <summary>
  333. /// Virtual method that will invoke the <see cref="DateChanged"/> with a <see cref="DateTimeEventArgs"/>.
  334. /// </summary>
  335. /// <param name="args">The arguments of the <see cref="DateTimeEventArgs"/></param>
  336. public virtual void OnDateChanged (DateTimeEventArgs<DateTime> args)
  337. {
  338. DateChanged?.Invoke (args);
  339. }
  340. }
  341. /// <summary>
  342. /// Handled the <see cref="EventArgs"/> for <see cref="DateField"/> or <see cref="TimeField"/> events.
  343. /// </summary>
  344. public class DateTimeEventArgs<T> : EventArgs {
  345. /// <summary>
  346. /// The old <see cref="DateField"/> or <see cref="TimeField"/> value.
  347. /// </summary>
  348. public T OldValue {get;}
  349. /// <summary>
  350. /// The new <see cref="DateField"/> or <see cref="TimeField"/> value.
  351. /// </summary>
  352. public T NewValue { get; }
  353. /// <summary>
  354. /// The <see cref="DateField"/> or <see cref="TimeField"/> format.
  355. /// </summary>
  356. public string Format { get; }
  357. /// <summary>
  358. /// Initializes a new instance of <see cref="DateTimeEventArgs"/>
  359. /// </summary>
  360. /// <param name="oldValue">The old <see cref="DateField"/> or <see cref="TimeField"/> value.</param>
  361. /// <param name="newValue">The new <see cref="DateField"/> or <see cref="TimeField"/> value.</param>
  362. /// <param name="format">The <see cref="DateField"/> or <see cref="TimeField"/> format.</param>
  363. public DateTimeEventArgs (T oldValue, T newValue, string format)
  364. {
  365. OldValue = oldValue;
  366. NewValue = newValue;
  367. Format = format;
  368. }
  369. }
  370. }