TimeField.cs 12 KB

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