DateField.cs 11 KB

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