TimeField.cs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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 NStack;
  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. Initialize (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. Initialize (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 Initialize (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. TextChanged += TextField_TextChanged;
  72. // Things this view knows how to do
  73. AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; });
  74. AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (); 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. AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight);
  81. AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight);
  82. AddKeyBinding (Key.Delete, Command.DeleteCharLeft);
  83. AddKeyBinding (Key.Backspace, Command.DeleteCharLeft);
  84. AddKeyBinding (Key.Home, Command.LeftHome);
  85. AddKeyBinding (Key.A | Key.CtrlMask, Command.LeftHome);
  86. AddKeyBinding (Key.CursorLeft, Command.Left);
  87. AddKeyBinding (Key.B | Key.CtrlMask, Command.Left);
  88. AddKeyBinding (Key.End, Command.RightEnd);
  89. AddKeyBinding (Key.E | Key.CtrlMask, Command.RightEnd);
  90. AddKeyBinding (Key.CursorRight, Command.Right);
  91. AddKeyBinding (Key.F | Key.CtrlMask, Command.Right);
  92. }
  93. void TextField_TextChanged (object sender, TextChangedEventArgs e)
  94. {
  95. try {
  96. if (!TimeSpan.TryParseExact (Text.ToString ().Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result))
  97. Text = e.OldValue;
  98. } catch (Exception) {
  99. Text = e.OldValue;
  100. }
  101. }
  102. /// <summary>
  103. /// Gets or sets the time of the <see cref="TimeField"/>.
  104. /// </summary>
  105. /// <remarks>
  106. /// </remarks>
  107. public TimeSpan Time {
  108. get {
  109. return time;
  110. }
  111. set {
  112. if (ReadOnly)
  113. return;
  114. var oldTime = time;
  115. time = value;
  116. this.Text = " " + value.ToString (format.Trim ());
  117. var args = new DateTimeEventArgs<TimeSpan> (oldTime, value, format);
  118. if (oldTime != value) {
  119. OnTimeChanged (args);
  120. }
  121. }
  122. }
  123. /// <summary>
  124. /// Get or sets whether <see cref="TimeField"/> uses the short or long time format.
  125. /// </summary>
  126. public bool IsShortFormat {
  127. get => isShort;
  128. set {
  129. isShort = value;
  130. if (isShort)
  131. Width = 7;
  132. else
  133. Width = 10;
  134. var ro = ReadOnly;
  135. if (ro)
  136. ReadOnly = false;
  137. SetText (Text);
  138. ReadOnly = ro;
  139. SetNeedsDisplay ();
  140. }
  141. }
  142. /// <inheritdoc/>
  143. public override int CursorPosition {
  144. get => base.CursorPosition;
  145. set {
  146. base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1);
  147. }
  148. }
  149. bool SetText (Rune key)
  150. {
  151. var text = TextModel.ToRunes (Text);
  152. var newText = text.GetRange (0, CursorPosition);
  153. newText.Add (key);
  154. if (CursorPosition < fieldLen)
  155. newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList ();
  156. return SetText (ustring.Make (newText));
  157. }
  158. bool SetText (ustring text)
  159. {
  160. if (text.IsEmpty) {
  161. return false;
  162. }
  163. ustring [] vals = text.Split (ustring.Make (sepChar));
  164. bool isValidTime = true;
  165. int hour = Int32.Parse (vals [0].ToString ());
  166. int minute = Int32.Parse (vals [1].ToString ());
  167. int second = isShort ? 0 : vals.Length > 2 ? Int32.Parse (vals [2].ToString ()) : 0;
  168. if (hour < 0) {
  169. isValidTime = false;
  170. hour = 0;
  171. vals [0] = "0";
  172. } else if (hour > 23) {
  173. isValidTime = false;
  174. hour = 23;
  175. vals [0] = "23";
  176. }
  177. if (minute < 0) {
  178. isValidTime = false;
  179. minute = 0;
  180. vals [1] = "0";
  181. } else if (minute > 59) {
  182. isValidTime = false;
  183. minute = 59;
  184. vals [1] = "59";
  185. }
  186. if (second < 0) {
  187. isValidTime = false;
  188. second = 0;
  189. vals [2] = "0";
  190. } else if (second > 59) {
  191. isValidTime = false;
  192. second = 59;
  193. vals [2] = "59";
  194. }
  195. string t = isShort ? $" {hour,2:00}{sepChar}{minute,2:00}" : $" {hour,2:00}{sepChar}{minute,2:00}{sepChar}{second,2:00}";
  196. if (!TimeSpan.TryParseExact (t.Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) ||
  197. !isValidTime)
  198. return false;
  199. Time = result;
  200. return true;
  201. }
  202. void IncCursorPosition ()
  203. {
  204. if (CursorPosition == fieldLen)
  205. return;
  206. if (Text [++CursorPosition] == sepChar.ToCharArray () [0])
  207. CursorPosition++;
  208. }
  209. void DecCursorPosition ()
  210. {
  211. if (CursorPosition == 1)
  212. return;
  213. if (Text [--CursorPosition] == sepChar.ToCharArray () [0])
  214. CursorPosition--;
  215. }
  216. void AdjCursorPosition ()
  217. {
  218. if (Text [CursorPosition] == sepChar.ToCharArray () [0])
  219. CursorPosition++;
  220. }
  221. ///<inheritdoc/>
  222. public override bool ProcessKey (KeyEvent kb)
  223. {
  224. var result = InvokeKeybindings (kb);
  225. if (result != null)
  226. return (bool)result;
  227. // Ignore non-numeric characters.
  228. if (kb.Key < (Key)((int)Key.D0) || kb.Key > (Key)((int)Key.D9))
  229. return false;
  230. if (ReadOnly)
  231. return true;
  232. if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ()))
  233. IncCursorPosition ();
  234. return true;
  235. }
  236. bool MoveRight ()
  237. {
  238. IncCursorPosition ();
  239. return true;
  240. }
  241. new bool MoveEnd ()
  242. {
  243. CursorPosition = fieldLen;
  244. return true;
  245. }
  246. bool MoveLeft ()
  247. {
  248. DecCursorPosition ();
  249. return true;
  250. }
  251. bool MoveHome ()
  252. {
  253. // Home, C-A
  254. CursorPosition = 1;
  255. return true;
  256. }
  257. /// <inheritdoc/>
  258. public override void DeleteCharLeft (bool useOldCursorPos = true)
  259. {
  260. if (ReadOnly)
  261. return;
  262. SetText ('0');
  263. DecCursorPosition ();
  264. return;
  265. }
  266. /// <inheritdoc/>
  267. public override void DeleteCharRight ()
  268. {
  269. if (ReadOnly)
  270. return;
  271. SetText ('0');
  272. return;
  273. }
  274. ///<inheritdoc/>
  275. public override bool MouseEvent (MouseEvent ev)
  276. {
  277. if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked))
  278. return false;
  279. if (!HasFocus)
  280. SetFocus ();
  281. var point = ev.X;
  282. if (point > fieldLen)
  283. point = fieldLen;
  284. if (point < 1)
  285. point = 1;
  286. CursorPosition = point;
  287. AdjCursorPosition ();
  288. return true;
  289. }
  290. /// <summary>
  291. /// Event firing method that invokes the <see cref="TimeChanged"/> event.
  292. /// </summary>
  293. /// <param name="args">The event arguments</param>
  294. public virtual void OnTimeChanged (DateTimeEventArgs<TimeSpan> args)
  295. {
  296. TimeChanged?.Invoke (this,args);
  297. }
  298. }
  299. }