NumericUpDown.cs 9.9 KB

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