509 lines
16 KiB
C#
509 lines
16 KiB
C#
|
|
|
|
|
|
// This Source Code is partially based on the source code provided by the .NET Foundation.
|
|
|
|
// TODO: Mask (with placeholder); Clipboard paste;
|
|
// TODO: Constant decimals when formatting. Although this can actually be done with NumberFormatter.
|
|
// TODO: Disable expression by default
|
|
// TODO: Lock to digit characters only by property
|
|
//
|
|
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Data;
|
|
using System.Windows.Input;
|
|
|
|
// ReSharper disable once CheckNamespace
|
|
namespace WPFluent.Controls;
|
|
|
|
/// <summary>
|
|
/// Represents a control that can be used to display and edit numbers.
|
|
/// </summary>
|
|
[TemplatePart(Name = PART_ClearButton, Type = typeof(Button))]
|
|
[TemplatePart(Name = PART_InlineIncrementButton, Type = typeof(RepeatButton))]
|
|
[TemplatePart(Name = PART_InlineDecrementButton, Type = typeof(RepeatButton))]
|
|
public partial class NumberBox : TextBox
|
|
{
|
|
// Template part names
|
|
private const string PART_ClearButton = nameof(PART_ClearButton);
|
|
private const string PART_InlineDecrementButton = nameof(PART_InlineDecrementButton);
|
|
private const string PART_InlineIncrementButton = nameof(PART_InlineIncrementButton);
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="AcceptsExpression"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty AcceptsExpressionProperty = DependencyProperty.Register(
|
|
nameof(AcceptsExpression),
|
|
typeof(bool),
|
|
typeof(NumberBox),
|
|
new PropertyMetadata(true));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="LargeChange"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty LargeChangeProperty = DependencyProperty.Register(
|
|
nameof(LargeChange),
|
|
typeof(double),
|
|
typeof(NumberBox),
|
|
new PropertyMetadata(10.0d));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="MaxDecimalPlaces"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty MaxDecimalPlacesProperty = DependencyProperty.Register(
|
|
nameof(MaxDecimalPlaces),
|
|
typeof(int),
|
|
typeof(NumberBox),
|
|
new PropertyMetadata(6));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="Maximum"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(
|
|
nameof(Maximum),
|
|
typeof(double),
|
|
typeof(NumberBox),
|
|
new PropertyMetadata(double.MaxValue));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="Minimum"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(
|
|
nameof(Minimum),
|
|
typeof(double),
|
|
typeof(NumberBox),
|
|
new PropertyMetadata(double.MinValue));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="NumberFormatter"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty NumberFormatterProperty = DependencyProperty.Register(
|
|
nameof(NumberFormatter),
|
|
typeof(INumberFormatter),
|
|
typeof(NumberBox),
|
|
new PropertyMetadata(null, OnNumberFormatterChanged));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="SmallChange"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty SmallChangeProperty = DependencyProperty.Register(
|
|
nameof(SmallChange),
|
|
typeof(double),
|
|
typeof(NumberBox),
|
|
new PropertyMetadata(1.0d));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="SpinButtonPlacementMode"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty SpinButtonPlacementModeProperty = DependencyProperty.Register(
|
|
nameof(SpinButtonPlacementMode),
|
|
typeof(NumberBoxSpinButtonPlacementMode),
|
|
typeof(NumberBox),
|
|
new PropertyMetadata(NumberBoxSpinButtonPlacementMode.Inline));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="ValidationMode"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty ValidationModeProperty = DependencyProperty.Register(
|
|
nameof(ValidationMode),
|
|
typeof(NumberBoxValidationMode),
|
|
typeof(NumberBox),
|
|
new PropertyMetadata(NumberBoxValidationMode.InvalidInputOverwritten));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="ValueChanged"/> routed event.
|
|
/// </summary>
|
|
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
|
|
nameof(ValueChanged),
|
|
RoutingStrategy.Bubble,
|
|
typeof(NumberBoxValueChangedEvent),
|
|
typeof(NumberBox));
|
|
|
|
/// <summary>
|
|
/// Identifies the <see cref="Value"/> dependency property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
|
|
nameof(Value),
|
|
typeof(double?),
|
|
typeof(NumberBox),
|
|
new FrameworkPropertyMetadata(
|
|
null,
|
|
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
|
OnValueChanged,
|
|
null,
|
|
false,
|
|
UpdateSourceTrigger.LostFocus));
|
|
|
|
private bool _valueUpdating;
|
|
|
|
static NumberBox()
|
|
{
|
|
AcceptsReturnProperty.OverrideMetadata(typeof(NumberBox), new FrameworkPropertyMetadata(false));
|
|
MaxLinesProperty.OverrideMetadata(typeof(NumberBox), new FrameworkPropertyMetadata(1));
|
|
MinLinesProperty.OverrideMetadata(typeof(NumberBox), new FrameworkPropertyMetadata(1));
|
|
}
|
|
|
|
public NumberBox() : base()
|
|
{
|
|
NumberFormatter ??= NumberBox.GetRegionalSettingsAwareDecimalFormatter();
|
|
|
|
DataObject.AddPastingHandler(this, OnClipboardPaste);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs after the user triggers evaluation of new input by pressing the Enter key, clicking a spin button, or by
|
|
/// changing focus.
|
|
/// </summary>
|
|
public event NumberBoxValueChangedEvent ValueChanged
|
|
{
|
|
add => AddHandler(ValueChangedEvent, value);
|
|
remove => RemoveHandler(ValueChangedEvent, value);
|
|
}
|
|
|
|
private static INumberFormatter GetRegionalSettingsAwareDecimalFormatter() { return new ValidateNumberFormatter(); }
|
|
|
|
private void MoveCaretToTextEnd() { CaretIndex = Text.Length; }
|
|
|
|
private static void OnNumberFormatterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if(e.NewValue is not INumberParser)
|
|
{
|
|
throw new InvalidOperationException($"{nameof(NumberFormatter)} must implement {typeof(INumberParser)}");
|
|
}
|
|
}
|
|
|
|
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if(d is NumberBox numberBox)
|
|
{
|
|
numberBox.OnValueChanged(d, (double?)e.OldValue);
|
|
}
|
|
}
|
|
|
|
private void StepValue(double? change)
|
|
{
|
|
Debug.InfoWriteLine($"{typeof(NumberBox)} {nameof(StepValue)} raised, change {change}");
|
|
|
|
// Before adjusting the value, validate the contents of the textbox so we don't override it.
|
|
ValidateInput();
|
|
|
|
var newValue = Value ?? 0;
|
|
|
|
if(change is not null)
|
|
{
|
|
newValue += change ?? 0d;
|
|
}
|
|
|
|
SetCurrentValue(ValueProperty, newValue);
|
|
|
|
MoveCaretToTextEnd();
|
|
}
|
|
|
|
private void SubscribeToButtonClickEvent<TButton>(string elementName, Action action) where TButton : ButtonBase
|
|
{
|
|
if(GetTemplateChild(elementName) is TButton button)
|
|
{
|
|
button.Click += (s, e) =>
|
|
{
|
|
Debug.InfoWriteLineForButtonClick(s);
|
|
action();
|
|
};
|
|
}
|
|
}
|
|
|
|
private void UpdateTextToValue()
|
|
{
|
|
var newText = string.Empty;
|
|
|
|
if(Value is not null && NumberFormatter is not null)
|
|
{
|
|
newText = NumberFormatter.FormatDouble(Math.Round((double)Value, MaxDecimalPlaces));
|
|
}
|
|
|
|
SetCurrentValue(TextProperty, newText);
|
|
}
|
|
|
|
private void UpdateValueToText() { ValidateInput(); }
|
|
|
|
private void ValidateInput()
|
|
{
|
|
var text = Text.Trim();
|
|
|
|
if(string.IsNullOrEmpty(text))
|
|
{
|
|
SetCurrentValue(ValueProperty, null);
|
|
|
|
return;
|
|
}
|
|
|
|
var numberParser = NumberFormatter as INumberParser;
|
|
var value = numberParser!.ParseDouble(text);
|
|
|
|
if(value is null || Equals(Value, value))
|
|
{
|
|
UpdateTextToValue();
|
|
|
|
return;
|
|
}
|
|
|
|
if(value > Maximum)
|
|
{
|
|
value = Maximum;
|
|
}
|
|
|
|
if(value < Minimum)
|
|
{
|
|
value = Minimum;
|
|
}
|
|
|
|
SetCurrentValue(ValueProperty, value);
|
|
|
|
UpdateTextToValue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is called when something is pasted in this <see cref="NumberBox"/>.
|
|
/// </summary>
|
|
protected virtual void OnClipboardPaste(object sender, DataObjectPastingEventArgs e)
|
|
{
|
|
// TODO: Fix clipboard
|
|
if(sender is not NumberBox)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ValidateInput();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void OnLostFocus(RoutedEventArgs e)
|
|
{
|
|
base.OnLostFocus(e);
|
|
|
|
ValidateInput();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void OnPreviewKeyDown(KeyEventArgs e)
|
|
{
|
|
if(IsReadOnly)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch(e.Key)
|
|
{
|
|
case Key.PageUp:
|
|
StepValue(LargeChange);
|
|
e.Handled = true;
|
|
break;
|
|
case Key.PageDown:
|
|
StepValue(-LargeChange);
|
|
e.Handled = true;
|
|
break;
|
|
case Key.Up:
|
|
StepValue(SmallChange);
|
|
e.Handled = true;
|
|
break;
|
|
case Key.Down:
|
|
StepValue(-SmallChange);
|
|
e.Handled = true;
|
|
break;
|
|
}
|
|
|
|
base.OnPreviewKeyDown(e);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void OnPreviewKeyUp(KeyEventArgs e)
|
|
{
|
|
switch(e.Key)
|
|
{
|
|
case Key.Enter:
|
|
if(TextWrapping != TextWrapping.Wrap)
|
|
{
|
|
ValidateInput();
|
|
MoveCaretToTextEnd();
|
|
}
|
|
|
|
e.Handled = true;
|
|
break;
|
|
|
|
case Key.Escape:
|
|
UpdateTextToValue();
|
|
e.Handled = true;
|
|
break;
|
|
}
|
|
|
|
base.OnPreviewKeyUp(e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is called when <see cref="Value"/> in this <see cref="NumberBox"/> changes.
|
|
/// </summary>
|
|
protected virtual void OnValueChanged(DependencyObject d, double? oldValue)
|
|
{
|
|
if(_valueUpdating)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_valueUpdating = true;
|
|
|
|
var newValue = Value;
|
|
|
|
if(newValue > Maximum)
|
|
{
|
|
SetCurrentValue(ValueProperty, Maximum);
|
|
}
|
|
|
|
if(newValue < Minimum)
|
|
{
|
|
SetCurrentValue(ValueProperty, Minimum);
|
|
}
|
|
|
|
if(!Equals(newValue, oldValue))
|
|
{
|
|
RaiseEvent(new NumberBoxValueChangedEventArgs(oldValue, newValue, this));
|
|
}
|
|
|
|
UpdateTextToValue();
|
|
|
|
_valueUpdating = false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void OnApplyTemplate()
|
|
{
|
|
SubscribeToButtonClickEvent<System.Windows.Controls.Button>(PART_ClearButton, () => OnClearButtonClick());
|
|
SubscribeToButtonClickEvent<RepeatButton>(PART_InlineIncrementButton, () => StepValue(SmallChange));
|
|
SubscribeToButtonClickEvent<RepeatButton>(PART_InlineDecrementButton, () => StepValue(-SmallChange));
|
|
|
|
// If Text has been set, but Value hasn't, update Value based on Text.
|
|
if(string.IsNullOrEmpty(Text) && Value != null)
|
|
{
|
|
UpdateValueToText();
|
|
}
|
|
else
|
|
{
|
|
UpdateTextToValue();
|
|
}
|
|
|
|
base.OnApplyTemplate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the control will accept and evaluate a basic formulaic expression
|
|
/// entered as input.
|
|
/// </summary>
|
|
public bool AcceptsExpression
|
|
{
|
|
get => (bool)GetValue(AcceptsExpressionProperty);
|
|
set => SetValue(AcceptsExpressionProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the value that is added to or subtracted from <see cref="Value"/> when a large change is made, such
|
|
/// as with the PageUP and PageDown keys.
|
|
/// </summary>
|
|
public double LargeChange
|
|
{
|
|
get => (double)GetValue(LargeChangeProperty);
|
|
set => SetValue(LargeChangeProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the number of decimal places to be rounded when converting from Text to Value.
|
|
/// </summary>
|
|
public int MaxDecimalPlaces
|
|
{
|
|
get => (int)GetValue(MaxDecimalPlacesProperty);
|
|
set => SetValue(MaxDecimalPlacesProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the numerical maximum for <see cref="Value"/>.
|
|
/// </summary>
|
|
public double Maximum { get => (double)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the numerical minimum for <see cref="Value"/>.
|
|
/// </summary>
|
|
public double Minimum { get => (double)GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the number formatter.
|
|
/// </summary>
|
|
public INumberFormatter NumberFormatter
|
|
{
|
|
get => (INumberFormatter)GetValue(NumberFormatterProperty);
|
|
set => SetValue(NumberFormatterProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the value that is added to or subtracted from <see cref="Value"/> when a small change is made, such
|
|
/// as with an arrow key or scrolling.
|
|
/// </summary>
|
|
public double SmallChange
|
|
{
|
|
get => (double)GetValue(SmallChangeProperty);
|
|
set => SetValue(SmallChangeProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value that indicates the placement of buttons used to increment or decrement the <see
|
|
/// cref="Value"/> property.
|
|
/// </summary>
|
|
public NumberBoxSpinButtonPlacementMode SpinButtonPlacementMode
|
|
{
|
|
get => (NumberBoxSpinButtonPlacementMode)GetValue(SpinButtonPlacementModeProperty);
|
|
set => SetValue(SpinButtonPlacementModeProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the input validation behavior to invoke when invalid input is entered.
|
|
/// </summary>
|
|
public NumberBoxValidationMode ValidationMode
|
|
{
|
|
get => (NumberBoxValidationMode)GetValue(ValidationModeProperty);
|
|
set => SetValue(ValidationModeProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the numeric value of a <see cref="NumberBox"/>.
|
|
/// </summary>
|
|
public double? Value { get => (double?)GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
|
|
|
|
private static partial class Debug
|
|
{
|
|
public static partial void InfoWriteLine(string debugLine);
|
|
|
|
public static partial void InfoWriteLineForButtonClick(object sender);
|
|
|
|
#if DEBUG
|
|
public static partial void InfoWriteLine(string debugLine)
|
|
{ System.Diagnostics.Debug.WriteLine($"INFO: {debugLine}", "WPFluent.NumberBox"); }
|
|
|
|
public static partial void InfoWriteLineForButtonClick(object sender)
|
|
{
|
|
var buttonName =
|
|
sender is ButtonBase element ? element.Name : throw new InvalidCastException(nameof(sender));
|
|
|
|
InfoWriteLine($"{typeof(NumberBox)} {buttonName} clicked");
|
|
}
|
|
|
|
#else
|
|
public static partial void InfoWriteLine(string debugLine)
|
|
{
|
|
// Do nothing in non-DEBUG builds
|
|
}
|
|
|
|
public static partial void InfoWriteLineForButtonClick(object sender)
|
|
{
|
|
// Do nothing in non-DEBUG builds
|
|
}
|
|
|
|
#endif // DEBUG
|
|
}
|
|
}
|