2
0

NumericUpDown.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. #nullable enable
  2. using System.ComponentModel;
  3. using System.Numerics;
  4. namespace Terminal.Gui.Views;
  5. /// <summary>
  6. /// Enables the user to increase or decrease a value with the mouse or keyboard in type-safe way.
  7. /// </summary>
  8. /// <remarks>
  9. /// Supports the following types: <see cref="int"/>, <see cref="long"/>, <see cref="double"/>, <see cref="double"/>,
  10. /// <see cref="decimal"/>. Attempting to use any other type will result in an <see cref="InvalidOperationException"/>.
  11. /// </remarks>
  12. public class NumericUpDown<T> : View where T : notnull
  13. {
  14. private readonly Button _down;
  15. // TODO: Use a TextField instead of a Label
  16. private readonly View _number;
  17. private readonly Button _up;
  18. /// <summary>
  19. /// Initializes a new instance of the <see cref="NumericUpDown{T}"/> class.
  20. /// </summary>
  21. /// <exception cref="InvalidOperationException"></exception>
  22. public NumericUpDown ()
  23. {
  24. Type type = typeof (T);
  25. if (!(type == typeof (object) || NumericHelper.SupportsType (type)))
  26. {
  27. throw new InvalidOperationException ("T must be a numeric type that supports addition and subtraction.");
  28. }
  29. // `object` is supported only for AllViewsTester
  30. if (type != typeof (object))
  31. {
  32. if (NumericHelper.TryGetHelper (typeof (T), out INumericHelper? helper))
  33. {
  34. Increment = (T)helper!.One;
  35. Value = (T)helper!.Zero;
  36. }
  37. }
  38. Width = Dim.Auto (DimAutoStyle.Content);
  39. Height = Dim.Auto (DimAutoStyle.Content);
  40. _down = new ()
  41. {
  42. Height = 1,
  43. Width = 1,
  44. NoPadding = true,
  45. NoDecorations = true,
  46. Title = $"{Glyphs.DownArrow}",
  47. WantContinuousButtonPressed = true,
  48. CanFocus = false,
  49. ShadowStyle = ShadowStyle.None,
  50. };
  51. _number = new ()
  52. {
  53. Text = Value?.ToString () ?? "Err",
  54. X = Pos.Right (_down),
  55. Y = Pos.Top (_down),
  56. Width = Dim.Auto (minimumContentDim: Dim.Func (_ => string.Format (Format, Value).GetColumns())),
  57. Height = 1,
  58. TextAlignment = Alignment.Center,
  59. CanFocus = true,
  60. };
  61. _up = new ()
  62. {
  63. X = Pos.Right (_number),
  64. Y = Pos.Top (_number),
  65. Height = 1,
  66. Width = 1,
  67. NoPadding = true,
  68. NoDecorations = true,
  69. Title = $"{Glyphs.UpArrow}",
  70. WantContinuousButtonPressed = true,
  71. CanFocus = false,
  72. ShadowStyle = ShadowStyle.None,
  73. };
  74. CanFocus = true;
  75. _down.Accepting += OnDownButtonOnAccept;
  76. _up.Accepting += OnUpButtonOnAccept;
  77. Add (_down, _number, _up);
  78. AddCommand (
  79. Command.Up,
  80. (ctx) =>
  81. {
  82. if (type == typeof (object))
  83. {
  84. return false;
  85. }
  86. // BUGBUG: If this is uncommented, the numericupdown in a shortcut will not work
  87. //if (RaiseSelecting (ctx) is true)
  88. //{
  89. // return true;
  90. //}
  91. if (Value is { } v && Increment is { } i && NumericHelper.TryGetHelper (typeof (T), out INumericHelper? helper))
  92. {
  93. Value = (T)helper!.Add (v, i);
  94. }
  95. return true;
  96. });
  97. AddCommand (
  98. Command.Down,
  99. (ctx) =>
  100. {
  101. if (type == typeof (object))
  102. {
  103. return false;
  104. }
  105. // BUGBUG: If this is uncommented, the numericupdown in a shortcut will not work
  106. //if (RaiseSelecting (ctx) is true)
  107. //{
  108. // return true;
  109. //}
  110. if (Value is { } v && Increment is { } i && NumericHelper.TryGetHelper (typeof (T), out INumericHelper? helper))
  111. {
  112. Value = (T)helper!.Subtract (v, i);
  113. }
  114. return true;
  115. });
  116. KeyBindings.Add (Key.CursorUp, Command.Up);
  117. KeyBindings.Add (Key.CursorDown, Command.Down);
  118. SetText ();
  119. return;
  120. void OnDownButtonOnAccept (object? s, CommandEventArgs e)
  121. {
  122. InvokeCommand (Command.Down);
  123. e.Handled = true;
  124. }
  125. void OnUpButtonOnAccept (object? s, CommandEventArgs e)
  126. {
  127. InvokeCommand (Command.Up);
  128. e.Handled = true;
  129. }
  130. }
  131. private T _value = default!;
  132. /// <summary>
  133. /// Gets or sets the value that will be incremented or decremented.
  134. /// </summary>
  135. /// <remarks>
  136. /// <para>
  137. /// <see cref="ValueChanging"/> and <see cref="ValueChanged"/> events are raised when the value changes.
  138. /// The <see cref="ValueChanging"/> event can be canceled the change setting
  139. /// <see cref="HandledEventArgs"/><c>.Handled</c> to <see langword="true"/>.
  140. /// </para>
  141. /// </remarks>
  142. public T Value
  143. {
  144. get => _value;
  145. set
  146. {
  147. if (_value.Equals (value))
  148. {
  149. return;
  150. }
  151. T oldValue = value;
  152. CancelEventArgs<T> args = new (in _value, ref value);
  153. ValueChanging?.Invoke (this, args);
  154. if (args.Cancel)
  155. {
  156. return;
  157. }
  158. _value = value;
  159. SetText ();
  160. ValueChanged?.Invoke (this, new (in value));
  161. }
  162. }
  163. /// <summary>
  164. /// Raised when the value is about to change. Set <see cref="CancelEventArgs{T}"/><c>.Cancel</c> to true to prevent the
  165. /// change.
  166. /// </summary>
  167. public event EventHandler<CancelEventArgs<T>>? ValueChanging;
  168. /// <summary>
  169. /// Raised when the value has changed.
  170. /// </summary>
  171. public event EventHandler<EventArgs<T>>? ValueChanged;
  172. private string _format = "{0}";
  173. /// <summary>
  174. /// Gets or sets the format string used to display the value. The default is "{0}".
  175. /// </summary>
  176. public string Format
  177. {
  178. get => _format;
  179. set
  180. {
  181. if (_format == value)
  182. {
  183. return;
  184. }
  185. _format = value;
  186. FormatChanged?.Invoke (this, new (value));
  187. SetText ();
  188. }
  189. }
  190. /// <summary>
  191. /// Raised when <see cref="Format"/> has changed.
  192. /// </summary>
  193. public event EventHandler<EventArgs<string>>? FormatChanged;
  194. private void SetText ()
  195. {
  196. _number.Text = string.Format (Format, _value);
  197. Text = _number.Text;
  198. }
  199. private T? _increment;
  200. /// <summary>
  201. /// </summary>
  202. public T? Increment
  203. {
  204. get => _increment;
  205. set
  206. {
  207. if (_increment is { } oldVal && value is { } newVal && oldVal.Equals (newVal))
  208. {
  209. return;
  210. }
  211. _increment = value;
  212. IncrementChanged?.Invoke (this, new (value!));
  213. }
  214. }
  215. /// <summary>
  216. /// Raised when <see cref="Increment"/> has changed.
  217. /// </summary>
  218. public event EventHandler<EventArgs<T>>? IncrementChanged;
  219. // Prevent the drawing of Text
  220. /// <inheritdoc />
  221. protected override bool OnDrawingText () { return true; }
  222. /// <summary>
  223. /// Attempts to convert the specified <paramref name="value"/> to type <typeparamref name="TValue"/>.
  224. /// </summary>
  225. /// <typeparam name="TValue">The type to which the value should be converted.</typeparam>
  226. /// <param name="value">The value to convert.</param>
  227. /// <param name="result">
  228. /// When this method returns, contains the converted value if the conversion succeeded,
  229. /// or the default value of <typeparamref name="TValue"/> if the conversion failed.
  230. /// </param>
  231. /// <returns>
  232. /// <c>true</c> if the conversion was successful; otherwise, <c>false</c>.
  233. /// </returns>
  234. public static bool TryConvert<TValue> (object value, out TValue? result)
  235. {
  236. try
  237. {
  238. result = (TValue)Convert.ChangeType (value, typeof (TValue));
  239. return true;
  240. }
  241. catch
  242. {
  243. result = default (TValue);
  244. return false;
  245. }
  246. }
  247. }
  248. /// <summary>
  249. /// Enables the user to increase or decrease an <see langword="int"/> by clicking on the up or down buttons.
  250. /// </summary>
  251. public class NumericUpDown : NumericUpDown<int>
  252. { }
  253. internal interface INumericHelper
  254. {
  255. object One { get; }
  256. object Zero { get; }
  257. object Add (object a, object b);
  258. object Subtract (object a, object b);
  259. }
  260. internal static class NumericHelper
  261. {
  262. private static readonly Dictionary<Type, INumericHelper> _helpers = new ();
  263. static NumericHelper ()
  264. {
  265. // Register known INumber<T> types
  266. Register<int> ();
  267. Register<long> ();
  268. Register<float> ();
  269. Register<double> ();
  270. Register<decimal> ();
  271. // Add more as needed
  272. }
  273. private static void Register<T> () where T : INumber<T>
  274. {
  275. _helpers [typeof (T)] = new NumericHelperImpl<T> ();
  276. }
  277. public static bool TryGetHelper (Type t, out INumericHelper? helper)
  278. => _helpers.TryGetValue (t, out helper);
  279. private class NumericHelperImpl<T> : INumericHelper where T : INumber<T>
  280. {
  281. public object One => T.One;
  282. public object Zero => T.Zero;
  283. public object Add (object a, object b) => (T)a + (T)b;
  284. public object Subtract (object a, object b) => (T)a - (T)b;
  285. }
  286. public static bool SupportsType (Type type) => _helpers.ContainsKey (type);
  287. }