TimeField.cs 12 KB

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