Browse Source

Merge remote-tracking branch 'origin/master' into pr/1427-added-autocompletebox-control

 Conflicts:
	src/Avalonia.Themes.Default/DefaultTheme.xaml
Steven Kirk 7 years ago
parent
commit
18cf638b30

+ 6 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -81,6 +81,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>
@@ -175,6 +178,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 - 0
samples/ControlCatalog/MainView.xaml

@@ -20,6 +20,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 - 0
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 - 0
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; }
+    }
+}

+ 2 - 2
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 - 0
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 - 0
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 - 0
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 - 0
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; }
+    }
+}

+ 1 - 5
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -221,7 +221,7 @@ namespace Avalonia.Controls.Presenters
         {
             var content = Content;
             var oldChild = Child;
-            var newChild = CreateChild();            
+            var newChild = CreateChild();
 
             // Remove the old child if we're not recycling it.
             if (oldChild != null && newChild != oldChild)
@@ -397,8 +397,6 @@ namespace Avalonia.Controls.Presenters
                     size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
                 }
 
-                size = LayoutHelper.ApplyLayoutConstraints(Child, size);
-
                 if (useLayoutRounding)
                 {
                     size = new Size(
@@ -412,7 +410,6 @@ namespace Avalonia.Controls.Presenters
                 switch (horizontalContentAlignment)
                 {
                     case HorizontalAlignment.Center:
-                    case HorizontalAlignment.Stretch:
                         originX += (availableSizeMinusMargins.Width - size.Width) / 2;
                         break;
                     case HorizontalAlignment.Right:
@@ -423,7 +420,6 @@ namespace Avalonia.Controls.Presenters
                 switch (verticalContentAlignment)
                 {
                     case VerticalAlignment.Center:
-                    case VerticalAlignment.Stretch:
                         originY += (availableSizeMinusMargins.Height - size.Height) / 2;
                         break;
                     case VerticalAlignment.Bottom:

+ 1 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -43,5 +43,6 @@
   <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"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.AutoCompleteBox.xaml?assembly=Avalonia.Themes.Default"/>
 </Styles>

+ 4 - 0
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 - 0
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>

+ 8 - 8
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 }

+ 25 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs

@@ -80,6 +80,31 @@ namespace Avalonia.Controls.UnitTests.Presenters
             Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
         }
 
+        [Fact]
+        public void Should_Correctly_Align_Child_With_Fixed_Size()
+        {
+            Border content;
+            var target = new ContentPresenter
+            {
+                HorizontalContentAlignment = HorizontalAlignment.Stretch,
+                VerticalContentAlignment = VerticalAlignment.Stretch,
+                Content = content = new Border
+                {
+                    HorizontalAlignment = HorizontalAlignment.Left,
+                    VerticalAlignment = VerticalAlignment.Bottom,
+                    Width = 16,
+                    Height = 16,
+                },
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            // Check correct result for Issue #1447.
+            Assert.Equal(new Rect(0, 84, 16, 16), content.Bounds);
+        }
+
         [Fact]
         public void Content_Can_Be_Stretched()
         {