TimeField.cs 9.6 KB

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