Merge pull request #66 from irihitech/numeric
New Control: NumericUpDown family
This commit is contained in:
@@ -15,6 +15,7 @@ public static class MenuKeys
|
||||
public const string MenuKeyLoading = "Loading";
|
||||
public const string MenuKeyMessageBox = "MessageBox";
|
||||
public const string MenuKeyNavigation = "Navigation";
|
||||
public const string MenuKeyNumericUpDown = "NumericUpDown";
|
||||
public const string MenuKeyPagination = "Pagination";
|
||||
public const string MenuKeyTagInput = "TagInput";
|
||||
public const string MenuKeyTimeline = "Timeline";
|
||||
|
||||
26
demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml
Normal file
26
demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<UserControl
|
||||
x:Class="Ursa.Demo.Pages.NumericUpDownDemo"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Styles>
|
||||
<Style Selector=":is(u|NumericUpDown)">
|
||||
<Setter Property="Width" Value="240"></Setter>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<StackPanel HorizontalAlignment="Left">
|
||||
<u:NumericIntUpDown Name="input" InnerLeftContent="Age" Step="1" Value="2" Watermark="Input Value" Classes="ClearButton" />
|
||||
<TextBlock Text="{Binding #input.Value}" ></TextBlock>
|
||||
<u:NumericDoubleUpDown Name="inputDouble" Step="0.5" Value="3.1" EmptyInputValue="1"></u:NumericDoubleUpDown>
|
||||
<TextBlock Text="{Binding #inputDouble.Value}"></TextBlock>
|
||||
<u:NumericByteUpDown Name="inputByte" Step="1" Value="3" EmptyInputValue="1"></u:NumericByteUpDown>
|
||||
<TextBlock Text="{Binding #inputByte.Value}"></TextBlock>
|
||||
<TextBlock Text="Drag"></TextBlock>
|
||||
<u:NumericIntUpDown Step="1" Value="2" Watermark="Input Value" AllowDrag="True" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
15
demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml.cs
Normal file
15
demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ursa.Demo.ViewModels;
|
||||
|
||||
namespace Ursa.Demo.Pages;
|
||||
|
||||
public partial class NumericUpDownDemo : UserControl
|
||||
{
|
||||
public NumericUpDownDemo()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new NumericUpDownDemoViewModel();
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ public class MainViewViewModel : ViewModelBase
|
||||
MenuKeys.MenuKeyLoading => new LoadingDemoViewModel(),
|
||||
MenuKeys.MenuKeyMessageBox => new MessageBoxDemoViewModel(),
|
||||
MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(),
|
||||
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
|
||||
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
|
||||
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
|
||||
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
|
||||
|
||||
@@ -24,6 +24,7 @@ public class MenuViewModel: ViewModelBase
|
||||
new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading },
|
||||
new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox },
|
||||
new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation },
|
||||
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown },
|
||||
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
|
||||
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
|
||||
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline },
|
||||
|
||||
8
demo/Ursa.Demo/ViewModels/NumericUpDownDemoViewModel.cs
Normal file
8
demo/Ursa.Demo/ViewModels/NumericUpDownDemoViewModel.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Ursa.Demo.ViewModels;
|
||||
|
||||
public class NumericUpDownDemoViewModel: ObservableObject
|
||||
{
|
||||
|
||||
}
|
||||
87
src/Ursa.Themes.Semi/Controls/NumericUpDown.axaml
Normal file
87
src/Ursa.Themes.Semi/Controls/NumericUpDown.axaml
Normal file
@@ -0,0 +1,87 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="https://irihi.tech/ursa">
|
||||
<!-- Add Resources Here -->
|
||||
|
||||
<ControlTheme x:Key="InputClearButton" TargetType="Button">
|
||||
<Setter Property="Button.Foreground" Value="{DynamicResource TextBoxButtonDefaultForeground}" />
|
||||
<Setter Property="Button.Cursor" Value="Hand" />
|
||||
<Setter Property="Button.Template">
|
||||
<ControlTemplate TargetType="Button">
|
||||
<!-- Background must be transparent or hit test will fail -->
|
||||
<ContentControl Background="Transparent">
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{DynamicResource TextBoxClearButtonData}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
</ContentControl>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^:pointerover">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBoxButtonPointeroverForeground}" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type u:NumericUpDown}" TargetType="{x:Type u:NumericUpDown}">
|
||||
<Setter Property="NumericUpDown.VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="NumericUpDown.CornerRadius" Value="{DynamicResource NumericUpDownCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="u:NumericUpDown">
|
||||
<DataValidationErrors>
|
||||
<ButtonSpinner
|
||||
Name="PART_Spinner"
|
||||
MinWidth="0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AllowSpin="{TemplateBinding AllowSpin}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
<Panel>
|
||||
<TextBox
|
||||
Name="PART_TextBox"
|
||||
Height="{TemplateBinding Height}"
|
||||
MinHeight="{DynamicResource NumericUpDownWrapperDefaultHeight}"
|
||||
AcceptsReturn="False"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
DataValidationErrors.Errors="{ReflectionBinding $parent[NumericUpDown].(DataValidationErrors.Errors)}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
IsReadOnly="{TemplateBinding IsReadOnly}"
|
||||
TextWrapping="NoWrap"
|
||||
InnerLeftContent="{TemplateBinding InnerLeftContent}"
|
||||
Theme="{DynamicResource NonErrorTextBox}"
|
||||
Watermark="{TemplateBinding Watermark}" />
|
||||
<Panel
|
||||
Name="PART_DragPanel"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
Cursor="SizeAll"
|
||||
IsVisible="{TemplateBinding AllowDrag}" />
|
||||
<Button
|
||||
Name="PART_ClearButton"
|
||||
Command="{Binding $parent[u:NumericUpDown].Clear}"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 0 8 0"
|
||||
IsVisible="False"
|
||||
Focusable="False"
|
||||
Theme="{StaticResource InputClearButton}" />
|
||||
</Panel>
|
||||
</ButtonSpinner>
|
||||
</DataValidationErrors>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Style Selector="^.clearButton, ^.ClearButton">
|
||||
<Style Selector="^[IsReadOnly=False]:focus /template/ Button#PART_ClearButton">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="^[IsReadOnly=False]:pointerover /template/ Button#PART_ClearButton">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -13,6 +13,7 @@
|
||||
<ResourceInclude Source="Loading.axaml" />
|
||||
<ResourceInclude Source="MessageBoxWindow.axaml" />
|
||||
<ResourceInclude Source="Navigation.axaml" />
|
||||
<ResourceInclude Source="NumericUpDown.axaml" />
|
||||
<ResourceInclude Source="Pagination.axaml" />
|
||||
<ResourceInclude Source="TagInput.axaml" />
|
||||
<ResourceInclude Source="Timeline.axaml" />
|
||||
|
||||
236
src/Ursa/Controls/NumericUpDown/IntUpDown.cs
Normal file
236
src/Ursa/Controls/NumericUpDown/IntUpDown.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Utilities;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class NumericIntUpDown : NumericUpDownBase<int>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericIntUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericIntUpDown>(int.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericIntUpDown>(int.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericIntUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out int number) =>
|
||||
int.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(int? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override int Zero => 0;
|
||||
|
||||
protected override int? Add(int? a, int? b) => a + b;
|
||||
|
||||
protected override int? Minus(int? a, int? b) => a - b;
|
||||
}
|
||||
|
||||
public class NumericDoubleUpDown : NumericUpDownBase<double>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericDoubleUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericDoubleUpDown>(double.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericDoubleUpDown>(double.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericDoubleUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out double number) =>
|
||||
double.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(double? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override double Zero => 0;
|
||||
|
||||
protected override double? Add(double? a, double? b) => a + b;
|
||||
|
||||
protected override double? Minus(double? a, double? b) => a - b;
|
||||
}
|
||||
|
||||
public class NumericByteUpDown : NumericUpDownBase<byte>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericByteUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericByteUpDown>(byte.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericByteUpDown>(byte.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericByteUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out byte number) =>
|
||||
byte.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(byte? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override byte Zero => 0;
|
||||
|
||||
protected override byte? Add(byte? a, byte? b) => (byte?) (a + b);
|
||||
|
||||
protected override byte? Minus(byte? a, byte? b) => (byte?) (a - b);
|
||||
}
|
||||
|
||||
public class NumericSByteUpDown : NumericUpDownBase<sbyte>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericSByteUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericSByteUpDown>(sbyte.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericSByteUpDown>(sbyte.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericSByteUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out sbyte number) =>
|
||||
sbyte.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(sbyte? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override sbyte Zero => 0;
|
||||
|
||||
protected override sbyte? Add(sbyte? a, sbyte? b) => (sbyte?) (a + b);
|
||||
|
||||
protected override sbyte? Minus(sbyte? a, sbyte? b) => (sbyte?) (a - b);
|
||||
}
|
||||
|
||||
public class NumericShortUpDown : NumericUpDownBase<short>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericShortUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericShortUpDown>(short.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericShortUpDown>(short.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericShortUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out short number) =>
|
||||
short.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(short? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override short Zero => 0;
|
||||
|
||||
protected override short? Add(short? a, short? b) => (short?) (a + b);
|
||||
|
||||
protected override short? Minus(short? a, short? b) => (short?) (a - b);
|
||||
}
|
||||
|
||||
public class NumericUShortUpDown : NumericUpDownBase<ushort>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericUShortUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericUShortUpDown>(ushort.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericUShortUpDown>(ushort.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericUShortUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out ushort number) =>
|
||||
ushort.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(ushort? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override ushort Zero => 0;
|
||||
|
||||
protected override ushort? Add(ushort? a, ushort? b) => (ushort?) (a + b);
|
||||
|
||||
protected override ushort? Minus(ushort? a, ushort? b) => (ushort?) (a - b);
|
||||
}
|
||||
|
||||
public class NumericLongUpDown : NumericUpDownBase<long>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericLongUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericLongUpDown>(long.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericLongUpDown>(long.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericLongUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out long number) =>
|
||||
long.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(long? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override long Zero => 0;
|
||||
|
||||
protected override long? Add(long? a, long? b) => a + b;
|
||||
|
||||
protected override long? Minus(long? a, long? b) => a - b;
|
||||
}
|
||||
|
||||
public class NumericULongUpDown : NumericUpDownBase<ulong>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericULongUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericULongUpDown>(ulong.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericULongUpDown>(ulong.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericULongUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out ulong number) =>
|
||||
ulong.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(ulong? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override ulong Zero => 0;
|
||||
|
||||
protected override ulong? Add(ulong? a, ulong? b) => a + b;
|
||||
|
||||
protected override ulong? Minus(ulong? a, ulong? b) => a - b;
|
||||
}
|
||||
|
||||
public class NumericFloatUpDown : NumericUpDownBase<float>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericFloatUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericFloatUpDown>(float.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericFloatUpDown>(float.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericFloatUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out float number) =>
|
||||
float.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(float? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override float Zero => 0;
|
||||
|
||||
protected override float? Add(float? a, float? b) => a + b;
|
||||
|
||||
protected override float? Minus(float? a, float? b) => a - b;
|
||||
}
|
||||
|
||||
public class NumericDecimalUpDown : NumericUpDownBase<decimal>
|
||||
{
|
||||
protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown);
|
||||
|
||||
static NumericDecimalUpDown()
|
||||
{
|
||||
MaximumProperty.OverrideDefaultValue<NumericDecimalUpDown>(decimal.MaxValue);
|
||||
MinimumProperty.OverrideDefaultValue<NumericDecimalUpDown>(decimal.MinValue);
|
||||
StepProperty.OverrideDefaultValue<NumericDecimalUpDown>(1);
|
||||
}
|
||||
|
||||
protected override bool ParseText(string? text, out decimal number) =>
|
||||
decimal.TryParse(text, ParsingNumberStyle, NumberFormat, out number);
|
||||
|
||||
protected override string? ValueToString(decimal? value) => value?.ToString(FormatString, NumberFormat);
|
||||
|
||||
protected override decimal Zero => 0;
|
||||
|
||||
protected override decimal? Add(decimal? a, decimal? b) => a + b;
|
||||
|
||||
protected override decimal? Minus(decimal? a, decimal? b) => a - b;
|
||||
}
|
||||
|
||||
614
src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs
Normal file
614
src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs
Normal file
@@ -0,0 +1,614 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Net.Mime;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_Spinner, typeof(ButtonSpinner))]
|
||||
[TemplatePart(PART_TextBox, typeof(TextBox))]
|
||||
[TemplatePart(PART_DragPanel, typeof(Panel))]
|
||||
public abstract class NumericUpDown : TemplatedControl
|
||||
{
|
||||
public const string PART_Spinner = "PART_Spinner";
|
||||
public const string PART_TextBox = "PART_TextBox";
|
||||
public const string PART_DragPanel = "PART_DragPanel";
|
||||
|
||||
protected internal ButtonSpinner? _spinner;
|
||||
protected internal TextBox? _textBox;
|
||||
protected internal Panel? _dragPanel;
|
||||
|
||||
private Point? _point;
|
||||
protected internal bool _updateFromTextInput;
|
||||
|
||||
public static readonly StyledProperty<bool> AllowDragProperty = AvaloniaProperty.Register<NumericUpDown, bool>(
|
||||
nameof(AllowDrag), defaultValue: false);
|
||||
|
||||
public bool AllowDrag
|
||||
{
|
||||
get => GetValue(AllowDragProperty);
|
||||
set => SetValue(AllowDragProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsReadOnlyProperty = AvaloniaProperty.Register<NumericUpDown, bool>(
|
||||
nameof(IsReadOnly));
|
||||
|
||||
public bool IsReadOnly
|
||||
{
|
||||
get => GetValue(IsReadOnlyProperty);
|
||||
set => SetValue(IsReadOnlyProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> InnerLeftContentProperty = AvaloniaProperty.Register<NumericUpDown, object?>(
|
||||
nameof(InnerLeftContent));
|
||||
|
||||
public object? InnerLeftContent
|
||||
{
|
||||
get => GetValue(InnerLeftContentProperty);
|
||||
set => SetValue(InnerLeftContentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string?> WatermarkProperty = AvaloniaProperty.Register<NumericUpDown, string?>(
|
||||
nameof(Watermark));
|
||||
|
||||
public string? Watermark
|
||||
{
|
||||
get => GetValue(WatermarkProperty);
|
||||
set => SetValue(WatermarkProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<NumberFormatInfo?> NumberFormatProperty = AvaloniaProperty.Register<NumericUpDown, NumberFormatInfo?>(
|
||||
nameof(NumberFormat), defaultValue: NumberFormatInfo.CurrentInfo);
|
||||
|
||||
public NumberFormatInfo? NumberFormat
|
||||
{
|
||||
get => GetValue(NumberFormatProperty);
|
||||
set => SetValue(NumberFormatProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<string> FormatStringProperty = AvaloniaProperty.Register<NumericUpDown, string>(
|
||||
nameof(FormatString), string.Empty);
|
||||
|
||||
public string FormatString
|
||||
{
|
||||
get => GetValue(FormatStringProperty);
|
||||
set => SetValue(FormatStringProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<NumberStyles> ParsingNumberStyleProperty = AvaloniaProperty.Register<NumericUpDown, NumberStyles>(
|
||||
nameof(ParsingNumberStyle), defaultValue: NumberStyles.Any);
|
||||
|
||||
public NumberStyles ParsingNumberStyle
|
||||
{
|
||||
get => GetValue(ParsingNumberStyleProperty);
|
||||
set => SetValue(ParsingNumberStyleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IValueConverter?> TextConverterProperty = AvaloniaProperty.Register<NumericUpDown, IValueConverter?>(
|
||||
nameof(TextConverter));
|
||||
|
||||
public IValueConverter? TextConverter
|
||||
{
|
||||
get => GetValue(TextConverterProperty);
|
||||
set => SetValue(TextConverterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> AllowSpinProperty = AvaloniaProperty.Register<NumericUpDown, bool>(
|
||||
nameof(AllowSpin), true);
|
||||
|
||||
public bool AllowSpin
|
||||
{
|
||||
get => GetValue(AllowSpinProperty);
|
||||
set => SetValue(AllowSpinProperty, value);
|
||||
}
|
||||
|
||||
public event EventHandler<SpinEventArgs>? Spinned;
|
||||
|
||||
static NumericUpDown()
|
||||
{
|
||||
NumberFormatProperty.Changed.AddClassHandler<NumericUpDown>((o, e) => o.OnFormatChange(e));
|
||||
FormatStringProperty.Changed.AddClassHandler<NumericUpDown>((o, e) => o.OnFormatChange(e));
|
||||
IsReadOnlyProperty.Changed.AddClassHandler<NumericUpDown>((o,e)=>o.ChangeToSetSpinDirection(e));
|
||||
TextConverterProperty.Changed.AddClassHandler<NumericUpDown>((o, e) => o.OnFormatChange(e));
|
||||
}
|
||||
|
||||
protected void ChangeToSetSpinDirection(AvaloniaPropertyChangedEventArgs avaloniaPropertyChangedEventArgs, bool afterInitialization = false)
|
||||
{
|
||||
if (afterInitialization)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnFormatChange(AvaloniaPropertyChangedEventArgs arg)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
SyncTextAndValue(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
if(_spinner is not null)
|
||||
{
|
||||
_spinner.Spin -= OnSpin;
|
||||
}
|
||||
if (_dragPanel is not null)
|
||||
{
|
||||
_dragPanel.PointerPressed -= OnDragPanelPointerPressed;
|
||||
_dragPanel.PointerMoved -= OnDragPanelPointerMoved;
|
||||
_dragPanel.PointerReleased -= OnDragPanelPointerReleased;
|
||||
}
|
||||
_spinner = e.NameScope.Find<ButtonSpinner>(PART_Spinner);
|
||||
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
|
||||
_dragPanel = e.NameScope.Find<Panel>(PART_DragPanel);
|
||||
if (_spinner is not null)
|
||||
{
|
||||
_spinner.Spin += OnSpin;
|
||||
}
|
||||
if (_dragPanel is not null)
|
||||
{
|
||||
_dragPanel.PointerPressed+= OnDragPanelPointerPressed;
|
||||
_dragPanel.PointerMoved += OnDragPanelPointerMoved;
|
||||
_dragPanel.PointerReleased += OnDragPanelPointerReleased;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override void OnLostFocus(RoutedEventArgs e)
|
||||
{
|
||||
CommitInput(true);
|
||||
base.OnLostFocus(e);
|
||||
if(AllowDrag && _dragPanel is not null)
|
||||
{
|
||||
_dragPanel.IsVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
var commitSuccess = CommitInput(true);
|
||||
e.Handled = !commitSuccess;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
if (AllowDrag && _dragPanel is not null)
|
||||
{
|
||||
_dragPanel.IsVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDragPanelPointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
_point = e.GetPosition(this);
|
||||
if (e.ClickCount == 2 && _dragPanel is not null && AllowDrag)
|
||||
{
|
||||
_dragPanel.IsVisible = false;
|
||||
}
|
||||
_textBox?.Focus();
|
||||
}
|
||||
|
||||
private void OnDragPanelPointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
_point = null;
|
||||
}
|
||||
|
||||
private void OnDragPanelPointerMoved(object sender, PointerEventArgs e)
|
||||
{
|
||||
if (!AllowDrag || IsReadOnly) return;
|
||||
if(!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
|
||||
var point = e.GetPosition(this);
|
||||
var delta = point - _point;
|
||||
if (delta is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int d = GetDelta(delta.Value);
|
||||
if(d > 0)
|
||||
{
|
||||
Increase();
|
||||
}
|
||||
else if (d < 0)
|
||||
{
|
||||
Decrease();
|
||||
}
|
||||
_point = point;
|
||||
}
|
||||
|
||||
private int GetDelta(Point point)
|
||||
{
|
||||
bool horizontal = Math.Abs(point.X) > Math.Abs(point.Y);
|
||||
var value = horizontal ? point.X : -point.Y;
|
||||
return value switch
|
||||
{
|
||||
> 0 => 1,
|
||||
< 0 => -1,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
private void OnSpin(object sender, SpinEventArgs e)
|
||||
{
|
||||
if (AllowSpin && !IsReadOnly)
|
||||
{
|
||||
var spin = !e.UsingMouseWheel;
|
||||
spin |= _textBox is { IsFocused: true };
|
||||
if (spin)
|
||||
{
|
||||
e.Handled = true;
|
||||
var handler = Spinned;
|
||||
handler?.Invoke(this, e);
|
||||
if (e.Direction == SpinDirection.Increase)
|
||||
{
|
||||
Increase();
|
||||
}
|
||||
else
|
||||
{
|
||||
Decrease();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void SetValidSpinDirection();
|
||||
|
||||
protected abstract void Increase();
|
||||
protected abstract void Decrease();
|
||||
|
||||
protected virtual bool CommitInput(bool forceTextUpdate = false)
|
||||
{
|
||||
return SyncTextAndValue(true, _textBox?.Text, forceTextUpdate);
|
||||
}
|
||||
|
||||
protected abstract bool SyncTextAndValue(bool fromTextToValue = false, string? text = null,
|
||||
bool forceTextUpdate = false);
|
||||
|
||||
public abstract void Clear();
|
||||
}
|
||||
|
||||
public abstract class NumericUpDownBase<T>: NumericUpDown where T: struct, IComparable<T>
|
||||
{
|
||||
public static readonly StyledProperty<T?> ValueProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T?>(
|
||||
nameof(Value));
|
||||
|
||||
public T? Value
|
||||
{
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<T> MaximumProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
||||
nameof(Maximum), coerce: CoerceMaximum);
|
||||
|
||||
public T Maximum
|
||||
{
|
||||
get => GetValue(MaximumProperty);
|
||||
set => SetValue(MaximumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<T> MinimumProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
||||
nameof(Minimum), coerce: CoerceMinimum);
|
||||
|
||||
public T Minimum
|
||||
{
|
||||
get => GetValue(MinimumProperty);
|
||||
set => SetValue(MinimumProperty, value);
|
||||
}
|
||||
|
||||
#region Max and Min Coerce
|
||||
private static T CoerceMaximum(AvaloniaObject instance, T value)
|
||||
{
|
||||
if (instance is NumericUpDownBase<T> n)
|
||||
{
|
||||
return n.CoerceMaximum(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private T CoerceMaximum(T value)
|
||||
{
|
||||
if (value.CompareTo(Minimum) < 0)
|
||||
{
|
||||
return Minimum;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static T CoerceMinimum(AvaloniaObject instance, T value)
|
||||
{
|
||||
if (instance is NumericUpDownBase<T> n)
|
||||
{
|
||||
return n.CoerceMinimum(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private T CoerceMinimum(T value)
|
||||
{
|
||||
if (value.CompareTo(Maximum) > 0)
|
||||
{
|
||||
return Maximum;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static readonly StyledProperty<T> StepProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
||||
nameof(Step));
|
||||
|
||||
public T Step
|
||||
{
|
||||
get => GetValue(StepProperty);
|
||||
set => SetValue(StepProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<T?> EmptyInputValueProperty =
|
||||
AvaloniaProperty.Register<NumericUpDownBase<T>, T?>(
|
||||
nameof(EmptyInputValue), defaultValue: null);
|
||||
|
||||
public T? EmptyInputValue
|
||||
{
|
||||
get => GetValue(EmptyInputValueProperty);
|
||||
set => SetValue(EmptyInputValueProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ValueChanged"/> event.
|
||||
/// </summary>
|
||||
public static readonly RoutedEvent<ValueChangedEventArgs<T>> ValueChangedEvent =
|
||||
RoutedEvent.Register<NumericUpDown, ValueChangedEventArgs<T>>(nameof(ValueChanged), RoutingStrategies.Bubble);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the <see cref="Value"/> changes.
|
||||
/// </summary>
|
||||
public event EventHandler<ValueChangedEventArgs<T>>? ValueChanged
|
||||
{
|
||||
add => AddHandler(ValueChangedEvent, value);
|
||||
remove => RemoveHandler(ValueChangedEvent, value);
|
||||
}
|
||||
|
||||
static NumericUpDownBase()
|
||||
{
|
||||
StepProperty.Changed.AddClassHandler<NumericUpDownBase<T>>((o, e) => o.ChangeToSetSpinDirection(e));
|
||||
MaximumProperty.Changed.AddClassHandler<NumericUpDownBase<T>>((o, e) => o.OnConstraintChanged(e));
|
||||
MinimumProperty.Changed.AddClassHandler<NumericUpDownBase<T>>((o, e) => o.OnConstraintChanged(e));
|
||||
ValueProperty.Changed.AddClassHandler<NumericUpDownBase<T>>((o, e) => o.OnValueChanged(e) );
|
||||
}
|
||||
|
||||
private void OnConstraintChanged(AvaloniaPropertyChangedEventArgs avaloniaPropertyChangedEventArgs)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
if (Value.HasValue)
|
||||
{
|
||||
SetCurrentValue(ValueProperty, Clamp(Value, Maximum, Minimum));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValueChanged(AvaloniaPropertyChangedEventArgs args)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
SyncTextAndValue(false, null, true);
|
||||
}
|
||||
SetValidSpinDirection();
|
||||
T? oldValue = args.GetOldValue<T?>();
|
||||
T? newValue = args.GetNewValue<T?>();
|
||||
var e = new ValueChangedEventArgs<T>(ValueChangedEvent, oldValue, newValue);
|
||||
RaiseEvent(e);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
if (_textBox != null)
|
||||
{
|
||||
_textBox.Text = ConvertValueToText(Value);
|
||||
}
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
|
||||
protected virtual T? Clamp(T? value, T max, T min)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (value.Value.CompareTo(max) > 0)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
if (value.Value.CompareTo(min) < 0)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected override void SetValidSpinDirection()
|
||||
{
|
||||
var validDirection = ValidSpinDirections.None;
|
||||
if (!IsReadOnly)
|
||||
{
|
||||
if (Value is null)
|
||||
{
|
||||
validDirection = ValidSpinDirections.Increase | ValidSpinDirections.Decrease;
|
||||
}
|
||||
if (Value.HasValue && Value.Value.CompareTo(Maximum) < 0)
|
||||
{
|
||||
validDirection |= ValidSpinDirections.Increase;
|
||||
}
|
||||
|
||||
if (Value.HasValue && Value.Value.CompareTo(Minimum) > 0)
|
||||
{
|
||||
validDirection |= ValidSpinDirections.Decrease;
|
||||
}
|
||||
}
|
||||
if (_spinner != null)
|
||||
{
|
||||
_spinner.ValidSpinDirection = validDirection;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isSyncingTextAndValue;
|
||||
|
||||
protected override bool SyncTextAndValue(bool fromTextToValue = false, string? text = null, bool forceTextUpdate = false)
|
||||
{
|
||||
if (_isSyncingTextAndValue) return true;
|
||||
_isSyncingTextAndValue = true;
|
||||
var parsedTextIsValid = true;
|
||||
try
|
||||
{
|
||||
if (fromTextToValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newValue = ConvertTextToValue(text);
|
||||
if (EmptyInputValue is not null && newValue is null)
|
||||
{
|
||||
newValue = EmptyInputValue;
|
||||
}
|
||||
if (!Equals(newValue, Value))
|
||||
{
|
||||
SetCurrentValue(ValueProperty, newValue);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
parsedTextIsValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_updateFromTextInput)
|
||||
{
|
||||
if (forceTextUpdate)
|
||||
{
|
||||
var newText = ConvertValueToText(Value);
|
||||
if (_textBox!= null && !Equals(_textBox.Text, newText))
|
||||
{
|
||||
_textBox.Text = newText;
|
||||
_textBox.CaretIndex = newText?.Length ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_updateFromTextInput && !parsedTextIsValid)
|
||||
{
|
||||
if (_spinner is not null)
|
||||
{
|
||||
_spinner.ValidSpinDirection = ValidSpinDirections.None;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetValidSpinDirection();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSyncingTextAndValue = false;
|
||||
}
|
||||
return parsedTextIsValid;
|
||||
}
|
||||
|
||||
protected virtual T? ConvertTextToValue(string? text)
|
||||
{
|
||||
T? result;
|
||||
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||
if (TextConverter != null)
|
||||
{
|
||||
var valueFromText = TextConverter.Convert(text, typeof(T?), null, CultureInfo.CurrentCulture);
|
||||
return (T?)valueFromText;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ParseText(text, out var outputValue))
|
||||
{
|
||||
throw new InvalidDataException("Input string was not in a correct format.");
|
||||
}
|
||||
|
||||
result = outputValue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual string? ConvertValueToText(T? value)
|
||||
{
|
||||
if (TextConverter is not null)
|
||||
{
|
||||
return TextConverter.ConvertBack(Value, typeof(int), null, CultureInfo.CurrentCulture)?.ToString();
|
||||
}
|
||||
|
||||
if (FormatString.Contains("{0"))
|
||||
{
|
||||
return string.Format(NumberFormat, FormatString, value);
|
||||
}
|
||||
|
||||
return ValueToString(Value);
|
||||
}
|
||||
|
||||
protected override void Increase()
|
||||
{
|
||||
T? value;
|
||||
if (Value is not null)
|
||||
{
|
||||
value = Add(Value.Value, Step);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = IsSet(MinimumProperty) ? Minimum : Zero;
|
||||
}
|
||||
SetCurrentValue(ValueProperty, Clamp(value, Maximum, Minimum));
|
||||
}
|
||||
|
||||
protected override void Decrease()
|
||||
{
|
||||
T? value;
|
||||
if (Value is not null)
|
||||
{
|
||||
value = Minus(Value.Value, Step);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = IsSet(MaximumProperty) ? Maximum : Zero;
|
||||
}
|
||||
|
||||
SetCurrentValue(ValueProperty, Clamp(value, Maximum, Minimum));
|
||||
}
|
||||
|
||||
protected abstract bool ParseText(string? text, out T number);
|
||||
protected abstract string? ValueToString(T? value);
|
||||
protected abstract T Zero { get; }
|
||||
protected abstract T? Add(T? a, T? b);
|
||||
protected abstract T? Minus(T? a, T? b);
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
SetCurrentValue(ValueProperty, EmptyInputValue);
|
||||
SyncTextAndValue(false, forceTextUpdate: true);
|
||||
}
|
||||
}
|
||||
15
src/Ursa/Controls/NumericUpDown/ValueChangedEventArgs.cs
Normal file
15
src/Ursa/Controls/NumericUpDown/ValueChangedEventArgs.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class ValueChangedEventArgs<T>: RoutedEventArgs where T: struct, IComparable<T>
|
||||
{
|
||||
public ValueChangedEventArgs(RoutedEvent routedEvent, T? oldValue, T? newValue): base(routedEvent)
|
||||
{
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
}
|
||||
public T? OldValue { get; }
|
||||
public T? NewValue { get; }
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user