using System.ComponentModel; using System.Numerics; namespace Terminal.Gui.Views; /// /// Enables the user to increase or decrease a value with the mouse or keyboard in type-safe way. /// /// /// Supports the following types: , , , , /// . Attempting to use any other type will result in an . /// public class NumericUpDown : View where T : notnull { private readonly Button _down; // TODO: Use a TextField instead of a Label private readonly View _number; private readonly Button _up; /// /// Initializes a new instance of the class. /// /// 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 (RaiseActivating (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 (RaiseActivating (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!; /// /// Gets or sets the value that will be incremented or decremented. /// /// /// /// and events are raised when the value changes. /// The event can be canceled the change setting /// .Handled to . /// /// public T Value { get => _value; set { if (_value.Equals (value)) { return; } T oldValue = value; CancelEventArgs args = new (in _value, ref value); ValueChanging?.Invoke (this, args); if (args.Cancel) { return; } _value = value; SetText (); ValueChanged?.Invoke (this, new (in value)); } } /// /// Raised when the value is about to change. Set .Cancel to true to prevent the /// change. /// public event EventHandler>? ValueChanging; /// /// Raised when the value has changed. /// public event EventHandler>? ValueChanged; private string _format = "{0}"; /// /// Gets or sets the format string used to display the value. The default is "{0}". /// public string Format { get => _format; set { if (_format == value) { return; } _format = value; FormatChanged?.Invoke (this, new (value)); SetText (); } } /// /// Raised when has changed. /// public event EventHandler>? FormatChanged; private void SetText () { _number.Text = string.Format (Format, _value); Text = _number.Text; } private T? _increment; /// /// public T? Increment { get => _increment; set { if (_increment is { } oldVal && value is { } newVal && oldVal.Equals (newVal)) { return; } _increment = value; IncrementChanged?.Invoke (this, new (value!)); } } /// /// Raised when has changed. /// public event EventHandler>? IncrementChanged; // Prevent the drawing of Text /// protected override bool OnDrawingText () { return true; } /// /// Attempts to convert the specified to type . /// /// The type to which the value should be converted. /// The value to convert. /// /// When this method returns, contains the converted value if the conversion succeeded, /// or the default value of if the conversion failed. /// /// /// true if the conversion was successful; otherwise, false. /// public static bool TryConvert (object value, out TValue? result) { try { result = (TValue)Convert.ChangeType (value, typeof (TValue)); return true; } catch { result = default (TValue); return false; } } } /// /// Enables the user to increase or decrease an by clicking on the up or down buttons. /// public class NumericUpDown : NumericUpDown { } 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 _helpers = new (); static NumericHelper () { // Register known INumber types Register (); Register (); Register (); Register (); Register (); // Add more as needed } private static void Register () where T : INumber { _helpers [typeof (T)] = new NumericHelperImpl (); } public static bool TryGetHelper (Type t, out INumericHelper? helper) => _helpers.TryGetValue (t, out helper); private class NumericHelperImpl : INumericHelper where T : INumber { 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); }