TimeField.cs 12 KB

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