| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- using System.ComponentModel;
- using System.Numerics;
- namespace Terminal.Gui.Views;
- /// <summary>
- /// Enables the user to increase or decrease a value with the mouse or keyboard in type-safe way.
- /// </summary>
- /// <remarks>
- /// Supports the following types: <see cref="int"/>, <see cref="long"/>, <see cref="double"/>, <see cref="double"/>,
- /// <see cref="decimal"/>. Attempting to use any other type will result in an <see cref="InvalidOperationException"/>.
- /// </remarks>
- public class NumericUpDown<T> : View where T : notnull
- {
- private readonly Button _down;
- // TODO: Use a TextField instead of a Label
- private readonly View _number;
- private readonly Button _up;
- /// <summary>
- /// Initializes a new instance of the <see cref="NumericUpDown{T}"/> class.
- /// </summary>
- /// <exception cref="InvalidOperationException"></exception>
- public NumericUpDown ()
- {
- Type type = typeof (T);
- if (!(type == typeof (object) || NumericHelper.SupportsType (type)))
- {
- throw new InvalidOperationException ("T must be a numeric type that supports addition and subtraction.");
- }
- // `object` is supported only for AllViewsTester
- if (type != typeof (object))
- {
- if (NumericHelper.TryGetHelper (typeof (T), out INumericHelper? helper))
- {
- Increment = (T)helper!.One;
- Value = (T)helper!.Zero;
- }
- }
- Width = Dim.Auto (DimAutoStyle.Content);
- Height = Dim.Auto (DimAutoStyle.Content);
- _down = new ()
- {
- Height = 1,
- Width = 1,
- NoPadding = true,
- NoDecorations = true,
- Title = $"{Glyphs.DownArrow}",
- WantContinuousButtonPressed = true,
- CanFocus = false,
- ShadowStyle = ShadowStyle.None,
- };
- _number = new ()
- {
- Text = Value?.ToString () ?? "Err",
- X = Pos.Right (_down),
- Y = Pos.Top (_down),
- Width = Dim.Auto (minimumContentDim: Dim.Func (_ => string.Format (Format, Value).GetColumns())),
- Height = 1,
- TextAlignment = Alignment.Center,
- CanFocus = true,
- };
- _up = new ()
- {
- X = Pos.Right (_number),
- Y = Pos.Top (_number),
- Height = 1,
- Width = 1,
- NoPadding = true,
- NoDecorations = true,
- Title = $"{Glyphs.UpArrow}",
- WantContinuousButtonPressed = true,
- CanFocus = false,
- ShadowStyle = ShadowStyle.None,
- };
- CanFocus = true;
- _down.Accepting += OnDownButtonOnAccept;
- _up.Accepting += OnUpButtonOnAccept;
- Add (_down, _number, _up);
- AddCommand (
- Command.Up,
- (ctx) =>
- {
- if (type == typeof (object))
- {
- return false;
- }
- // BUGBUG: If this is uncommented, the numericupdown in a shortcut will not work
- //if (RaiseSelecting (ctx) is true)
- //{
- // return true;
- //}
- if (Value is { } v && Increment is { } i && NumericHelper.TryGetHelper (typeof (T), out INumericHelper? helper))
- {
- Value = (T)helper!.Add (v, i);
- }
- return true;
- });
- AddCommand (
- Command.Down,
- (ctx) =>
- {
- if (type == typeof (object))
- {
- return false;
- }
- // BUGBUG: If this is uncommented, the numericupdown in a shortcut will not work
- //if (RaiseSelecting (ctx) is true)
- //{
- // return true;
- //}
- if (Value is { } v && Increment is { } i && NumericHelper.TryGetHelper (typeof (T), out INumericHelper? helper))
- {
- Value = (T)helper!.Subtract (v, i);
- }
- return true;
- });
- KeyBindings.Add (Key.CursorUp, Command.Up);
- KeyBindings.Add (Key.CursorDown, Command.Down);
- SetText ();
- return;
- void OnDownButtonOnAccept (object? s, CommandEventArgs e)
- {
- InvokeCommand (Command.Down);
- e.Handled = true;
- }
- void OnUpButtonOnAccept (object? s, CommandEventArgs e)
- {
- InvokeCommand (Command.Up);
- e.Handled = true;
- }
- }
- private T _value = default!;
- /// <summary>
- /// Gets or sets the value that will be incremented or decremented.
- /// </summary>
- /// <remarks>
- /// <para>
- /// <see cref="ValueChanging"/> and <see cref="ValueChanged"/> events are raised when the value changes.
- /// The <see cref="ValueChanging"/> event can be canceled the change setting
- /// <see cref="HandledEventArgs"/><c>.Handled</c> to <see langword="true"/>.
- /// </para>
- /// </remarks>
- public T Value
- {
- get => _value;
- set
- {
- if (_value.Equals (value))
- {
- return;
- }
- T oldValue = value;
- CancelEventArgs<T> args = new (in _value, ref value);
- ValueChanging?.Invoke (this, args);
- if (args.Cancel)
- {
- return;
- }
- _value = value;
- SetText ();
- ValueChanged?.Invoke (this, new (in value));
- }
- }
- /// <summary>
- /// Raised when the value is about to change. Set <see cref="CancelEventArgs{T}"/><c>.Cancel</c> to true to prevent the
- /// change.
- /// </summary>
- public event EventHandler<CancelEventArgs<T>>? ValueChanging;
- /// <summary>
- /// Raised when the value has changed.
- /// </summary>
- public event EventHandler<EventArgs<T>>? ValueChanged;
- private string _format = "{0}";
- /// <summary>
- /// Gets or sets the format string used to display the value. The default is "{0}".
- /// </summary>
- public string Format
- {
- get => _format;
- set
- {
- if (_format == value)
- {
- return;
- }
- _format = value;
- FormatChanged?.Invoke (this, new (value));
- SetText ();
- }
- }
- /// <summary>
- /// Raised when <see cref="Format"/> has changed.
- /// </summary>
- public event EventHandler<EventArgs<string>>? FormatChanged;
- private void SetText ()
- {
- _number.Text = string.Format (Format, _value);
- Text = _number.Text;
- }
- private T? _increment;
- /// <summary>
- /// </summary>
- public T? Increment
- {
- get => _increment;
- set
- {
- if (_increment is { } oldVal && value is { } newVal && oldVal.Equals (newVal))
- {
- return;
- }
- _increment = value;
- IncrementChanged?.Invoke (this, new (value!));
- }
- }
- /// <summary>
- /// Raised when <see cref="Increment"/> has changed.
- /// </summary>
- public event EventHandler<EventArgs<T>>? IncrementChanged;
- // Prevent the drawing of Text
- /// <inheritdoc />
- protected override bool OnDrawingText () { return true; }
- /// <summary>
- /// Attempts to convert the specified <paramref name="value"/> to type <typeparamref name="TValue"/>.
- /// </summary>
- /// <typeparam name="TValue">The type to which the value should be converted.</typeparam>
- /// <param name="value">The value to convert.</param>
- /// <param name="result">
- /// When this method returns, contains the converted value if the conversion succeeded,
- /// or the default value of <typeparamref name="TValue"/> if the conversion failed.
- /// </param>
- /// <returns>
- /// <c>true</c> if the conversion was successful; otherwise, <c>false</c>.
- /// </returns>
- public static bool TryConvert<TValue> (object value, out TValue? result)
- {
- try
- {
- result = (TValue)Convert.ChangeType (value, typeof (TValue));
- return true;
- }
- catch
- {
- result = default (TValue);
- return false;
- }
- }
- }
- /// <summary>
- /// Enables the user to increase or decrease an <see langword="int"/> by clicking on the up or down buttons.
- /// </summary>
- public class NumericUpDown : NumericUpDown<int>
- { }
- internal interface INumericHelper
- {
- object One { get; }
- object Zero { get; }
- object Add (object a, object b);
- object Subtract (object a, object b);
- }
- internal static class NumericHelper
- {
- private static readonly Dictionary<Type, INumericHelper> _helpers = new ();
- static NumericHelper ()
- {
- // Register known INumber<T> types
- Register<int> ();
- Register<long> ();
- Register<float> ();
- Register<double> ();
- Register<decimal> ();
- // Add more as needed
- }
- private static void Register<T> () where T : INumber<T>
- {
- _helpers [typeof (T)] = new NumericHelperImpl<T> ();
- }
- public static bool TryGetHelper (Type t, out INumericHelper? helper)
- => _helpers.TryGetValue (t, out helper);
- private class NumericHelperImpl<T> : INumericHelper where T : INumber<T>
- {
- public object One => T.One;
- public object Zero => T.Zero;
- public object Add (object a, object b) => (T)a + (T)b;
- public object Subtract (object a, object b) => (T)a - (T)b;
- }
- public static bool SupportsType (Type type) => _helpers.ContainsKey (type);
- }
|