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.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. EventArgs<TimeSpan> args = new (value);
  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 (EventArgs<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. public event EventHandler<EventArgs<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 (CursorPosition < Text.GetColumns() -1 && 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, ResultEventArgs<string> e)
  355. {
  356. try
  357. {
  358. var spaces = 0;
  359. for (var i = 0; i < e.Result.Length; i++)
  360. {
  361. if (e.Result [i] == ' ')
  362. {
  363. spaces++;
  364. }
  365. else
  366. {
  367. break;
  368. }
  369. }
  370. spaces += FieldLength;
  371. string trimmedText = e.Result [..spaces];
  372. spaces -= FieldLength;
  373. trimmedText = trimmedText.Replace (new string (' ', spaces), " ");
  374. if (trimmedText != e.Result)
  375. {
  376. e.Result = trimmedText;
  377. }
  378. if (!TimeSpan.TryParseExact (
  379. e.Result.Trim (),
  380. Format.Trim (),
  381. CultureInfo.CurrentCulture,
  382. TimeSpanStyles.None,
  383. out TimeSpan result
  384. ))
  385. {
  386. e.Handled = true;
  387. }
  388. AdjCursorPosition (CursorPosition);
  389. }
  390. catch (Exception)
  391. {
  392. e.Handled = true;
  393. }
  394. }
  395. }