TimeField.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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. SetNeedsDisplay ();
  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 (MouseEvent ev)
  138. {
  139. if (SelectedLength == 0 && ev.Flags.HasFlag (MouseFlags.Button1Pressed))
  140. {
  141. int point = ev.Position.X;
  142. AdjCursorPosition (point);
  143. }
  144. return ev.Handled;
  145. }
  146. /// <inheritdoc/>
  147. protected override bool OnKeyDownNotHandled (Key a)
  148. {
  149. // Ignore non-numeric characters.
  150. if (a.KeyCode is >= (KeyCode)(int)KeyCode.D0 and <= (KeyCode)(int)KeyCode.D9)
  151. {
  152. if (!ReadOnly)
  153. {
  154. if (SetText ((Rune)a))
  155. {
  156. IncCursorPosition ();
  157. }
  158. }
  159. return true;
  160. }
  161. return false;
  162. }
  163. /// <summary>Event firing method that invokes the <see cref="TimeChanged"/> event.</summary>
  164. /// <param name="args">The event arguments</param>
  165. public virtual void OnTimeChanged (DateTimeEventArgs<TimeSpan> args) { TimeChanged?.Invoke (this, args); }
  166. /// <summary>TimeChanged event, raised when the Date has changed.</summary>
  167. /// <remarks>This event is raised when the <see cref="Time"/> changes.</remarks>
  168. /// <remarks>
  169. /// The passed <see cref="EventArgs"/> is a <see cref="DateTimeEventArgs{T}"/> containing the old value, new
  170. /// value, and format string.
  171. /// </remarks>
  172. public event EventHandler<DateTimeEventArgs<TimeSpan>> TimeChanged;
  173. private void AdjCursorPosition (int point, bool increment = true)
  174. {
  175. int newPoint = point;
  176. if (point > FieldLength)
  177. {
  178. newPoint = FieldLength;
  179. }
  180. if (point < 1)
  181. {
  182. newPoint = 1;
  183. }
  184. if (newPoint != point)
  185. {
  186. CursorPosition = newPoint;
  187. }
  188. while (Text [CursorPosition] == _sepChar [0])
  189. {
  190. if (increment)
  191. {
  192. CursorPosition++;
  193. }
  194. else
  195. {
  196. CursorPosition--;
  197. }
  198. }
  199. }
  200. private void DecCursorPosition ()
  201. {
  202. if (CursorPosition <= 1)
  203. {
  204. CursorPosition = 1;
  205. return;
  206. }
  207. CursorPosition--;
  208. AdjCursorPosition (CursorPosition, false);
  209. }
  210. private void IncCursorPosition ()
  211. {
  212. if (CursorPosition >= FieldLength)
  213. {
  214. CursorPosition = FieldLength;
  215. return;
  216. }
  217. CursorPosition++;
  218. AdjCursorPosition (CursorPosition);
  219. }
  220. private new bool MoveEnd ()
  221. {
  222. ClearAllSelection ();
  223. CursorPosition = FieldLength;
  224. return true;
  225. }
  226. private bool MoveHome ()
  227. {
  228. // Home, C-A
  229. ClearAllSelection ();
  230. CursorPosition = 1;
  231. return true;
  232. }
  233. private bool MoveLeft ()
  234. {
  235. ClearAllSelection ();
  236. DecCursorPosition ();
  237. return true;
  238. }
  239. private bool MoveRight ()
  240. {
  241. ClearAllSelection ();
  242. IncCursorPosition ();
  243. return true;
  244. }
  245. private string NormalizeFormat (string text, string fmt = null, string sepChar = null)
  246. {
  247. if (string.IsNullOrEmpty (fmt))
  248. {
  249. fmt = Format;
  250. }
  251. fmt = fmt.Replace ("\\", "");
  252. if (string.IsNullOrEmpty (sepChar))
  253. {
  254. sepChar = _sepChar;
  255. }
  256. if (fmt.Length != text.Length)
  257. {
  258. return text;
  259. }
  260. char [] fmtText = text.ToCharArray ();
  261. for (var i = 0; i < text.Length; i++)
  262. {
  263. char c = fmt [i];
  264. if (c.ToString () == sepChar && text [i].ToString () != sepChar)
  265. {
  266. fmtText [i] = c;
  267. }
  268. }
  269. return new string (fmtText);
  270. }
  271. private bool SetText (Rune key)
  272. {
  273. List<Rune> text = Text.EnumerateRunes ().ToList ();
  274. List<Rune> newText = text.GetRange (0, CursorPosition);
  275. newText.Add (key);
  276. if (CursorPosition < FieldLength)
  277. {
  278. newText =
  279. [
  280. .. newText,
  281. .. text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))
  282. ];
  283. }
  284. return SetText (StringExtensions.ToString (newText));
  285. }
  286. private bool SetText (string text)
  287. {
  288. if (string.IsNullOrEmpty (text))
  289. {
  290. return false;
  291. }
  292. text = NormalizeFormat (text);
  293. string [] vals = text.Split (_sepChar);
  294. var isValidTime = true;
  295. int hour = int.Parse (vals [0]);
  296. int minute = int.Parse (vals [1]);
  297. int second = _isShort ? 0 :
  298. vals.Length > 2 ? int.Parse (vals [2]) : 0;
  299. if (hour < 0)
  300. {
  301. isValidTime = false;
  302. hour = 0;
  303. vals [0] = "0";
  304. }
  305. else if (hour > 23)
  306. {
  307. isValidTime = false;
  308. hour = 23;
  309. vals [0] = "23";
  310. }
  311. if (minute < 0)
  312. {
  313. isValidTime = false;
  314. minute = 0;
  315. vals [1] = "0";
  316. }
  317. else if (minute > 59)
  318. {
  319. isValidTime = false;
  320. minute = 59;
  321. vals [1] = "59";
  322. }
  323. if (second < 0)
  324. {
  325. isValidTime = false;
  326. second = 0;
  327. vals [2] = "0";
  328. }
  329. else if (second > 59)
  330. {
  331. isValidTime = false;
  332. second = 59;
  333. vals [2] = "59";
  334. }
  335. string t = _isShort
  336. ? $" {hour,2:00}{_sepChar}{minute,2:00}"
  337. : $" {hour,2:00}{_sepChar}{minute,2:00}{_sepChar}{second,2:00}";
  338. if (!TimeSpan.TryParseExact (
  339. t.Trim (),
  340. Format.Trim (),
  341. CultureInfo.CurrentCulture,
  342. TimeSpanStyles.None,
  343. out TimeSpan result
  344. )
  345. || !isValidTime)
  346. {
  347. return false;
  348. }
  349. if (IsInitialized)
  350. {
  351. Time = result;
  352. }
  353. return true;
  354. }
  355. private void TextField_TextChanging (object sender, CancelEventArgs<string> e)
  356. {
  357. try
  358. {
  359. var spaces = 0;
  360. for (var i = 0; i < e.NewValue.Length; i++)
  361. {
  362. if (e.NewValue [i] == ' ')
  363. {
  364. spaces++;
  365. }
  366. else
  367. {
  368. break;
  369. }
  370. }
  371. spaces += FieldLength;
  372. string trimmedText = e.NewValue [..spaces];
  373. spaces -= FieldLength;
  374. trimmedText = trimmedText.Replace (new string (' ', spaces), " ");
  375. if (trimmedText != e.NewValue)
  376. {
  377. e.NewValue = trimmedText;
  378. }
  379. if (!TimeSpan.TryParseExact (
  380. e.NewValue.Trim (),
  381. Format.Trim (),
  382. CultureInfo.CurrentCulture,
  383. TimeSpanStyles.None,
  384. out TimeSpan result
  385. ))
  386. {
  387. e.Cancel = true;
  388. }
  389. AdjCursorPosition (CursorPosition);
  390. }
  391. catch (Exception)
  392. {
  393. e.Cancel = true;
  394. }
  395. }
  396. }