TimeField.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. //
  2. // TimeField.cs: text entry for time
  3. //
  4. // Author: Jörg Preiß
  5. //
  6. // Licensed under the MIT license
  7. using System.Globalization;
  8. namespace Terminal.Gui;
  9. /// <summary>Time editing <see cref="View"/></summary>
  10. /// <remarks>The <see cref="TimeField"/> <see cref="View"/> provides time editing functionality with mouse support.</remarks>
  11. public class TimeField : TextField
  12. {
  13. private readonly int _longFieldLen = 8;
  14. private readonly string _longFormat;
  15. private readonly string _sepChar;
  16. private readonly int _shortFieldLen = 5;
  17. private readonly string _shortFormat;
  18. private bool _isShort;
  19. private TimeSpan _time;
  20. /// <summary>Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Computed"/> positioning.</summary>
  21. public TimeField ()
  22. {
  23. CultureInfo cultureInfo = CultureInfo.CurrentCulture;
  24. _sepChar = cultureInfo.DateTimeFormat.TimeSeparator;
  25. _longFormat = $" hh\\{_sepChar}mm\\{_sepChar}ss";
  26. _shortFormat = $" hh\\{_sepChar}mm";
  27. Width = FieldLength + 2;
  28. Time = TimeSpan.MinValue;
  29. CursorPosition = 1;
  30. TextChanging += TextField_TextChanging;
  31. // Things this view knows how to do
  32. AddCommand (
  33. Command.DeleteCharRight,
  34. () =>
  35. {
  36. DeleteCharRight ();
  37. return true;
  38. }
  39. );
  40. AddCommand (
  41. Command.DeleteCharLeft,
  42. () =>
  43. {
  44. DeleteCharLeft (false);
  45. return true;
  46. }
  47. );
  48. AddCommand (Command.LeftHome, () => MoveHome ());
  49. AddCommand (Command.Left, () => MoveLeft ());
  50. AddCommand (Command.RightEnd, () => MoveEnd ());
  51. AddCommand (Command.Right, () => MoveRight ());
  52. // Default keybindings for this view
  53. KeyBindings.Add (KeyCode.Delete, Command.DeleteCharRight);
  54. KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight);
  55. KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft);
  56. KeyBindings.Add (Key.D.WithAlt, Command.DeleteCharLeft);
  57. KeyBindings.Add (Key.Home, Command.LeftHome);
  58. KeyBindings.Add (Key.A.WithCtrl, Command.LeftHome);
  59. KeyBindings.Add (Key.CursorLeft, Command.Left);
  60. KeyBindings.Add (Key.B.WithCtrl, Command.Left);
  61. KeyBindings.Add (Key.End, Command.RightEnd);
  62. KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd);
  63. KeyBindings.Add (Key.CursorRight, Command.Right);
  64. KeyBindings.Add (Key.F.WithCtrl, Command.Right);
  65. }
  66. /// <inheritdoc/>
  67. public override int CursorPosition
  68. {
  69. get => base.CursorPosition;
  70. set => base.CursorPosition = Math.Max (Math.Min (value, FieldLength), 1);
  71. }
  72. /// <summary>Get or sets whether <see cref="TimeField"/> uses the short or long time format.</summary>
  73. public bool IsShortFormat
  74. {
  75. get => _isShort;
  76. set
  77. {
  78. _isShort = value;
  79. Width = FieldLength + 2;
  80. bool ro = ReadOnly;
  81. if (ro)
  82. {
  83. ReadOnly = false;
  84. }
  85. SetText (Text);
  86. ReadOnly = ro;
  87. SetNeedsDisplay ();
  88. }
  89. }
  90. /// <summary>Gets or sets the time of the <see cref="TimeField"/>.</summary>
  91. /// <remarks></remarks>
  92. public TimeSpan Time
  93. {
  94. get => _time;
  95. set
  96. {
  97. if (ReadOnly)
  98. {
  99. return;
  100. }
  101. TimeSpan oldTime = _time;
  102. _time = value;
  103. Text = " " + value.ToString (Format.Trim ());
  104. DateTimeEventArgs<TimeSpan> args = new (oldTime, value, Format);
  105. if (oldTime != value)
  106. {
  107. OnTimeChanged (args);
  108. }
  109. }
  110. }
  111. private int FieldLength => _isShort ? _shortFieldLen : _longFieldLen;
  112. private string Format => _isShort ? _shortFormat : _longFormat;
  113. /// <inheritdoc/>
  114. public override void DeleteCharLeft (bool useOldCursorPos = true)
  115. {
  116. if (ReadOnly)
  117. {
  118. return;
  119. }
  120. ClearAllSelection ();
  121. SetText ((Rune)'0');
  122. DecCursorPosition ();
  123. }
  124. /// <inheritdoc/>
  125. public override void DeleteCharRight ()
  126. {
  127. if (ReadOnly)
  128. {
  129. return;
  130. }
  131. ClearAllSelection ();
  132. SetText ((Rune)'0');
  133. }
  134. /// <inheritdoc/>
  135. public override bool MouseEvent (MouseEvent ev)
  136. {
  137. bool result = base.MouseEvent (ev);
  138. if (result && SelectedLength == 0 && ev.Flags.HasFlag (MouseFlags.Button1Pressed))
  139. {
  140. int point = ev.X;
  141. AdjCursorPosition (point);
  142. }
  143. return result;
  144. }
  145. /// <inheritdoc/>
  146. public override bool OnProcessKeyDown (Key a)
  147. {
  148. // Ignore non-numeric characters.
  149. if (a.KeyCode is >= (KeyCode)(int)KeyCode.D0 and <= (KeyCode)(int)KeyCode.D9)
  150. {
  151. if (!ReadOnly)
  152. {
  153. if (SetText ((Rune)a))
  154. {
  155. IncCursorPosition ();
  156. }
  157. }
  158. return true;
  159. }
  160. return false;
  161. }
  162. /// <summary>Event firing method that invokes the <see cref="TimeChanged"/> event.</summary>
  163. /// <param name="args">The event arguments</param>
  164. public virtual void OnTimeChanged (DateTimeEventArgs<TimeSpan> args) { TimeChanged?.Invoke (this, args); }
  165. /// <summary>TimeChanged event, raised when the Date has changed.</summary>
  166. /// <remarks>This event is raised when the <see cref="Time"/> changes.</remarks>
  167. /// <remarks>
  168. /// The passed <see cref="EventArgs"/> is a <see cref="DateTimeEventArgs{T}"/> containing the old value, new
  169. /// value, and format string.
  170. /// </remarks>
  171. public event EventHandler<DateTimeEventArgs<TimeSpan>> TimeChanged;
  172. private void AdjCursorPosition (int point, bool increment = true)
  173. {
  174. int newPoint = point;
  175. if (point > FieldLength)
  176. {
  177. newPoint = FieldLength;
  178. }
  179. if (point < 1)
  180. {
  181. newPoint = 1;
  182. }
  183. if (newPoint != point)
  184. {
  185. CursorPosition = newPoint;
  186. }
  187. while (Text [CursorPosition] == _sepChar [0])
  188. {
  189. if (increment)
  190. {
  191. CursorPosition++;
  192. }
  193. else
  194. {
  195. CursorPosition--;
  196. }
  197. }
  198. }
  199. private void DecCursorPosition ()
  200. {
  201. if (CursorPosition <= 1)
  202. {
  203. CursorPosition = 1;
  204. return;
  205. }
  206. CursorPosition--;
  207. AdjCursorPosition (CursorPosition, false);
  208. }
  209. private void IncCursorPosition ()
  210. {
  211. if (CursorPosition >= FieldLength)
  212. {
  213. CursorPosition = FieldLength;
  214. return;
  215. }
  216. CursorPosition++;
  217. AdjCursorPosition (CursorPosition);
  218. }
  219. private new bool MoveEnd ()
  220. {
  221. ClearAllSelection ();
  222. CursorPosition = FieldLength;
  223. return true;
  224. }
  225. private bool MoveHome ()
  226. {
  227. // Home, C-A
  228. ClearAllSelection ();
  229. CursorPosition = 1;
  230. return true;
  231. }
  232. private bool MoveLeft ()
  233. {
  234. ClearAllSelection ();
  235. DecCursorPosition ();
  236. return true;
  237. }
  238. private bool MoveRight ()
  239. {
  240. ClearAllSelection ();
  241. IncCursorPosition ();
  242. return true;
  243. }
  244. private string NormalizeFormat (string text, string fmt = null, string sepChar = null)
  245. {
  246. if (string.IsNullOrEmpty (fmt))
  247. {
  248. fmt = Format;
  249. }
  250. fmt = fmt.Replace ("\\", "");
  251. if (string.IsNullOrEmpty (sepChar))
  252. {
  253. sepChar = _sepChar;
  254. }
  255. if (fmt.Length != text.Length)
  256. {
  257. return text;
  258. }
  259. char [] fmtText = text.ToCharArray ();
  260. for (var i = 0; i < text.Length; i++)
  261. {
  262. char c = fmt [i];
  263. if (c.ToString () == sepChar && text [i].ToString () != sepChar)
  264. {
  265. fmtText [i] = c;
  266. }
  267. }
  268. return new string (fmtText);
  269. }
  270. private bool SetText (Rune key)
  271. {
  272. List<Rune> text = Text.EnumerateRunes ().ToList ();
  273. List<Rune> newText = text.GetRange (0, CursorPosition);
  274. newText.Add (key);
  275. if (CursorPosition < FieldLength)
  276. {
  277. newText =
  278. [
  279. .. newText,
  280. .. text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))
  281. ];
  282. }
  283. return SetText (StringExtensions.ToString (newText));
  284. }
  285. private bool SetText (string text)
  286. {
  287. if (string.IsNullOrEmpty (text))
  288. {
  289. return false;
  290. }
  291. text = NormalizeFormat (text);
  292. string [] vals = text.Split (_sepChar);
  293. var isValidTime = true;
  294. int hour = int.Parse (vals [0]);
  295. int minute = int.Parse (vals [1]);
  296. int second = _isShort ? 0 :
  297. vals.Length > 2 ? int.Parse (vals [2]) : 0;
  298. if (hour < 0)
  299. {
  300. isValidTime = false;
  301. hour = 0;
  302. vals [0] = "0";
  303. }
  304. else if (hour > 23)
  305. {
  306. isValidTime = false;
  307. hour = 23;
  308. vals [0] = "23";
  309. }
  310. if (minute < 0)
  311. {
  312. isValidTime = false;
  313. minute = 0;
  314. vals [1] = "0";
  315. }
  316. else if (minute > 59)
  317. {
  318. isValidTime = false;
  319. minute = 59;
  320. vals [1] = "59";
  321. }
  322. if (second < 0)
  323. {
  324. isValidTime = false;
  325. second = 0;
  326. vals [2] = "0";
  327. }
  328. else if (second > 59)
  329. {
  330. isValidTime = false;
  331. second = 59;
  332. vals [2] = "59";
  333. }
  334. string t = _isShort
  335. ? $" {hour,2:00}{_sepChar}{minute,2:00}"
  336. : $" {hour,2:00}{_sepChar}{minute,2:00}{_sepChar}{second,2:00}";
  337. if (!TimeSpan.TryParseExact (
  338. t.Trim (),
  339. Format.Trim (),
  340. CultureInfo.CurrentCulture,
  341. TimeSpanStyles.None,
  342. out TimeSpan result
  343. )
  344. || !isValidTime)
  345. {
  346. return false;
  347. }
  348. if (IsInitialized)
  349. {
  350. Time = result;
  351. }
  352. return true;
  353. }
  354. private void TextField_TextChanging (object sender, TextChangingEventArgs e)
  355. {
  356. try
  357. {
  358. var spaces = 0;
  359. for (var i = 0; i < e.NewText.Length; i++)
  360. {
  361. if (e.NewText [i] == ' ')
  362. {
  363. spaces++;
  364. }
  365. else
  366. {
  367. break;
  368. }
  369. }
  370. spaces += FieldLength;
  371. string trimedText = e.NewText [..spaces];
  372. spaces -= FieldLength;
  373. trimedText = trimedText.Replace (new string (' ', spaces), " ");
  374. if (trimedText != e.NewText)
  375. {
  376. e.NewText = trimedText;
  377. }
  378. if (!TimeSpan.TryParseExact (
  379. e.NewText.Trim (),
  380. Format.Trim (),
  381. CultureInfo.CurrentCulture,
  382. TimeSpanStyles.None,
  383. out TimeSpan result
  384. ))
  385. {
  386. e.Cancel = true;
  387. }
  388. AdjCursorPosition (CursorPosition);
  389. }
  390. catch (Exception)
  391. {
  392. e.Cancel = true;
  393. }
  394. }
  395. }