committed by
GitHub
55 changed files with 6481 additions and 158 deletions
@ -0,0 +1,59 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui"> |
||||
|
<StackPanel Orientation="Vertical" Gap="4"> |
||||
|
<TextBlock Classes="h1">AutoCompleteBox</TextBlock> |
||||
|
<TextBlock Classes="h2">A control into which the user can input text</TextBlock> |
||||
|
|
||||
|
<StackPanel Orientation="Horizontal" |
||||
|
Margin="0,16,0,0" |
||||
|
HorizontalAlignment="Center" |
||||
|
Gap="8"> |
||||
|
<StackPanel Orientation="Vertical"> |
||||
|
<TextBlock Text="MinimumPrefixLength: 1"/> |
||||
|
<AutoCompleteBox Width="200" |
||||
|
Margin="0,0,0,8" |
||||
|
MinimumPrefixLength="1"/> |
||||
|
<TextBlock Text="MinimumPrefixLength: 3"/> |
||||
|
<AutoCompleteBox Width="200" |
||||
|
Margin="0,0,0,8" |
||||
|
MinimumPrefixLength="3"/> |
||||
|
<TextBlock Text="MinimumPopulateDelay: 1 Second"/> |
||||
|
<AutoCompleteBox Width="200" |
||||
|
Margin="0,0,0,8" |
||||
|
MinimumPopulateDelay="1"/> |
||||
|
<TextBlock Text="MaxDropDownHeight: 60"/> |
||||
|
<AutoCompleteBox Width="200" |
||||
|
Margin="0,0,0,8" |
||||
|
MaxDropDownHeight="60"/> |
||||
|
<AutoCompleteBox Width="200" |
||||
|
Margin="0,0,0,8" |
||||
|
Watermark="Watermark"/> |
||||
|
<TextBlock Text="Disabled"/> |
||||
|
<AutoCompleteBox Width="200" |
||||
|
IsEnabled="False"/> |
||||
|
</StackPanel> |
||||
|
|
||||
|
|
||||
|
<StackPanel Orientation="Vertical"> |
||||
|
|
||||
|
<TextBlock Text="ValueMemeberSelector"/> |
||||
|
<AutoCompleteBox Width="200" |
||||
|
Margin="0,0,0,8" |
||||
|
ValueMemberSelector="Capital"/> |
||||
|
<TextBlock Text="ValueMemberBinding"/> |
||||
|
<AutoCompleteBox Width="200" |
||||
|
Margin="0,0,0,8" |
||||
|
ValueMemberBinding="{Binding Capital}"/> |
||||
|
<TextBlock Text="Multi-Binding"/> |
||||
|
<AutoCompleteBox Name="MultiBindingBox" |
||||
|
Width="200" |
||||
|
Margin="0,0,0,8" |
||||
|
FilterMode="Contains"/> |
||||
|
<TextBlock Text="Async Populate"/> |
||||
|
<AutoCompleteBox Name="AsyncBox" |
||||
|
Width="200" |
||||
|
Margin="0,0,0,8" |
||||
|
FilterMode="None"/> |
||||
|
</StackPanel> |
||||
|
</StackPanel> |
||||
|
</StackPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,143 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.LogicalTree; |
||||
|
using Avalonia.Markup; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using Avalonia.Markup.Xaml.Data; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace ControlCatalog.Pages |
||||
|
{ |
||||
|
public class AutoCompleteBoxPage : UserControl |
||||
|
{ |
||||
|
public class StateData |
||||
|
{ |
||||
|
public string Name { get; private set; } |
||||
|
public string Abbreviation { get; private set; } |
||||
|
public string Capital { get; private set; } |
||||
|
|
||||
|
public StateData(string name, string abbreviatoin, string capital) |
||||
|
{ |
||||
|
Name = name; |
||||
|
Abbreviation = abbreviatoin; |
||||
|
Capital = capital; |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
return Name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private StateData[] BuildAllStates() |
||||
|
{ |
||||
|
return new StateData[] |
||||
|
{ |
||||
|
new StateData("Alabama","AL","Montgomery"), |
||||
|
new StateData("Alaska","AK","Juneau"), |
||||
|
new StateData("Arizona","AZ","Phoenix"), |
||||
|
new StateData("Arkansas","AR","Little Rock"), |
||||
|
new StateData("California","CA","Sacramento"), |
||||
|
new StateData("Colorado","CO","Denver"), |
||||
|
new StateData("Connecticut","CT","Hartford"), |
||||
|
new StateData("Delaware","DE","Dover"), |
||||
|
new StateData("Florida","FL","Tallahassee"), |
||||
|
new StateData("Georgia","GA","Atlanta"), |
||||
|
new StateData("Hawaii","HI","Honolulu"), |
||||
|
new StateData("Idaho","ID","Boise"), |
||||
|
new StateData("Illinois","IL","Springfield"), |
||||
|
new StateData("Indiana","IN","Indianapolis"), |
||||
|
new StateData("Iowa","IA","Des Moines"), |
||||
|
new StateData("Kansas","KS","Topeka"), |
||||
|
new StateData("Kentucky","KY","Frankfort"), |
||||
|
new StateData("Louisiana","LA","Baton Rouge"), |
||||
|
new StateData("Maine","ME","Augusta"), |
||||
|
new StateData("Maryland","MD","Annapolis"), |
||||
|
new StateData("Massachusetts","MA","Boston"), |
||||
|
new StateData("Michigan","MI","Lansing"), |
||||
|
new StateData("Minnesota","MN","St. Paul"), |
||||
|
new StateData("Mississippi","MS","Jackson"), |
||||
|
new StateData("Missouri","MO","Jefferson City"), |
||||
|
new StateData("Montana","MT","Helena"), |
||||
|
new StateData("Nebraska","NE","Lincoln"), |
||||
|
new StateData("Nevada","NV","Carson City"), |
||||
|
new StateData("New Hampshire","NH","Concord"), |
||||
|
new StateData("New Jersey","NJ","Trenton"), |
||||
|
new StateData("New Mexico","NM","Santa Fe"), |
||||
|
new StateData("New York","NY","Albany"), |
||||
|
new StateData("North Carolina","NC","Raleigh"), |
||||
|
new StateData("North Dakota","ND","Bismarck"), |
||||
|
new StateData("Ohio","OH","Columbus"), |
||||
|
new StateData("Oklahoma","OK","Oklahoma City"), |
||||
|
new StateData("Oregon","OR","Salem"), |
||||
|
new StateData("Pennsylvania","PA","Harrisburg"), |
||||
|
new StateData("Rhode Island","RI","Providence"), |
||||
|
new StateData("South Carolina","SC","Columbia"), |
||||
|
new StateData("South Dakota","SD","Pierre"), |
||||
|
new StateData("Tennessee","TN","Nashville"), |
||||
|
new StateData("Texas","TX","Austin"), |
||||
|
new StateData("Utah","UT","Salt Lake City"), |
||||
|
new StateData("Vermont","VT","Montpelier"), |
||||
|
new StateData("Virginia","VA","Richmond"), |
||||
|
new StateData("Washington","WA","Olympia"), |
||||
|
new StateData("West Virginia","WV","Charleston"), |
||||
|
new StateData("Wisconsin","WI","Madison"), |
||||
|
new StateData("Wyoming","WY","Cheyenne"), |
||||
|
}; |
||||
|
} |
||||
|
public StateData[] States { get; private set; } |
||||
|
|
||||
|
public AutoCompleteBoxPage() |
||||
|
{ |
||||
|
this.InitializeComponent(); |
||||
|
|
||||
|
States = BuildAllStates(); |
||||
|
|
||||
|
foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) |
||||
|
{ |
||||
|
box.Items = States; |
||||
|
} |
||||
|
|
||||
|
var converter = new FuncMultiValueConverter<string, string>(parts => |
||||
|
{ |
||||
|
return String.Format("{0} ({1})", parts.ToArray()); |
||||
|
}); |
||||
|
var binding = new MultiBinding { Converter = converter }; |
||||
|
binding.Bindings.Add(new Binding("Name")); |
||||
|
binding.Bindings.Add(new Binding("Abbreviation")); |
||||
|
|
||||
|
var multibindingBox = this.FindControl<AutoCompleteBox>("MultiBindingBox"); |
||||
|
multibindingBox.ValueMemberBinding = binding; |
||||
|
|
||||
|
var asyncBox = this.FindControl<AutoCompleteBox>("AsyncBox"); |
||||
|
asyncBox.AsyncPopulator = PopulateAsync; |
||||
|
} |
||||
|
private IEnumerable<AutoCompleteBox> GetAllAutoCompleteBox() |
||||
|
{ |
||||
|
return |
||||
|
this.GetLogicalDescendants() |
||||
|
.OfType<AutoCompleteBox>(); |
||||
|
} |
||||
|
|
||||
|
private bool StringContains(string str, string query) |
||||
|
{ |
||||
|
return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; |
||||
|
} |
||||
|
private async Task<IEnumerable<object>> PopulateAsync(string searchText, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken); |
||||
|
|
||||
|
return |
||||
|
States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText)) |
||||
|
.ToList(); |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<StackPanel Orientation="Vertical" Gap="4"> |
||||
|
<TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock> |
||||
|
<TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock> |
||||
|
|
||||
|
<TextBlock Margin="2,5,2,2" FontSize="14" FontWeight="Bold">Features:</TextBlock> |
||||
|
<Grid Margin="2" ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto"> |
||||
|
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto, Auto" RowDefinitions="35,35,35,35,35"> |
||||
|
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">ShowButtonSpinner:</TextBlock> |
||||
|
<CheckBox Grid.Row="0" Grid.Column="1" IsChecked="{Binding #upDown.ShowButtonSpinner}" VerticalAlignment="Center" Margin="2"/> |
||||
|
|
||||
|
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">IsReadOnly:</TextBlock> |
||||
|
<CheckBox Grid.Row="1" Grid.Column="1" IsChecked="{Binding #upDown.IsReadOnly}" VerticalAlignment="Center" Margin="2"/> |
||||
|
|
||||
|
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">AllowSpin:</TextBlock> |
||||
|
<CheckBox Grid.Row="2" Grid.Column="1" IsChecked="{Binding #upDown.AllowSpin}" IsEnabled="{Binding #upDown.!IsReadOnly}" VerticalAlignment="Center" Margin="2"/> |
||||
|
|
||||
|
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">ClipValueToMinMax:</TextBlock> |
||||
|
<CheckBox Grid.Row="3" Grid.Column="1" IsChecked="{Binding #upDown.ClipValueToMinMax}" VerticalAlignment="Center" Margin="2"/> |
||||
|
|
||||
|
</Grid> |
||||
|
<Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="35,35,35,35,35"> |
||||
|
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock> |
||||
|
<DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}" |
||||
|
VerticalAlignment="Center" Margin="2"> |
||||
|
<DropDown.ItemTemplate> |
||||
|
<DataTemplate> |
||||
|
<StackPanel Orientation="Horizontal" Gap="2"> |
||||
|
<TextBlock Text="{Binding Name}"/> |
||||
|
<TextBlock Text="-"/> |
||||
|
<TextBlock Text="{Binding Value}"/> |
||||
|
</StackPanel> |
||||
|
</DataTemplate> |
||||
|
</DropDown.ItemTemplate> |
||||
|
</DropDown> |
||||
|
|
||||
|
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock> |
||||
|
<DropDown Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}" |
||||
|
VerticalAlignment="Center" Margin="2"/> |
||||
|
|
||||
|
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock> |
||||
|
<DropDown Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" SelectedItem="{Binding #upDown.CultureInfo}" |
||||
|
VerticalAlignment="Center" Margin="2"/> |
||||
|
|
||||
|
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock> |
||||
|
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding #upDown.Watermark}" VerticalAlignment="Center" Margin="2" /> |
||||
|
|
||||
|
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock> |
||||
|
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" /> |
||||
|
</Grid> |
||||
|
<Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="35,35,35,35,35" ColumnDefinitions="Auto, 120"> |
||||
|
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock> |
||||
|
<NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}" |
||||
|
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> |
||||
|
|
||||
|
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock> |
||||
|
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}" |
||||
|
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> |
||||
|
|
||||
|
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock> |
||||
|
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center" |
||||
|
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> |
||||
|
|
||||
|
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock> |
||||
|
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center" |
||||
|
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/> |
||||
|
|
||||
|
</Grid> |
||||
|
</Grid> |
||||
|
|
||||
|
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10"> |
||||
|
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock> |
||||
|
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5" |
||||
|
CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100" |
||||
|
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/> |
||||
|
</StackPanel> |
||||
|
|
||||
|
</StackPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,94 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using ReactiveUI; |
||||
|
|
||||
|
namespace ControlCatalog.Pages |
||||
|
{ |
||||
|
public class NumericUpDownPage : UserControl |
||||
|
{ |
||||
|
public NumericUpDownPage() |
||||
|
{ |
||||
|
this.InitializeComponent(); |
||||
|
var viewModel = new NumbersPageViewModel(); |
||||
|
DataContext = viewModel; |
||||
|
} |
||||
|
|
||||
|
private void InitializeComponent() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public class NumbersPageViewModel : ReactiveObject |
||||
|
{ |
||||
|
private IList<FormatObject> _formats; |
||||
|
private FormatObject _selectedFormat; |
||||
|
private IList<Location> _spinnerLocations; |
||||
|
|
||||
|
public NumbersPageViewModel() |
||||
|
{ |
||||
|
SelectedFormat = Formats.FirstOrDefault(); |
||||
|
} |
||||
|
|
||||
|
public IList<FormatObject> Formats |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return _formats ?? (_formats = new List<FormatObject>() |
||||
|
{ |
||||
|
new FormatObject() {Name = "Currency", Value = "C2"}, |
||||
|
new FormatObject() {Name = "Fixed point", Value = "F2"}, |
||||
|
new FormatObject() {Name = "General", Value = "G"}, |
||||
|
new FormatObject() {Name = "Number", Value = "N"}, |
||||
|
new FormatObject() {Name = "Percent", Value = "P"}, |
||||
|
new FormatObject() {Name = "Degrees", Value = "{0:N2} °"}, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IList<Location> SpinnerLocations |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (_spinnerLocations == null) |
||||
|
{ |
||||
|
_spinnerLocations = new List<Location>(); |
||||
|
foreach (Location value in Enum.GetValues(typeof(Location))) |
||||
|
{ |
||||
|
_spinnerLocations.Add(value); |
||||
|
} |
||||
|
} |
||||
|
return _spinnerLocations ; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IList<CultureInfo> Cultures { get; } = new List<CultureInfo>() |
||||
|
{ |
||||
|
new CultureInfo("en-US"), |
||||
|
new CultureInfo("en-GB"), |
||||
|
new CultureInfo("fr-FR"), |
||||
|
new CultureInfo("ar-DZ"), |
||||
|
new CultureInfo("zh-CN"), |
||||
|
new CultureInfo("cs-CZ") |
||||
|
}; |
||||
|
|
||||
|
public FormatObject SelectedFormat |
||||
|
{ |
||||
|
get { return _selectedFormat; } |
||||
|
set { this.RaiseAndSetIfChanged(ref _selectedFormat, value); } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class FormatObject |
||||
|
{ |
||||
|
public string Value { get; set; } |
||||
|
public string Name { get; set; } |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,998 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Threading; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
|
||||
|
/// </summary>
|
||||
|
public class NumericUpDown : TemplatedControl |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="AllowSpin"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<bool> AllowSpinProperty = |
||||
|
ButtonSpinner.AllowSpinProperty.AddOwner<NumericUpDown>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="ButtonSpinnerLocation"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty = |
||||
|
ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner<NumericUpDown>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="ShowButtonSpinner"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<bool> ShowButtonSpinnerProperty = |
||||
|
ButtonSpinner.ShowButtonSpinnerProperty.AddOwner<NumericUpDown>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="ClipValueToMinMax"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly DirectProperty<NumericUpDown, bool> ClipValueToMinMaxProperty = |
||||
|
AvaloniaProperty.RegisterDirect<NumericUpDown, bool>(nameof(ClipValueToMinMax), |
||||
|
updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="CultureInfo"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly DirectProperty<NumericUpDown, CultureInfo> CultureInfoProperty = |
||||
|
AvaloniaProperty.RegisterDirect<NumericUpDown, CultureInfo>(nameof(CultureInfo), o => o.CultureInfo, |
||||
|
(o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="FormatString"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<string> FormatStringProperty = |
||||
|
AvaloniaProperty.Register<NumericUpDown, string>(nameof(FormatString), string.Empty); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Increment"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<double> IncrementProperty = |
||||
|
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, validate: OnCoerceIncrement); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="IsReadOnly"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<bool> IsReadOnlyProperty = |
||||
|
AvaloniaProperty.Register<NumericUpDown, bool>(nameof(IsReadOnly)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Maximum"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<double> MaximumProperty = |
||||
|
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Minimum"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<double> MinimumProperty = |
||||
|
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="ParsingNumberStyle"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly DirectProperty<NumericUpDown, NumberStyles> ParsingNumberStyleProperty = |
||||
|
AvaloniaProperty.RegisterDirect<NumericUpDown, NumberStyles>(nameof(ParsingNumberStyle), |
||||
|
updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Text"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly DirectProperty<NumericUpDown, string> TextProperty = |
||||
|
AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v, |
||||
|
defaultBindingMode: BindingMode.TwoWay); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Value"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly DirectProperty<NumericUpDown, double> ValueProperty = |
||||
|
AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value, |
||||
|
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Watermark"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<string> WatermarkProperty = |
||||
|
AvaloniaProperty.Register<NumericUpDown, string>(nameof(Watermark)); |
||||
|
|
||||
|
private IDisposable _textBoxTextChangedSubscription; |
||||
|
|
||||
|
private double _value; |
||||
|
private string _text; |
||||
|
private bool _internalValueSet; |
||||
|
private bool _clipValueToMinMax; |
||||
|
private bool _isSyncingTextAndValueProperties; |
||||
|
private bool _isTextChangedFromUI; |
||||
|
private CultureInfo _cultureInfo; |
||||
|
private NumberStyles _parsingNumberStyle = NumberStyles.Any; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Spinner template part.
|
||||
|
/// </summary>
|
||||
|
private Spinner Spinner { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the TextBox template part.
|
||||
|
/// </summary>
|
||||
|
private TextBox TextBox { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel.
|
||||
|
/// </summary>
|
||||
|
public bool AllowSpin |
||||
|
{ |
||||
|
get { return GetValue(AllowSpinProperty); } |
||||
|
set { SetValue(AllowSpinProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets current location of the <see cref="ButtonSpinner"/>.
|
||||
|
/// </summary>
|
||||
|
public Location ButtonSpinnerLocation |
||||
|
{ |
||||
|
get { return GetValue(ButtonSpinnerLocationProperty); } |
||||
|
set { SetValue(ButtonSpinnerLocationProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a value indicating whether the spin buttons should be shown.
|
||||
|
/// </summary>
|
||||
|
public bool ShowButtonSpinner |
||||
|
{ |
||||
|
get { return GetValue(ShowButtonSpinnerProperty); } |
||||
|
set { SetValue(ShowButtonSpinnerProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets if the value should be clipped when minimum/maximum is reached.
|
||||
|
/// </summary>
|
||||
|
public bool ClipValueToMinMax |
||||
|
{ |
||||
|
get { return _clipValueToMinMax; } |
||||
|
set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the current CultureInfo.
|
||||
|
/// </summary>
|
||||
|
public CultureInfo CultureInfo |
||||
|
{ |
||||
|
get { return _cultureInfo; } |
||||
|
set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the display format of the <see cref="Value"/>.
|
||||
|
/// </summary>
|
||||
|
public string FormatString |
||||
|
{ |
||||
|
get { return GetValue(FormatStringProperty); } |
||||
|
set { SetValue(FormatStringProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the amount in which to increment the <see cref="Value"/>.
|
||||
|
/// </summary>
|
||||
|
public double Increment |
||||
|
{ |
||||
|
get { return GetValue(IncrementProperty); } |
||||
|
set { SetValue(IncrementProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets if the control is read only.
|
||||
|
/// </summary>
|
||||
|
public bool IsReadOnly |
||||
|
{ |
||||
|
get { return GetValue(IsReadOnlyProperty); } |
||||
|
set { SetValue(IsReadOnlyProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the maximum allowed value.
|
||||
|
/// </summary>
|
||||
|
public double Maximum |
||||
|
{ |
||||
|
get { return GetValue(MaximumProperty); } |
||||
|
set { SetValue(MaximumProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the minimum allowed value.
|
||||
|
/// </summary>
|
||||
|
public double Minimum |
||||
|
{ |
||||
|
get { return GetValue(MinimumProperty); } |
||||
|
set { SetValue(MinimumProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
|
||||
|
/// </summary>
|
||||
|
public NumberStyles ParsingNumberStyle |
||||
|
{ |
||||
|
get { return _parsingNumberStyle; } |
||||
|
set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the formatted string representation of the value.
|
||||
|
/// </summary>
|
||||
|
public string Text |
||||
|
{ |
||||
|
get { return _text; } |
||||
|
set { SetAndRaise(TextProperty, ref _text, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the value.
|
||||
|
/// </summary>
|
||||
|
public double Value |
||||
|
{ |
||||
|
get { return _value; } |
||||
|
set |
||||
|
{ |
||||
|
value = OnCoerceValue(value); |
||||
|
SetAndRaise(ValueProperty, ref _value, value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the object to use as a watermark if the <see cref="Value"/> is null.
|
||||
|
/// </summary>
|
||||
|
public string Watermark |
||||
|
{ |
||||
|
get { return GetValue(WatermarkProperty); } |
||||
|
set { SetValue(WatermarkProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes new instance of <see cref="NumericUpDown"/> class.
|
||||
|
/// </summary>
|
||||
|
public NumericUpDown() |
||||
|
{ |
||||
|
Initialized += (sender, e) => |
||||
|
{ |
||||
|
if (!_internalValueSet && IsInitialized) |
||||
|
{ |
||||
|
SyncTextAndValueProperties(false, null, true); |
||||
|
} |
||||
|
|
||||
|
SetValidSpinDirection(); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes static members of the <see cref="NumericUpDown"/> class.
|
||||
|
/// </summary>
|
||||
|
static NumericUpDown() |
||||
|
{ |
||||
|
CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); |
||||
|
FormatStringProperty.Changed.Subscribe(FormatStringChanged); |
||||
|
IncrementProperty.Changed.Subscribe(IncrementChanged); |
||||
|
IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); |
||||
|
MaximumProperty.Changed.Subscribe(OnMaximumChanged); |
||||
|
MinimumProperty.Changed.Subscribe(OnMinimumChanged); |
||||
|
TextProperty.Changed.Subscribe(OnTextChanged); |
||||
|
ValueProperty.Changed.Subscribe(OnValueChanged); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void OnTemplateApplied(TemplateAppliedEventArgs e) |
||||
|
{ |
||||
|
if (TextBox != null) |
||||
|
{ |
||||
|
TextBox.PointerPressed -= TextBoxOnPointerPressed; |
||||
|
_textBoxTextChangedSubscription?.Dispose(); |
||||
|
} |
||||
|
TextBox = e.NameScope.Find<TextBox>("PART_TextBox"); |
||||
|
if (TextBox != null) |
||||
|
{ |
||||
|
TextBox.Text = Text; |
||||
|
TextBox.PointerPressed += TextBoxOnPointerPressed; |
||||
|
_textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged()); |
||||
|
} |
||||
|
|
||||
|
if (Spinner != null) |
||||
|
{ |
||||
|
Spinner.Spin -= OnSpinnerSpin; |
||||
|
} |
||||
|
|
||||
|
Spinner = e.NameScope.Find<Spinner>("PART_Spinner"); |
||||
|
|
||||
|
if (Spinner != null) |
||||
|
{ |
||||
|
Spinner.Spin += OnSpinnerSpin; |
||||
|
} |
||||
|
|
||||
|
SetValidSpinDirection(); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void OnKeyDown(KeyEventArgs e) |
||||
|
{ |
||||
|
switch (e.Key) |
||||
|
{ |
||||
|
case Key.Enter: |
||||
|
var commitSuccess = CommitInput(); |
||||
|
e.Handled = !commitSuccess; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="CultureInfo"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="oldValue">The old value.</param>
|
||||
|
/// <param name="newValue">The new value.</param>
|
||||
|
protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue) |
||||
|
{ |
||||
|
if (IsInitialized) |
||||
|
{ |
||||
|
SyncTextAndValueProperties(false, null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="FormatString"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="oldValue">The old value.</param>
|
||||
|
/// <param name="newValue">The new value.</param>
|
||||
|
protected virtual void OnFormatStringChanged(string oldValue, string newValue) |
||||
|
{ |
||||
|
if (IsInitialized) |
||||
|
{ |
||||
|
SyncTextAndValueProperties(false, null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Increment"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="oldValue">The old value.</param>
|
||||
|
/// <param name="newValue">The new value.</param>
|
||||
|
protected virtual void OnIncrementChanged(double oldValue, double newValue) |
||||
|
{ |
||||
|
if (IsInitialized) |
||||
|
{ |
||||
|
SetValidSpinDirection(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="IsReadOnly"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="oldValue">The old value.</param>
|
||||
|
/// <param name="newValue">The new value.</param>
|
||||
|
protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue) |
||||
|
{ |
||||
|
SetValidSpinDirection(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Maximum"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="oldValue">The old value.</param>
|
||||
|
/// <param name="newValue">The new value.</param>
|
||||
|
protected virtual void OnMaximumChanged(double oldValue, double newValue) |
||||
|
{ |
||||
|
if (IsInitialized) |
||||
|
{ |
||||
|
SetValidSpinDirection(); |
||||
|
} |
||||
|
if (ClipValueToMinMax) |
||||
|
{ |
||||
|
Value = MathUtilities.Clamp(Value, Minimum, Maximum); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Minimum"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="oldValue">The old value.</param>
|
||||
|
/// <param name="newValue">The new value.</param>
|
||||
|
protected virtual void OnMinimumChanged(double oldValue, double newValue) |
||||
|
{ |
||||
|
if (IsInitialized) |
||||
|
{ |
||||
|
SetValidSpinDirection(); |
||||
|
} |
||||
|
if (ClipValueToMinMax) |
||||
|
{ |
||||
|
Value = MathUtilities.Clamp(Value, Minimum, Maximum); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Text"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="oldValue">The old value.</param>
|
||||
|
/// <param name="newValue">The new value.</param>
|
||||
|
protected virtual void OnTextChanged(string oldValue, string newValue) |
||||
|
{ |
||||
|
if (IsInitialized) |
||||
|
{ |
||||
|
SyncTextAndValueProperties(true, Text); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Value"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="oldValue">The old value.</param>
|
||||
|
/// <param name="newValue">The new value.</param>
|
||||
|
protected virtual void OnValueChanged(double oldValue, double newValue) |
||||
|
{ |
||||
|
if (!_internalValueSet && IsInitialized) |
||||
|
{ |
||||
|
SyncTextAndValueProperties(false, null, true); |
||||
|
} |
||||
|
|
||||
|
SetValidSpinDirection(); |
||||
|
|
||||
|
RaiseValueChangedEvent(oldValue, newValue); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Increment"/> property has to be coerced.
|
||||
|
/// </summary>
|
||||
|
/// <param name="baseValue">The value.</param>
|
||||
|
protected virtual double OnCoerceIncrement(double baseValue) |
||||
|
{ |
||||
|
return baseValue; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Maximum"/> property has to be coerced.
|
||||
|
/// </summary>
|
||||
|
/// <param name="baseValue">The value.</param>
|
||||
|
protected virtual double OnCoerceMaximum(double baseValue) |
||||
|
{ |
||||
|
return Math.Max(baseValue, Minimum); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Minimum"/> property has to be coerced.
|
||||
|
/// </summary>
|
||||
|
/// <param name="baseValue">The value.</param>
|
||||
|
protected virtual double OnCoerceMinimum(double baseValue) |
||||
|
{ |
||||
|
return Math.Min(baseValue, Maximum); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Value"/> property has to be coerced.
|
||||
|
/// </summary>
|
||||
|
/// <param name="baseValue">The value.</param>
|
||||
|
protected virtual double OnCoerceValue(double baseValue) |
||||
|
{ |
||||
|
return baseValue; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Raises the OnSpin event when spinning is initiated by the end-user.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
protected virtual void OnSpin(SpinEventArgs e) |
||||
|
{ |
||||
|
if (e == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException("e"); |
||||
|
} |
||||
|
|
||||
|
var handler = Spinned; |
||||
|
handler?.Invoke(this, e); |
||||
|
|
||||
|
if (e.Direction == SpinDirection.Increase) |
||||
|
{ |
||||
|
DoIncrement(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
DoDecrement(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Raises the <see cref="ValueChanged"/> event.
|
||||
|
/// </summary>
|
||||
|
/// <param name="oldValue">The old value.</param>
|
||||
|
/// <param name="newValue">The new value.</param>
|
||||
|
protected virtual void RaiseValueChangedEvent(double oldValue, double newValue) |
||||
|
{ |
||||
|
var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); |
||||
|
RaiseEvent(e); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts the formatted text to a value.
|
||||
|
/// </summary>
|
||||
|
private double ConvertTextToValue(string text) |
||||
|
{ |
||||
|
double result = 0; |
||||
|
|
||||
|
if (string.IsNullOrEmpty(text)) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
// Since the conversion from Value to text using a FormartString may not be parsable,
|
||||
|
// we verify that the already existing text is not the exact same value.
|
||||
|
var currentValueText = ConvertValueToText(); |
||||
|
if (Equals(currentValueText, text)) |
||||
|
{ |
||||
|
return Value; |
||||
|
} |
||||
|
|
||||
|
result = ConvertTextToValueCore(currentValueText, text); |
||||
|
|
||||
|
if (ClipValueToMinMax) |
||||
|
{ |
||||
|
return MathUtilities.Clamp(result, Minimum, Maximum); |
||||
|
} |
||||
|
|
||||
|
ValidateMinMax(result); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts the value to formatted text.
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
private string ConvertValueToText() |
||||
|
{ |
||||
|
//Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
|
||||
|
if (FormatString.Contains("{0")) |
||||
|
{ |
||||
|
return string.Format(CultureInfo, FormatString, Value); |
||||
|
} |
||||
|
|
||||
|
return Value.ToString(FormatString, CultureInfo); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called by OnSpin when the spin direction is SpinDirection.Increase.
|
||||
|
/// </summary>
|
||||
|
private void OnIncrement() |
||||
|
{ |
||||
|
var result = Value + Increment; |
||||
|
Value = MathUtilities.Clamp(result, Minimum, Maximum); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called by OnSpin when the spin direction is SpinDirection.Descrease.
|
||||
|
/// </summary>
|
||||
|
private void OnDecrement() |
||||
|
{ |
||||
|
var result = Value - Increment; |
||||
|
Value = MathUtilities.Clamp(result, Minimum, Maximum); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the valid spin directions.
|
||||
|
/// </summary>
|
||||
|
private void SetValidSpinDirection() |
||||
|
{ |
||||
|
var validDirections = ValidSpinDirections.None; |
||||
|
|
||||
|
// Zero increment always prevents spin.
|
||||
|
if (Increment != 0 && !IsReadOnly) |
||||
|
{ |
||||
|
if (Value < Maximum) |
||||
|
{ |
||||
|
validDirections = validDirections | ValidSpinDirections.Increase; |
||||
|
} |
||||
|
|
||||
|
if (Value > Minimum) |
||||
|
{ |
||||
|
validDirections = validDirections | ValidSpinDirections.Decrease; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Spinner != null) |
||||
|
{ |
||||
|
Spinner.ValidSpinDirection = validDirections; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="CultureInfo"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Sender is NumericUpDown upDown) |
||||
|
{ |
||||
|
var oldValue = (CultureInfo)e.OldValue; |
||||
|
var newValue = (CultureInfo)e.NewValue; |
||||
|
upDown.OnCultureInfoChanged(oldValue, newValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Increment"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Sender is NumericUpDown upDown) |
||||
|
{ |
||||
|
var oldValue = (double)e.OldValue; |
||||
|
var newValue = (double)e.NewValue; |
||||
|
upDown.OnIncrementChanged(oldValue, newValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="FormatString"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Sender is NumericUpDown upDown) |
||||
|
{ |
||||
|
var oldValue = (string)e.OldValue; |
||||
|
var newValue = (string)e.NewValue; |
||||
|
upDown.OnFormatStringChanged(oldValue, newValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="IsReadOnly"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Sender is NumericUpDown upDown) |
||||
|
{ |
||||
|
var oldValue = (bool)e.OldValue; |
||||
|
var newValue = (bool)e.NewValue; |
||||
|
upDown.OnIsReadOnlyChanged(oldValue, newValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Maximum"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Sender is NumericUpDown upDown) |
||||
|
{ |
||||
|
var oldValue = (double)e.OldValue; |
||||
|
var newValue = (double)e.NewValue; |
||||
|
upDown.OnMaximumChanged(oldValue, newValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Minimum"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Sender is NumericUpDown upDown) |
||||
|
{ |
||||
|
var oldValue = (double)e.OldValue; |
||||
|
var newValue = (double)e.NewValue; |
||||
|
upDown.OnMinimumChanged(oldValue, newValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Text"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Sender is NumericUpDown upDown) |
||||
|
{ |
||||
|
var oldValue = (string)e.OldValue; |
||||
|
var newValue = (string)e.NewValue; |
||||
|
upDown.OnTextChanged(oldValue, newValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Called when the <see cref="Value"/> property value changed.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">The event args.</param>
|
||||
|
private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Sender is NumericUpDown upDown) |
||||
|
{ |
||||
|
var oldValue = (double)e.OldValue; |
||||
|
var newValue = (double)e.NewValue; |
||||
|
upDown.OnValueChanged(oldValue, newValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void SetValueInternal(double value) |
||||
|
{ |
||||
|
_internalValueSet = true; |
||||
|
try |
||||
|
{ |
||||
|
Value = value; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_internalValueSet = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static double OnCoerceMaximum(NumericUpDown upDown, double value) |
||||
|
{ |
||||
|
return upDown.OnCoerceMaximum(value); |
||||
|
} |
||||
|
|
||||
|
private static double OnCoerceMinimum(NumericUpDown upDown, double value) |
||||
|
{ |
||||
|
return upDown.OnCoerceMinimum(value); |
||||
|
} |
||||
|
|
||||
|
private static double OnCoerceIncrement(NumericUpDown upDown, double value) |
||||
|
{ |
||||
|
return upDown.OnCoerceIncrement(value); |
||||
|
} |
||||
|
|
||||
|
private void TextBoxOnTextChanged() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
_isTextChangedFromUI = true; |
||||
|
if (TextBox != null) |
||||
|
{ |
||||
|
Text = TextBox.Text; |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_isTextChangedFromUI = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnSpinnerSpin(object sender, SpinEventArgs e) |
||||
|
{ |
||||
|
if (AllowSpin && !IsReadOnly) |
||||
|
{ |
||||
|
var spin = !e.UsingMouseWheel; |
||||
|
spin |= ((TextBox != null) && TextBox.IsFocused); |
||||
|
|
||||
|
if (spin) |
||||
|
{ |
||||
|
e.Handled = true; |
||||
|
OnSpin(e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void DoDecrement() |
||||
|
{ |
||||
|
if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease) |
||||
|
{ |
||||
|
OnDecrement(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void DoIncrement() |
||||
|
{ |
||||
|
if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase) |
||||
|
{ |
||||
|
OnIncrement(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public event EventHandler<SpinEventArgs> Spinned; |
||||
|
|
||||
|
private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) |
||||
|
{ |
||||
|
if (e.Device.Captured != Spinner) |
||||
|
{ |
||||
|
Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="ValueChanged"/> event.
|
||||
|
/// </summary>
|
||||
|
public static readonly RoutedEvent<NumericUpDownValueChangedEventArgs> ValueChangedEvent = |
||||
|
RoutedEvent.Register<NumericUpDown, NumericUpDownValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Raised when the <see cref="Value"/> changes.
|
||||
|
/// </summary>
|
||||
|
public event EventHandler<SpinEventArgs> ValueChanged |
||||
|
{ |
||||
|
add { AddHandler(ValueChangedEvent, value); } |
||||
|
remove { RemoveHandler(ValueChangedEvent, value); } |
||||
|
} |
||||
|
|
||||
|
private bool CommitInput() |
||||
|
{ |
||||
|
return SyncTextAndValueProperties(true, Text); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
|
||||
|
/// </summary>
|
||||
|
/// <param name="updateValueFromText">If value should be updated from text.</param>
|
||||
|
/// <param name="text">The text.</param>
|
||||
|
private bool SyncTextAndValueProperties(bool updateValueFromText, string text) |
||||
|
{ |
||||
|
return SyncTextAndValueProperties(updateValueFromText, text, false); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
|
||||
|
/// </summary>
|
||||
|
/// <param name="updateValueFromText">If value should be updated from text.</param>
|
||||
|
/// <param name="text">The text.</param>
|
||||
|
/// <param name="forceTextUpdate">Force text update.</param>
|
||||
|
private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate) |
||||
|
{ |
||||
|
if (_isSyncingTextAndValueProperties) |
||||
|
return true; |
||||
|
|
||||
|
_isSyncingTextAndValueProperties = true; |
||||
|
var parsedTextIsValid = true; |
||||
|
try |
||||
|
{ |
||||
|
if (updateValueFromText) |
||||
|
{ |
||||
|
if (!string.IsNullOrEmpty(text)) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var newValue = ConvertTextToValue(text); |
||||
|
if (!Equals(newValue, Value)) |
||||
|
{ |
||||
|
SetValueInternal(newValue); |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
parsedTextIsValid = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Do not touch the ongoing text input from user.
|
||||
|
if (!_isTextChangedFromUI) |
||||
|
{ |
||||
|
var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text); |
||||
|
if (!keepEmpty) |
||||
|
{ |
||||
|
var newText = ConvertValueToText(); |
||||
|
if (!Equals(Text, newText)) |
||||
|
{ |
||||
|
Text = newText; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Sync Text and textBox
|
||||
|
if (TextBox != null) |
||||
|
{ |
||||
|
TextBox.Text = Text; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (_isTextChangedFromUI && !parsedTextIsValid) |
||||
|
{ |
||||
|
// Text input was made from the user and the text
|
||||
|
// repesents an invalid value. Disable the spinner in this case.
|
||||
|
if (Spinner != null) |
||||
|
{ |
||||
|
Spinner.ValidSpinDirection = ValidSpinDirections.None; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
SetValidSpinDirection(); |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_isSyncingTextAndValueProperties = false; |
||||
|
} |
||||
|
return parsedTextIsValid; |
||||
|
} |
||||
|
|
||||
|
private double ConvertTextToValueCore(string currentValueText, string text) |
||||
|
{ |
||||
|
double result; |
||||
|
|
||||
|
if (IsPercent(FormatString)) |
||||
|
{ |
||||
|
result = decimal.ToDouble(ParsePercent(text, CultureInfo)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Problem while converting new text
|
||||
|
if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue)) |
||||
|
{ |
||||
|
var shouldThrow = true; |
||||
|
|
||||
|
// Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
|
||||
|
if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _)) |
||||
|
{ |
||||
|
// extract non-digit characters
|
||||
|
var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); |
||||
|
var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); |
||||
|
// same non-digit characters on currentValueText and new text => remove them on new Text to parse it again.
|
||||
|
if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) |
||||
|
{ |
||||
|
foreach (var character in textSpecialCharacters) |
||||
|
{ |
||||
|
text = text.Replace(character.ToString(), string.Empty); |
||||
|
} |
||||
|
// if without the special characters, parsing is good, do not throw
|
||||
|
if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue)) |
||||
|
{ |
||||
|
shouldThrow = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (shouldThrow) |
||||
|
{ |
||||
|
throw new InvalidDataException("Input string was not in a correct format."); |
||||
|
} |
||||
|
} |
||||
|
result = outputValue; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private void ValidateMinMax(double value) |
||||
|
{ |
||||
|
if (value < Minimum) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); |
||||
|
} |
||||
|
else if (value > Maximum) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parse percent format text
|
||||
|
/// </summary>
|
||||
|
/// <param name="text">Text to parse.</param>
|
||||
|
/// <param name="cultureInfo">The culture info.</param>
|
||||
|
private static decimal ParsePercent(string text, IFormatProvider cultureInfo) |
||||
|
{ |
||||
|
var info = NumberFormatInfo.GetInstance(cultureInfo); |
||||
|
text = text.Replace(info.PercentSymbol, null); |
||||
|
var result = decimal.Parse(text, NumberStyles.Any, info); |
||||
|
result = result / 100; |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private bool IsPercent(string stringToTest) |
||||
|
{ |
||||
|
var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal); |
||||
|
if (PIndex >= 0) |
||||
|
{ |
||||
|
//stringToTest contains a "P" between 2 "'", it's considered as text, not percent
|
||||
|
var isText = stringToTest.Substring(0, PIndex).Contains("'") |
||||
|
&& stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'"); |
||||
|
|
||||
|
return !isText; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using Avalonia.Interactivity; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
public class NumericUpDownValueChangedEventArgs : RoutedEventArgs |
||||
|
{ |
||||
|
public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue) : base(routedEvent) |
||||
|
{ |
||||
|
OldValue = oldValue; |
||||
|
NewValue = newValue; |
||||
|
} |
||||
|
|
||||
|
public double OldValue { get; } |
||||
|
public double NewValue { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,279 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Controls.Utils |
||||
|
{ |
||||
|
internal class BorderRenderHelper |
||||
|
{ |
||||
|
private bool _useComplexRendering; |
||||
|
private StreamGeometry _backgroundGeometryCache; |
||||
|
private StreamGeometry _borderGeometryCache; |
||||
|
|
||||
|
public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius) |
||||
|
{ |
||||
|
if (borderThickness.IsUniform && cornerRadius.IsUniform) |
||||
|
{ |
||||
|
_backgroundGeometryCache = null; |
||||
|
_borderGeometryCache = null; |
||||
|
_useComplexRendering = false; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_useComplexRendering = true; |
||||
|
|
||||
|
var boundRect = new Rect(finalSize); |
||||
|
var innerRect = boundRect.Deflate(borderThickness); |
||||
|
var innerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, false); |
||||
|
|
||||
|
StreamGeometry backgroundGeometry = null; |
||||
|
|
||||
|
if (innerRect.Width != 0 && innerRect.Height != 0) |
||||
|
{ |
||||
|
backgroundGeometry = new StreamGeometry(); |
||||
|
|
||||
|
using (var ctx = backgroundGeometry.Open()) |
||||
|
{ |
||||
|
CreateGeometry(ctx, innerRect, innerCoordinates); |
||||
|
} |
||||
|
|
||||
|
_backgroundGeometryCache = backgroundGeometry; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_backgroundGeometryCache = null; |
||||
|
} |
||||
|
|
||||
|
if (boundRect.Width != 0 && innerRect.Height != 0) |
||||
|
{ |
||||
|
var outerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, true); |
||||
|
var borderGeometry = new StreamGeometry(); |
||||
|
|
||||
|
using (var ctx = borderGeometry.Open()) |
||||
|
{ |
||||
|
CreateGeometry(ctx, boundRect, outerCoordinates); |
||||
|
|
||||
|
if (backgroundGeometry != null) |
||||
|
{ |
||||
|
CreateGeometry(ctx, innerRect, innerCoordinates); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_borderGeometryCache = borderGeometry; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_borderGeometryCache = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush) |
||||
|
{ |
||||
|
if (_useComplexRendering) |
||||
|
{ |
||||
|
var backgroundGeometry = _backgroundGeometryCache; |
||||
|
if (backgroundGeometry != null) |
||||
|
{ |
||||
|
context.DrawGeometry(background, null, backgroundGeometry); |
||||
|
} |
||||
|
|
||||
|
var borderGeometry = _borderGeometryCache; |
||||
|
if (borderGeometry != null) |
||||
|
{ |
||||
|
context.DrawGeometry(borderBrush, null, borderGeometry); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var borderThickness = borders.Left; |
||||
|
var cornerRadius = (float)radii.TopLeft; |
||||
|
var rect = new Rect(size); |
||||
|
|
||||
|
if (background != null) |
||||
|
{ |
||||
|
context.FillRectangle(background, rect.Deflate(borders), cornerRadius); |
||||
|
} |
||||
|
|
||||
|
if (borderBrush != null && borderThickness > 0) |
||||
|
{ |
||||
|
context.DrawRectangle(new Pen(borderBrush, borderThickness), rect.Deflate(borderThickness), cornerRadius); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderCoordinates borderCoordinates) |
||||
|
{ |
||||
|
var topLeft = new Point(borderCoordinates.LeftTop, 0); |
||||
|
var topRight = new Point(boundRect.Width - borderCoordinates.RightTop, 0); |
||||
|
var rightTop = new Point(boundRect.Width, borderCoordinates.TopRight); |
||||
|
var rightBottom = new Point(boundRect.Width, boundRect.Height - borderCoordinates.BottomRight); |
||||
|
var bottomRight = new Point(boundRect.Width - borderCoordinates.RightBottom, boundRect.Height); |
||||
|
var bottomLeft = new Point(borderCoordinates.LeftBottom, boundRect.Height); |
||||
|
var leftBottom = new Point(0, boundRect.Height - borderCoordinates.BottomLeft); |
||||
|
var leftTop = new Point(0, borderCoordinates.TopLeft); |
||||
|
|
||||
|
|
||||
|
if (topLeft.X > topRight.X) |
||||
|
{ |
||||
|
var scaledX = borderCoordinates.LeftTop / (borderCoordinates.LeftTop + borderCoordinates.RightTop) * boundRect.Width; |
||||
|
topLeft = new Point(scaledX, topLeft.Y); |
||||
|
topRight = new Point(scaledX, topRight.Y); |
||||
|
} |
||||
|
|
||||
|
if (rightTop.Y > rightBottom.Y) |
||||
|
{ |
||||
|
var scaledY = borderCoordinates.TopRight / (borderCoordinates.TopRight + borderCoordinates.BottomRight) * boundRect.Height; |
||||
|
rightTop = new Point(rightTop.X, scaledY); |
||||
|
rightBottom = new Point(rightBottom.X, scaledY); |
||||
|
} |
||||
|
|
||||
|
if (bottomRight.X < bottomLeft.X) |
||||
|
{ |
||||
|
var scaledX = borderCoordinates.LeftBottom / (borderCoordinates.LeftBottom + borderCoordinates.RightBottom) * boundRect.Width; |
||||
|
bottomRight = new Point(scaledX, bottomRight.Y); |
||||
|
bottomLeft = new Point(scaledX, bottomLeft.Y); |
||||
|
} |
||||
|
|
||||
|
if (leftBottom.Y < leftTop.Y) |
||||
|
{ |
||||
|
var scaledY = borderCoordinates.TopLeft / (borderCoordinates.TopLeft + borderCoordinates.BottomLeft) * boundRect.Height; |
||||
|
leftBottom = new Point(leftBottom.X, scaledY); |
||||
|
leftTop = new Point(leftTop.X, scaledY); |
||||
|
} |
||||
|
|
||||
|
var offset = new Vector(boundRect.TopLeft.X, boundRect.TopLeft.Y); |
||||
|
topLeft += offset; |
||||
|
topRight += offset; |
||||
|
rightTop += offset; |
||||
|
rightBottom += offset; |
||||
|
bottomRight += offset; |
||||
|
bottomLeft += offset; |
||||
|
leftBottom += offset; |
||||
|
leftTop += offset; |
||||
|
|
||||
|
context.BeginFigure(topLeft, true); |
||||
|
|
||||
|
//Top
|
||||
|
context.LineTo(topRight); |
||||
|
|
||||
|
//TopRight corner
|
||||
|
var radiusX = boundRect.TopRight.X - topRight.X; |
||||
|
var radiusY = rightTop.Y - boundRect.TopRight.Y; |
||||
|
if (radiusX != 0 || radiusY != 0) |
||||
|
{ |
||||
|
context.ArcTo(rightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise); |
||||
|
} |
||||
|
|
||||
|
//Right
|
||||
|
context.LineTo(rightBottom); |
||||
|
|
||||
|
//BottomRight corner
|
||||
|
radiusX = boundRect.BottomRight.X - bottomRight.X; |
||||
|
radiusY = boundRect.BottomRight.Y - rightBottom.Y; |
||||
|
if (radiusX != 0 || radiusY != 0) |
||||
|
{ |
||||
|
context.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); |
||||
|
} |
||||
|
|
||||
|
//Bottom
|
||||
|
context.LineTo(bottomLeft); |
||||
|
|
||||
|
//BottomLeft corner
|
||||
|
radiusX = bottomLeft.X - boundRect.BottomLeft.X; |
||||
|
radiusY = boundRect.BottomLeft.Y - leftBottom.Y; |
||||
|
if (radiusX != 0 || radiusY != 0) |
||||
|
{ |
||||
|
context.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); |
||||
|
} |
||||
|
|
||||
|
//Left
|
||||
|
context.LineTo(leftTop); |
||||
|
|
||||
|
//TopLeft corner
|
||||
|
radiusX = topLeft.X - boundRect.TopLeft.X; |
||||
|
radiusY = leftTop.Y - boundRect.TopLeft.Y; |
||||
|
|
||||
|
if (radiusX != 0 || radiusY != 0) |
||||
|
{ |
||||
|
context.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise); |
||||
|
} |
||||
|
|
||||
|
context.EndFigure(true); |
||||
|
} |
||||
|
|
||||
|
private struct BorderCoordinates |
||||
|
{ |
||||
|
internal BorderCoordinates(CornerRadius cornerRadius, Thickness borderThickness, bool isOuter) |
||||
|
{ |
||||
|
var left = 0.5 * borderThickness.Left; |
||||
|
var top = 0.5 * borderThickness.Top; |
||||
|
var right = 0.5 * borderThickness.Right; |
||||
|
var bottom = 0.5 * borderThickness.Bottom; |
||||
|
|
||||
|
if (isOuter) |
||||
|
{ |
||||
|
if (cornerRadius.TopLeft == 0) |
||||
|
{ |
||||
|
LeftTop = TopLeft = 0.0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
LeftTop = cornerRadius.TopLeft + left; |
||||
|
TopLeft = cornerRadius.TopLeft + top; |
||||
|
} |
||||
|
if (cornerRadius.TopRight == 0) |
||||
|
{ |
||||
|
TopRight = RightTop = 0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
TopRight = cornerRadius.TopRight + top; |
||||
|
RightTop = cornerRadius.TopRight + right; |
||||
|
} |
||||
|
if (cornerRadius.BottomRight == 0) |
||||
|
{ |
||||
|
RightBottom = BottomRight = 0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
RightBottom = cornerRadius.BottomRight + right; |
||||
|
BottomRight = cornerRadius.BottomRight + bottom; |
||||
|
} |
||||
|
if (cornerRadius.BottomLeft == 0) |
||||
|
{ |
||||
|
BottomLeft = LeftBottom = 0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
BottomLeft = cornerRadius.BottomLeft + bottom; |
||||
|
LeftBottom = cornerRadius.BottomLeft + left; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
LeftTop = Math.Max(0, cornerRadius.TopLeft - left); |
||||
|
TopLeft = Math.Max(0, cornerRadius.TopLeft - top); |
||||
|
TopRight = Math.Max(0, cornerRadius.TopRight - top); |
||||
|
RightTop = Math.Max(0, cornerRadius.TopRight - right); |
||||
|
RightBottom = Math.Max(0, cornerRadius.BottomRight - right); |
||||
|
BottomRight = Math.Max(0, cornerRadius.BottomRight - bottom); |
||||
|
BottomLeft = Math.Max(0, cornerRadius.BottomLeft - bottom); |
||||
|
LeftBottom = Math.Max(0, cornerRadius.BottomLeft - left); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal readonly double LeftTop; |
||||
|
internal readonly double TopLeft; |
||||
|
internal readonly double TopRight; |
||||
|
internal readonly double RightTop; |
||||
|
internal readonly double RightBottom; |
||||
|
internal readonly double BottomRight; |
||||
|
internal readonly double BottomLeft; |
||||
|
internal readonly double LeftBottom; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
// (c) Copyright Microsoft Corporation.
|
||||
|
// This source is subject to the Microsoft Public License (Ms-PL).
|
||||
|
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
|
||||
|
// All other rights reserved.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Input; |
||||
|
|
||||
|
namespace Avalonia.Controls.Utils |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines an item collection, selection members, and key handling for the
|
||||
|
/// selection adapter contained in the drop-down portion of an
|
||||
|
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
|
||||
|
/// </summary>
|
||||
|
public interface ISelectionAdapter |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets the selected item.
|
||||
|
/// </summary>
|
||||
|
/// <value>The currently selected item.</value>
|
||||
|
object SelectedItem { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Occurs when the
|
||||
|
/// <see cref="P:Avalonia.Controls.Utils.ISelectionAdapter.SelectedItem" />
|
||||
|
/// property value changes.
|
||||
|
/// </summary>
|
||||
|
event EventHandler<SelectionChangedEventArgs> SelectionChanged; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a collection that is used to generate content for the
|
||||
|
/// selection adapter.
|
||||
|
/// </summary>
|
||||
|
/// <value>The collection that is used to generate content for the
|
||||
|
/// selection adapter.</value>
|
||||
|
IEnumerable Items { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Occurs when a selected item is not cancelled and is committed as the
|
||||
|
/// selected item.
|
||||
|
/// </summary>
|
||||
|
event EventHandler<RoutedEventArgs> Commit; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Occurs when a selection has been canceled.
|
||||
|
/// </summary>
|
||||
|
event EventHandler<RoutedEventArgs> Cancel; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides handling for the
|
||||
|
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
|
||||
|
/// when a key is pressed while the drop-down portion of the
|
||||
|
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
|
||||
|
/// that contains data about the
|
||||
|
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
|
||||
|
void HandleKeyDown(KeyEventArgs e); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,342 @@ |
|||||
|
// (c) Copyright Microsoft Corporation.
|
||||
|
// This source is subject to the Microsoft Public License (Ms-PL).
|
||||
|
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
|
||||
|
// All other rights reserved.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.LogicalTree; |
||||
|
using System.Collections; |
||||
|
using System.Diagnostics; |
||||
|
|
||||
|
namespace Avalonia.Controls.Utils |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents the selection adapter contained in the drop-down portion of
|
||||
|
/// an <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
|
||||
|
/// </summary>
|
||||
|
public class SelectingItemsControlSelectionAdapter : ISelectionAdapter |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The SelectingItemsControl instance.
|
||||
|
/// </summary>
|
||||
|
private SelectingItemsControl _selector; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a value indicating whether the selection change event
|
||||
|
/// should not be fired.
|
||||
|
/// </summary>
|
||||
|
private bool IgnoringSelectionChanged { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the underlying
|
||||
|
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
||||
|
/// control.
|
||||
|
/// </summary>
|
||||
|
/// <value>The underlying
|
||||
|
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
||||
|
/// control.</value>
|
||||
|
public SelectingItemsControl SelectorControl |
||||
|
{ |
||||
|
get { return _selector; } |
||||
|
|
||||
|
set |
||||
|
{ |
||||
|
if (_selector != null) |
||||
|
{ |
||||
|
_selector.SelectionChanged -= OnSelectionChanged; |
||||
|
_selector.PointerReleased -= OnSelectorPointerReleased; |
||||
|
} |
||||
|
|
||||
|
_selector = value; |
||||
|
|
||||
|
if (_selector != null) |
||||
|
{ |
||||
|
_selector.SelectionChanged += OnSelectionChanged; |
||||
|
_selector.PointerReleased += OnSelectorPointerReleased; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Occurs when the
|
||||
|
/// <see cref="P:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.SelectedItem" />
|
||||
|
/// property value changes.
|
||||
|
/// </summary>
|
||||
|
public event EventHandler<SelectionChangedEventArgs> SelectionChanged; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Occurs when an item is selected and is committed to the underlying
|
||||
|
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
||||
|
/// control.
|
||||
|
/// </summary>
|
||||
|
public event EventHandler<RoutedEventArgs> Commit; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Occurs when a selection is canceled before it is committed.
|
||||
|
/// </summary>
|
||||
|
public event EventHandler<RoutedEventArgs> Cancel; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the
|
||||
|
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />
|
||||
|
/// class.
|
||||
|
/// </summary>
|
||||
|
public SelectingItemsControlSelectionAdapter() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the
|
||||
|
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapterr" />
|
||||
|
/// class with the specified
|
||||
|
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
||||
|
/// control.
|
||||
|
/// </summary>
|
||||
|
/// <param name="selector">The
|
||||
|
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" /> control
|
||||
|
/// to wrap as a
|
||||
|
/// <see cref="T:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter" />.</param>
|
||||
|
public SelectingItemsControlSelectionAdapter(SelectingItemsControl selector) |
||||
|
{ |
||||
|
SelectorControl = selector; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the selected item of the selection adapter.
|
||||
|
/// </summary>
|
||||
|
/// <value>The selected item of the underlying selection adapter.</value>
|
||||
|
public object SelectedItem |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return SelectorControl?.SelectedItem; |
||||
|
} |
||||
|
|
||||
|
set |
||||
|
{ |
||||
|
IgnoringSelectionChanged = true; |
||||
|
if (SelectorControl != null) |
||||
|
{ |
||||
|
SelectorControl.SelectedItem = value; |
||||
|
} |
||||
|
|
||||
|
// Attempt to reset the scroll viewer's position
|
||||
|
if (value == null) |
||||
|
{ |
||||
|
ResetScrollViewer(); |
||||
|
} |
||||
|
|
||||
|
IgnoringSelectionChanged = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a collection that is used to generate the content of
|
||||
|
/// the selection adapter.
|
||||
|
/// </summary>
|
||||
|
/// <value>The collection used to generate content for the selection
|
||||
|
/// adapter.</value>
|
||||
|
public IEnumerable Items |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return SelectorControl?.Items; |
||||
|
} |
||||
|
set |
||||
|
{ |
||||
|
if (SelectorControl != null) |
||||
|
{ |
||||
|
SelectorControl.Items = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// If the control contains a ScrollViewer, this will reset the viewer
|
||||
|
/// to be scrolled to the top.
|
||||
|
/// </summary>
|
||||
|
private void ResetScrollViewer() |
||||
|
{ |
||||
|
if (SelectorControl != null) |
||||
|
{ |
||||
|
ScrollViewer sv = SelectorControl.GetLogicalDescendants().OfType<ScrollViewer>().FirstOrDefault(); |
||||
|
if (sv != null) |
||||
|
{ |
||||
|
sv.Offset = new Vector(0, 0); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Handles the mouse left button up event on the selector control.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sender">The source object.</param>
|
||||
|
/// <param name="e">The event data.</param>
|
||||
|
private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e) |
||||
|
{ |
||||
|
if (e.MouseButton == MouseButton.Left) |
||||
|
{ |
||||
|
OnCommit(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Handles the SelectionChanged event on the SelectingItemsControl control.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sender">The source object.</param>
|
||||
|
/// <param name="e">The selection changed event data.</param>
|
||||
|
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) |
||||
|
{ |
||||
|
if (IgnoringSelectionChanged) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
SelectionChanged?.Invoke(sender, e); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Increments the
|
||||
|
/// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
|
||||
|
/// property of the underlying
|
||||
|
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
||||
|
/// control.
|
||||
|
/// </summary>
|
||||
|
protected void SelectedIndexIncrement() |
||||
|
{ |
||||
|
if (SelectorControl != null) |
||||
|
{ |
||||
|
SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount ? -1 : SelectorControl.SelectedIndex + 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Decrements the
|
||||
|
/// <see cref="P:Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex" />
|
||||
|
/// property of the underlying
|
||||
|
/// <see cref="T:Avalonia.Controls.Primitives.SelectingItemsControl" />
|
||||
|
/// control.
|
||||
|
/// </summary>
|
||||
|
protected void SelectedIndexDecrement() |
||||
|
{ |
||||
|
if (SelectorControl != null) |
||||
|
{ |
||||
|
int index = SelectorControl.SelectedIndex; |
||||
|
if (index >= 0) |
||||
|
{ |
||||
|
SelectorControl.SelectedIndex--; |
||||
|
} |
||||
|
else if (index == -1) |
||||
|
{ |
||||
|
SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides handling for the
|
||||
|
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event that occurs
|
||||
|
/// when a key is pressed while the drop-down portion of the
|
||||
|
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> has focus.
|
||||
|
/// </summary>
|
||||
|
/// <param name="e">A <see cref="T:Avalonia.Input.KeyEventArgs" />
|
||||
|
/// that contains data about the
|
||||
|
/// <see cref="E:Avalonia.Input.InputElement.KeyDown" /> event.</param>
|
||||
|
public void HandleKeyDown(KeyEventArgs e) |
||||
|
{ |
||||
|
switch (e.Key) |
||||
|
{ |
||||
|
case Key.Enter: |
||||
|
OnCommit(); |
||||
|
e.Handled = true; |
||||
|
break; |
||||
|
|
||||
|
case Key.Up: |
||||
|
SelectedIndexDecrement(); |
||||
|
e.Handled = true; |
||||
|
break; |
||||
|
|
||||
|
case Key.Down: |
||||
|
if ((e.Modifiers & InputModifiers.Alt) == InputModifiers.None) |
||||
|
{ |
||||
|
SelectedIndexIncrement(); |
||||
|
e.Handled = true; |
||||
|
} |
||||
|
break; |
||||
|
|
||||
|
case Key.Escape: |
||||
|
OnCancel(); |
||||
|
e.Handled = true; |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Raises the
|
||||
|
/// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Commit" />
|
||||
|
/// event.
|
||||
|
/// </summary>
|
||||
|
protected virtual void OnCommit() |
||||
|
{ |
||||
|
OnCommit(this, new RoutedEventArgs()); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Fires the Commit event.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sender">The source object.</param>
|
||||
|
/// <param name="e">The event data.</param>
|
||||
|
private void OnCommit(object sender, RoutedEventArgs e) |
||||
|
{ |
||||
|
Commit?.Invoke(sender, e); |
||||
|
|
||||
|
AfterAdapterAction(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Raises the
|
||||
|
/// <see cref="E:Avalonia.Controls.Utils.SelectingItemsControlSelectionAdapter.Cancel" />
|
||||
|
/// event.
|
||||
|
/// </summary>
|
||||
|
protected virtual void OnCancel() |
||||
|
{ |
||||
|
OnCancel(this, new RoutedEventArgs()); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Fires the Cancel event.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sender">The source object.</param>
|
||||
|
/// <param name="e">The event data.</param>
|
||||
|
private void OnCancel(object sender, RoutedEventArgs e) |
||||
|
{ |
||||
|
Cancel?.Invoke(sender, e); |
||||
|
|
||||
|
AfterAdapterAction(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Change the selection after the actions are complete.
|
||||
|
/// </summary>
|
||||
|
private void AfterAdapterAction() |
||||
|
{ |
||||
|
IgnoringSelectionChanged = true; |
||||
|
if (SelectorControl != null) |
||||
|
{ |
||||
|
SelectorControl.SelectedItem = null; |
||||
|
SelectorControl.SelectedIndex = -1; |
||||
|
} |
||||
|
IgnoringSelectionChanged = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
<Styles xmlns="https://github.com/avaloniaui"> |
||||
|
<Style Selector="AutoCompleteBox"> |
||||
|
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/> |
||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/> |
||||
|
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> |
||||
|
<Setter Property="Padding" Value="4"/> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<Panel> |
||||
|
<TextBox Name="PART_TextBox" |
||||
|
Background="{TemplateBinding Background}" |
||||
|
BorderBrush="{TemplateBinding BorderBrush}" |
||||
|
BorderThickness="{TemplateBinding BorderThickness}" |
||||
|
Padding="{TemplateBinding Padding}" |
||||
|
Watermark="{TemplateBinding Watermark}" |
||||
|
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" /> |
||||
|
|
||||
|
<Popup Name="PART_Popup" |
||||
|
MinWidth="{TemplateBinding Bounds.Width}" |
||||
|
MaxHeight="{TemplateBinding MaxDropDownHeight}" |
||||
|
PlacementTarget="{TemplateBinding}" |
||||
|
StaysOpen="False"> |
||||
|
<Border BorderBrush="{DynamicResource ThemeBorderMidBrush}" |
||||
|
BorderThickness="1"> |
||||
|
<ListBox Name="PART_SelectingItemsControl" |
||||
|
BorderThickness="0" |
||||
|
Background="{TemplateBinding Background}" |
||||
|
Foreground="{TemplateBinding Foreground}" |
||||
|
ItemTemplate="{TemplateBinding ItemTemplate}" |
||||
|
MemberSelector="{TemplateBinding ValueMemberSelector}" |
||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto" |
||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto" /> |
||||
|
</Border> |
||||
|
</Popup> |
||||
|
</Panel> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="AutoCompleteBox ListBoxItem:pointerover"> |
||||
|
<Setter Property="Background" Value="#ffd0d0d0"/> |
||||
|
</Style> |
||||
|
</Styles> |
||||
@ -0,0 +1,41 @@ |
|||||
|
<Styles xmlns="https://github.com/avaloniaui"> |
||||
|
<Style Selector="NumericUpDown"> |
||||
|
<Setter Property="TemplatedControl.BorderBrush" Value="{DynamicResource ThemeBorderLightBrush}"/> |
||||
|
<Setter Property="TemplatedControl.BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/> |
||||
|
<Setter Property="TemplatedControl.Background" Value="{DynamicResource ThemeBackgroundBrush}" /> |
||||
|
<Setter Property="TemplatedControl.Foreground" Value="{DynamicResource ThemeForegroundBrush}" /> |
||||
|
<Setter Property="TemplatedControl.Template"> |
||||
|
<ControlTemplate> |
||||
|
<ButtonSpinner Name="PART_Spinner" |
||||
|
Background="{TemplateBinding Background}" |
||||
|
BorderThickness="{TemplateBinding BorderThickness}" |
||||
|
BorderBrush="{TemplateBinding BorderBrush}" |
||||
|
HorizontalContentAlignment="Stretch" |
||||
|
VerticalContentAlignment="Stretch" |
||||
|
AllowSpin="{TemplateBinding AllowSpin}" |
||||
|
ShowButtonSpinner="{TemplateBinding ShowButtonSpinner}" |
||||
|
ButtonSpinnerLocation="{TemplateBinding ButtonSpinnerLocation}"> |
||||
|
<TextBox Name="PART_TextBox" |
||||
|
BorderThickness="0" |
||||
|
Background="Transparent" |
||||
|
ContextMenu="{TemplateBinding ContextMenu}" |
||||
|
FontFamily="{TemplateBinding FontFamily}" |
||||
|
FontSize="{TemplateBinding FontSize}" |
||||
|
FontStyle="{TemplateBinding FontStyle}" |
||||
|
FontWeight="{TemplateBinding FontWeight}" |
||||
|
Foreground="{TemplateBinding Foreground}" |
||||
|
Watermark="{TemplateBinding Watermark}" |
||||
|
IsReadOnly="{TemplateBinding IsReadOnly}" |
||||
|
Text="{TemplateBinding Text}" |
||||
|
Padding="{TemplateBinding Padding}" |
||||
|
TextAlignment="Left" |
||||
|
Margin="1" |
||||
|
MinWidth="20" |
||||
|
AcceptsReturn="False" |
||||
|
TextWrapping="NoWrap"> |
||||
|
</TextBox> |
||||
|
</ButtonSpinner> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
</Styles> |
||||
@ -0,0 +1,97 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
public struct CornerRadius |
||||
|
{ |
||||
|
public CornerRadius(double uniformRadius) |
||||
|
{ |
||||
|
TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius; |
||||
|
|
||||
|
} |
||||
|
public CornerRadius(double top, double bottom) |
||||
|
{ |
||||
|
TopLeft = TopRight = top; |
||||
|
BottomLeft = BottomRight = bottom; |
||||
|
} |
||||
|
public CornerRadius(double topLeft, double topRight, double bottomRight, double bottomLeft) |
||||
|
{ |
||||
|
TopLeft = topLeft; |
||||
|
TopRight = topRight; |
||||
|
BottomRight = bottomRight; |
||||
|
BottomLeft = bottomLeft; |
||||
|
} |
||||
|
|
||||
|
public double TopLeft { get; } |
||||
|
public double TopRight { get; } |
||||
|
public double BottomRight { get; } |
||||
|
public double BottomLeft { get; } |
||||
|
public bool IsEmpty => TopLeft.Equals(0) && IsUniform; |
||||
|
public bool IsUniform => TopLeft.Equals(TopRight) && BottomLeft.Equals(BottomRight) && TopRight.Equals(BottomRight); |
||||
|
|
||||
|
public override bool Equals(object obj) |
||||
|
{ |
||||
|
if (obj is CornerRadius) |
||||
|
{ |
||||
|
return this == (CornerRadius)obj; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public override int GetHashCode() |
||||
|
{ |
||||
|
return TopLeft.GetHashCode() ^ TopRight.GetHashCode() ^ BottomLeft.GetHashCode() ^ BottomRight.GetHashCode(); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
return $"{TopLeft},{TopRight},{BottomRight},{BottomLeft}"; |
||||
|
} |
||||
|
|
||||
|
public static CornerRadius Parse(string s, CultureInfo culture) |
||||
|
{ |
||||
|
var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) |
||||
|
.Select(x => x.Trim()) |
||||
|
.ToList(); |
||||
|
|
||||
|
switch (parts.Count) |
||||
|
{ |
||||
|
case 1: |
||||
|
var uniform = double.Parse(parts[0], culture); |
||||
|
return new CornerRadius(uniform); |
||||
|
case 2: |
||||
|
var top = double.Parse(parts[0], culture); |
||||
|
var bottom = double.Parse(parts[1], culture); |
||||
|
return new CornerRadius(top, bottom); |
||||
|
case 4: |
||||
|
var topLeft = double.Parse(parts[0], culture); |
||||
|
var topRight = double.Parse(parts[1], culture); |
||||
|
var bottomRight = double.Parse(parts[2], culture); |
||||
|
var bottomLeft = double.Parse(parts[3], culture); |
||||
|
return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft); |
||||
|
default: |
||||
|
{ |
||||
|
throw new FormatException("Invalid CornerRadius."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static bool operator ==(CornerRadius cr1, CornerRadius cr2) |
||||
|
{ |
||||
|
return cr1.TopLeft.Equals(cr2.TopLeft) |
||||
|
&& cr1.TopRight.Equals(cr2.TopRight) |
||||
|
&& cr1.BottomRight.Equals(cr2.BottomRight) |
||||
|
&& cr1.BottomLeft.Equals(cr2.BottomLeft); |
||||
|
} |
||||
|
|
||||
|
public static bool operator !=(CornerRadius cr1, CornerRadius cr2) |
||||
|
{ |
||||
|
return !(cr1 == cr2); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Globalization; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.Converters |
||||
|
{ |
||||
|
public class CornerRadiusTypeConverter : TypeConverter |
||||
|
{ |
||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
||||
|
{ |
||||
|
return sourceType == typeof(string); |
||||
|
} |
||||
|
|
||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
||||
|
{ |
||||
|
return CornerRadius.Parse((string)value, culture); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,43 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System.Globalization; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Visuals.UnitTests |
||||
|
{ |
||||
|
public class CornerRadiusTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Parse_Parses_Single_Uniform_Radius() |
||||
|
{ |
||||
|
var result = CornerRadius.Parse("3.4", CultureInfo.InvariantCulture); |
||||
|
|
||||
|
Assert.Equal(new CornerRadius(3.4), result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Parse_Parses_Top_Bottom() |
||||
|
{ |
||||
|
var result = CornerRadius.Parse("1.1,2.2", CultureInfo.InvariantCulture); |
||||
|
|
||||
|
Assert.Equal(new CornerRadius(1.1, 2.2), result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Parse_Parses_TopLeft_TopRight_BottomRight_BottomLeft() |
||||
|
{ |
||||
|
var result = CornerRadius.Parse("1.1,2.2,3.3,4.4", CultureInfo.InvariantCulture); |
||||
|
|
||||
|
Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Parse_Accepts_Spaces() |
||||
|
{ |
||||
|
var result = CornerRadius.Parse("1.1 2.2 3.3 4.4", CultureInfo.InvariantCulture); |
||||
|
|
||||
|
Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
Loading…
Reference in new issue