Browse Source

Merge branch 'master' into fixes/1447-alignment

pull/1449/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
4efd268788
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      samples/ControlCatalog/ControlCatalog.csproj
  2. 1
      samples/ControlCatalog/MainView.xaml
  3. 80
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  4. 94
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  5. 4
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  6. 5
      src/Avalonia.Controls/ButtonSpinner.cs
  7. 35
      src/Avalonia.Controls/MenuItem.cs
  8. 998
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  9. 16
      src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs
  10. 6
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  11. 3
      src/Avalonia.Controls/TextBox.cs
  12. 43
      src/Avalonia.Controls/Window.cs
  13. 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  14. 1
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  15. 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  16. 4
      src/Avalonia.Themes.Default/MenuItem.xaml
  17. 41
      src/Avalonia.Themes.Default/NumericUpDown.xaml
  18. 14
      src/Avalonia.Visuals/Matrix.cs
  19. 69
      src/Avalonia.Visuals/Media/SkewTransform.cs
  20. 16
      src/Gtk/Avalonia.Gtk3/KeyTransform.cs
  21. 8
      src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
  22. 8
      src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs
  23. 10
      src/Windows/Avalonia.Win32/WindowImpl.cs
  24. 94
      tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs

6
samples/ControlCatalog/ControlCatalog.csproj

@ -78,6 +78,9 @@
<EmbeddedResource Include="Pages\MenuPage.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Pages\NumericUpDownPage.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Pages\ProgressBarPage.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
@ -169,6 +172,9 @@
</Compile>
<Compile Include="Pages\ButtonSpinnerPage.xaml.cs">
<DependentUpon>ButtonSpinnerPage.xaml</DependentUpon>
</Compile>
<Compile Include="Pages\NumericUpDownPage.xaml.cs">
<DependentUpon>NumericUpDownPage.xaml</DependentUpon>
</Compile>
<Compile Include="Pages\ScreenPage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

1
samples/ControlCatalog/MainView.xaml

@ -19,6 +19,7 @@
<TabItem Header="Image"><pages:ImagePage/></TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
<TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="Slider"><pages:SliderPage/></TabItem>

80
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -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>

94
samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs

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

4
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@ -117,7 +117,7 @@ namespace Avalonia.Collections
_inner = new Dictionary<TKey, TValue>();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[]"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
if (CollectionChanged != null)
@ -222,4 +222,4 @@ namespace Avalonia.Collections
}
}
}
}
}

5
src/Avalonia.Controls/ButtonSpinner.cs

@ -201,6 +201,11 @@ namespace Avalonia.Controls
}
}
protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
{
SetButtonUsage();
}
/// <summary>
/// Called when the <see cref="AllowSpin"/> property value changed.
/// </summary>

35
src/Avalonia.Controls/MenuItem.cs

@ -93,6 +93,7 @@ namespace Avalonia.Controls
static MenuItem()
{
SelectableMixin.Attach<MenuItem>(IsSelectedProperty);
CommandProperty.Changed.Subscribe(CommandChanged);
FocusableProperty.OverrideDefaultValue<MenuItem>(true);
IconProperty.Changed.AddClassHandler<MenuItem>(x => x.IconChanged);
ItemsPanelProperty.OverrideDefaultValue<MenuItem>(DefaultPanel);
@ -424,6 +425,40 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Called when the <see cref="Command"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is MenuItem menuItem)
{
if (e.OldValue is ICommand oldCommand)
{
oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
}
if (e.NewValue is ICommand newCommand)
{
newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
}
menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);
}
}
/// <summary>
/// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void CanExecuteChanged(object sender, EventArgs e)
{
// HACK: Just set the IsEnabled property for the moment. This needs to be changed to
// use IsEnabledCore etc. but it will do for now.
IsEnabled = Command == null || Command.CanExecute(CommandParameter);
}
/// <summary>
/// Called when the <see cref="Icon"/> property changes.
/// </summary>

998
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

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

16
src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs

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

6
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -44,5 +44,11 @@ namespace Avalonia.Platform
/// Enables or disables the taskbar icon
/// </summary>
void ShowTaskbarIcon(bool value);
/// <summary>
/// Gets or sets a method called before the underlying implementation is destroyed.
/// Return true to prevent the underlying implementation from closing.
/// </summary>
Func<bool> Closing { get; set; }
}
}

3
src/Avalonia.Controls/TextBox.cs

@ -256,6 +256,8 @@ namespace Avalonia.Controls
{
_presenter?.ShowCaret();
}
e.Handled = true;
}
protected override void OnLostFocus(RoutedEventArgs e)
@ -269,6 +271,7 @@ namespace Avalonia.Controls
protected override void OnTextInput(TextInputEventArgs e)
{
HandleTextInput(e.Text);
e.Handled = true;
}
private void HandleTextInput(string input)

43
src/Avalonia.Controls/Window.cs

@ -13,6 +13,7 @@ using Avalonia.Styling;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using System.ComponentModel;
namespace Avalonia.Controls
{
@ -129,6 +130,7 @@ namespace Avalonia.Controls
public Window(IWindowImpl impl)
: base(impl)
{
impl.Closing = HandleClosing;
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
Screens = new Screens(PlatformImpl?.Screen);
}
@ -230,20 +232,23 @@ namespace Avalonia.Controls
/// <inheritdoc/>
Type IStyleable.StyleKey => typeof(Window);
/// <summary>
/// Fired before a window is closed.
/// </summary>
public event EventHandler<CancelEventArgs> Closing;
/// <summary>
/// Closes the window.
/// </summary>
public void Close()
{
s_windows.Remove(this);
PlatformImpl?.Dispose();
IsVisible = false;
Close(false);
}
protected override void HandleApplicationExiting()
{
base.HandleApplicationExiting();
Close();
Close(true);
}
/// <summary>
@ -258,7 +263,35 @@ namespace Avalonia.Controls
public void Close(object dialogResult)
{
_dialogResult = dialogResult;
Close();
Close(false);
}
internal void Close(bool ignoreCancel)
{
var cancelClosing = false;
try
{
cancelClosing = HandleClosing();
}
finally
{
if (ignoreCancel || !cancelClosing)
{
s_windows.Remove(this);
PlatformImpl?.Dispose();
IsVisible = false;
}
}
}
/// <summary>
/// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
/// </summary>
protected virtual bool HandleClosing()
{
var args = new CancelEventArgs();
Closing?.Invoke(this, args);
return args.Cancel;
}
/// <summary>

1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -39,6 +39,7 @@ namespace Avalonia.DesignerSupport.Remote
public Action<Point> PositionChanged { get; set; }
public Action Deactivated { get; set; }
public Action Activated { get; set; }
public Func<bool> Closing { get; set; }
public IPlatformHandle Handle { get; }
public WindowState WindowState { get; set; }
public Size MaxClientSize { get; } = new Size(4096, 4096);

1
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -26,6 +26,7 @@ namespace Avalonia.DesignerSupport.Remote
public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; }
public Action<double> ScalingChanged { get; set; }
public Func<bool> Closing { get; set; }
public Action Closed { get; set; }
public IMouseDevice MouseDevice { get; } = new MouseDevice();
public Point Position { get; set; }

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -43,4 +43,5 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.Calendar.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.DatePicker.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ButtonSpinner.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.NumericUpDown.xaml?assembly=Avalonia.Themes.Default"/>
</Styles>

4
src/Avalonia.Themes.Default/MenuItem.xaml

@ -133,4 +133,8 @@
<Style Selector="MenuItem:empty /template/ Path#rightArrow">
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="MenuItem:disabled">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
</Style>
</Styles>

41
src/Avalonia.Themes.Default/NumericUpDown.xaml

@ -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>

14
src/Avalonia.Visuals/Matrix.cs

@ -150,6 +150,19 @@ namespace Avalonia
return new Matrix(cos, sin, -sin, cos, 0, 0);
}
/// <summary>
/// Creates a skew matrix from the given axis skew angles in radians.
/// </summary>
/// <param name="xAngle">The amount of skew along the X-axis, in radians.</param>
/// <param name="yAngle">The amount of skew along the Y-axis, in radians.</param>
/// <returns>A rotation matrix.</returns>
public static Matrix CreateSkew(double xAngle, double yAngle)
{
double tanX = Math.Tan(xAngle);
double tanY = Math.Tan(yAngle);
return new Matrix(1.0, tanY, tanX, 1.0, 0.0, 0.0);
}
/// <summary>
/// Creates a scale matrix from the given X and Y components.
/// </summary>
@ -215,7 +228,6 @@ namespace Avalonia
return (_m11 * _m22) - (_m12 * _m21);
}
/// <summary>
/// Returns a boolean indicating whether the matrix is equal to the other given matrix.
/// </summary>

69
src/Avalonia.Visuals/Media/SkewTransform.cs

@ -0,0 +1,69 @@
// 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.VisualTree;
namespace Avalonia.Media
{
/// <summary>
/// Skews an <see cref="IVisual"/>.
/// </summary>
public class SkewTransform : Transform
{
/// <summary>
/// Defines the <see cref="AngleX"/> property.
/// </summary>
public static readonly StyledProperty<double> AngleXProperty =
AvaloniaProperty.Register<SkewTransform, double>(nameof(AngleX));
/// <summary>
/// Defines the <see cref="AngleY"/> property.
/// </summary>
public static readonly StyledProperty<double> AngleYProperty =
AvaloniaProperty.Register<SkewTransform, double>(nameof(AngleY));
/// <summary>
/// Initializes a new instance of the <see cref="SkewTransform"/> class.
/// </summary>
public SkewTransform()
{
this.GetObservable(AngleXProperty).Subscribe(_ => RaiseChanged());
this.GetObservable(AngleYProperty).Subscribe(_ => RaiseChanged());
}
/// <summary>
/// Initializes a new instance of the <see cref="SkewTransform"/> class.
/// </summary>
/// <param name="angleX">The skew angle of X-axis, in degrees.</param>
/// <param name="angleY">The skew angle of Y-axis, in degrees.</param>
public SkewTransform(double angleX, double angleY) : this()
{
AngleX = angleX;
AngleY = angleY;
}
/// <summary>
/// Gets or sets the AngleX property.
/// </summary>
public double AngleX
{
get { return GetValue(AngleXProperty); }
set { SetValue(AngleXProperty, value); }
}
/// <summary>
/// Gets or sets the AngleY property.
/// </summary>
public double AngleY
{
get { return GetValue(AngleYProperty); }
set { SetValue(AngleYProperty, value); }
}
/// <summary>
/// Gets the tranform's <see cref="Matrix"/>.
/// </summary>
public override Matrix Value => Matrix.CreateSkew(Matrix.ToRadians(AngleX), Matrix.ToRadians(AngleY));
}
}

16
src/Gtk/Avalonia.Gtk3/KeyTransform.cs

@ -151,8 +151,8 @@ namespace Avalonia.Gtk.Common
{ GdkKey.R2, Key.F22 },
{ GdkKey.F23, Key.F23 },
{ GdkKey.R4, Key.F24 },
//{ GdkKey.?, Key.NumLock }
//{ GdkKey.?, Key.Scroll }
{ GdkKey.Num_Lock, Key.NumLock },
{ GdkKey.Scroll_Lock, Key.Scroll },
//{ GdkKey.?, Key.LeftShift }
//{ GdkKey.?, Key.RightShift }
//{ GdkKey.?, Key.LeftCtrl }
@ -177,12 +177,12 @@ namespace Avalonia.Gtk.Common
//{ GdkKey.?, Key.SelectMedia }
//{ GdkKey.?, Key.LaunchApplication1 }
//{ GdkKey.?, Key.LaunchApplication2 }
//{ GdkKey.?, Key.OemSemicolon }
//{ GdkKey.?, Key.OemPlus }
//{ GdkKey.?, Key.OemComma }
//{ GdkKey.?, Key.OemMinus }
//{ GdkKey.?, Key.OemPeriod }
//{ GdkKey.?, Key.Oem2 }
{ GdkKey.semicolon, Key.OemSemicolon },
{ GdkKey.plus, Key.OemPlus },
{ GdkKey.comma, Key.OemComma },
{ GdkKey.minus, Key.OemMinus },
{ GdkKey.period, Key.OemPeriod },
{ GdkKey.slash, Key.Oem2 }
//{ GdkKey.?, Key.OemTilde }
//{ GdkKey.?, Key.AbntC1 }
//{ GdkKey.?, Key.AbntC2 }

8
src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs

@ -54,6 +54,7 @@ namespace Avalonia.Gtk3
ConnectEvent("key-press-event", OnKeyEvent);
ConnectEvent("key-release-event", OnKeyEvent);
ConnectEvent("leave-notify-event", OnLeaveNotifyEvent);
ConnectEvent("delete-event", OnClosingEvent);
Connect<Native.D.signal_generic>("destroy", OnDestroy);
Native.GtkWidgetRealize(gtkWidget);
GdkWindowHandle = this.Handle.Handle;
@ -125,6 +126,12 @@ namespace Avalonia.Gtk3
return rv;
}
private unsafe bool OnClosingEvent(IntPtr w, IntPtr ev, IntPtr userdata)
{
bool? preventClosing = Closing?.Invoke();
return preventClosing ?? false;
}
private unsafe bool OnButton(IntPtr w, IntPtr ev, IntPtr userdata)
{
var evnt = (GdkEventButton*)ev;
@ -343,6 +350,7 @@ namespace Avalonia.Gtk3
string IPlatformHandle.HandleDescriptor => "HWND";
public Action Activated { get; set; }
public Func<bool> Closing { get; set; }
public Action Closed { get; set; }
public Action Deactivated { get; set; }
public Action<RawInputEventArgs> Input { get; set; }

8
src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs

@ -4,6 +4,7 @@ using Avalonia.Input.Raw;
using Avalonia.Platform;
using MonoMac.AppKit;
using MonoMac.CoreGraphics;
using MonoMac.Foundation;
using MonoMac.ObjCRuntime;
namespace Avalonia.MonoMac
@ -69,6 +70,12 @@ namespace Avalonia.MonoMac
_impl.PositionChanged?.Invoke(_impl.Position);
}
public override bool WindowShouldClose(NSObject sender)
{
bool? preventClose = _impl.Closing?.Invoke();
return preventClose != true;
}
public override void WillClose(global::MonoMac.Foundation.NSNotification notification)
{
_impl.Window.Dispose();
@ -107,6 +114,7 @@ namespace Avalonia.MonoMac
public Action<Point> PositionChanged { get; set; }
public Action Deactivated { get; set; }
public Action Activated { get; set; }
public Func<bool> Closing { get; set; }
public override Size ClientSize => Window.ContentRectFor(Window.Frame).Size.ToAvaloniaSize();

10
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -56,6 +56,8 @@ namespace Avalonia.Win32
public Action Activated { get; set; }
public Func<bool> Closing { get; set; }
public Action Closed { get; set; }
public Action Deactivated { get; set; }
@ -431,6 +433,14 @@ namespace Avalonia.Win32
return IntPtr.Zero;
case UnmanagedMethods.WindowsMessage.WM_CLOSE:
bool? preventClosing = Closing?.Invoke();
if (preventClosing == true)
{
return IntPtr.Zero;
}
break;
case UnmanagedMethods.WindowsMessage.WM_DESTROY:
//Window doesn't exist anymore
_hwnd = IntPtr.Zero;

94
tests/Avalonia.Controls.UnitTests/LayoutTransformControlTests.cs

@ -30,6 +30,52 @@ namespace Avalonia.Controls.UnitTests
new Size(50, 25));
}
[Fact]
public void Measure_On_Skew_X_axis_45_degrees_Is_Correct()
{
TransformMeasureSizeTest(
new Size(100, 100),
new SkewTransform() { AngleX = 45 },
new Size(200, 100));
}
[Fact]
public void Measure_On_Skew_Y_axis_45_degrees_Is_Correct()
{
TransformMeasureSizeTest(
new Size(100, 100),
new SkewTransform() { AngleY = 45 },
new Size(100, 200));
}
[Fact]
public void Measure_On_Skew_X_axis_minus_45_degrees_Is_Correct()
{
TransformMeasureSizeTest(
new Size(100, 100),
new SkewTransform() { AngleX = -45 },
new Size(200, 100));
}
[Fact]
public void Measure_On_Skew_Y_axis_minus_45_degrees_Is_Correct()
{
TransformMeasureSizeTest(
new Size(100, 100),
new SkewTransform() { AngleY = -45 },
new Size(100, 200));
}
[Fact]
public void Measure_On_Skew_0_degrees_Is_Correct()
{
TransformMeasureSizeTest(
new Size(100, 100),
new SkewTransform() { AngleX = 0, AngleY = 0 },
new Size(100, 100));
}
[Fact]
public void Measure_On_Rotate_90_degrees_Is_Correct()
{
@ -125,7 +171,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Should_Generate_RenderTransform_90_degrees()
public void Should_Generate_RotateTransform_90_degrees()
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
100,
@ -147,7 +193,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Should_Generate_RenderTransform_minus_90_degrees()
public void Should_Generate_RotateTransform_minus_90_degrees()
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
100,
@ -189,6 +235,50 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(m.M32, res.M32, 3);
}
[Fact]
public void Should_Generate_SkewTransform_45_degrees()
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
100,
100,
new SkewTransform() { AngleX = 45, AngleY = 45 });
Assert.NotNull(lt.TransformRoot.RenderTransform);
Matrix m = lt.TransformRoot.RenderTransform.Value;
Matrix res = Matrix.CreateSkew(Matrix.ToRadians(45), Matrix.ToRadians(45));
Assert.Equal(m.M11, res.M11, 3);
Assert.Equal(m.M12, res.M12, 3);
Assert.Equal(m.M21, res.M21, 3);
Assert.Equal(m.M22, res.M22, 3);
Assert.Equal(m.M31, res.M31, 3);
Assert.Equal(m.M32, res.M32, 3);
}
[Fact]
public void Should_Generate_SkewTransform_minus_45_degrees()
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
100,
100,
new SkewTransform() { AngleX = -45, AngleY = -45 });
Assert.NotNull(lt.TransformRoot.RenderTransform);
Matrix m = lt.TransformRoot.RenderTransform.Value;
Matrix res = Matrix.CreateSkew(Matrix.ToRadians(-45), Matrix.ToRadians(-45));
Assert.Equal(m.M11, res.M11, 3);
Assert.Equal(m.M12, res.M12, 3);
Assert.Equal(m.M21, res.M21, 3);
Assert.Equal(m.M22, res.M22, 3);
Assert.Equal(m.M31, res.M31, 3);
Assert.Equal(m.M32, res.M32, 3);
}
private static void TransformMeasureSizeTest(Size size, Transform transform, Size expectedSize)
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(

Loading…
Cancel
Save