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);
}