From 2956f79c5c8268dd4d3f763324a501336e756394 Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Thu, 27 May 2021 21:52:37 +0930 Subject: [PATCH 01/27] Added DecimalPlaces property to NumericUpDown control --- .../NumericUpDown/NumericUpDown.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index abfbc038eb..e3db619269 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -119,6 +119,12 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty DecimalPlacesProperty = + AvaloniaProperty.Register(nameof(DecimalPlaces), 2); + private IDisposable _textBoxTextChangedSubscription; private double _value; @@ -261,6 +267,15 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets the number of decimal places to display in the up-down control. + /// + public int DecimalPlaces + { + get { return GetValue(DecimalPlacesProperty); } + set { SetValue(DecimalPlacesProperty, value); } + } + /// /// Gets or sets the object to use as a watermark if the is null. /// @@ -620,7 +635,7 @@ namespace Avalonia.Controls /// private void OnIncrement() { - var result = Value + Increment; + var result = Math.Round(Value + Increment, DecimalPlaces); Value = MathUtilities.Clamp(result, Minimum, Maximum); } @@ -629,7 +644,7 @@ namespace Avalonia.Controls /// private void OnDecrement() { - var result = Value - Increment; + var result = Math.Round(Value - Increment, DecimalPlaces); Value = MathUtilities.Clamp(result, Minimum, Maximum); } From 4cc7dba01a98e4781544b78811188614c0d9da0d Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Fri, 28 May 2021 10:21:15 +0930 Subject: [PATCH 02/27] Changed backing types to decimal. Added NumberFormat property. Changed ThrowCannotBeGreaterThanException to accept generics as it displays the values only. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 32 +++- .../NumericUpDown/NumericUpDown.cs | 165 +++++++++++------- .../NumericUpDownValueChangedEventArgs.cs | 6 +- 3 files changed, 132 insertions(+), 71 deletions(-) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 1ea369954a..596cbf1d7e 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -224,6 +224,34 @@ namespace Avalonia.Utilities } } + /// + /// Clamps a value between a minimum and maximum value. + /// + /// The value. + /// The minimum value. + /// The maximum value. + /// The clamped value. + public static decimal Clamp(decimal val, decimal min, decimal max) + { + if (min > max) + { + ThrowCannotBeGreaterThanException(min, max); + } + + if (val < min) + { + return min; + } + else if (val > max) + { + return max; + } + else + { + return val; + } + } + /// /// Clamps a value between a minimum and maximum value. /// @@ -281,8 +309,8 @@ namespace Avalonia.Utilities { return angle * 2 * Math.PI; } - - private static void ThrowCannotBeGreaterThanException(double min, double max) + + private static void ThrowCannotBeGreaterThanException(T min, T max) { throw new ArgumentException($"{min} cannot be greater than {max}."); } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index e3db619269..659bc272e0 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -49,6 +49,13 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(CultureInfo), o => o.CultureInfo, (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); + /// + /// Defines the property. + /// + public static readonly DirectProperty NumberFormatProperty = + AvaloniaProperty.RegisterDirect(nameof(NumberFormat), o => o.NumberFormat, + (o, v) => o.NumberFormat = v, NumberFormatInfo.CurrentInfo); + /// /// Defines the property. /// @@ -58,8 +65,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty IncrementProperty = - AvaloniaProperty.Register(nameof(Increment), 1.0d, coerce: OnCoerceIncrement); + public static readonly StyledProperty IncrementProperty = + AvaloniaProperty.Register(nameof(Increment), 1.0m, coerce: OnCoerceIncrement); /// /// Defines the property. @@ -70,14 +77,14 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty MaximumProperty = - AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, coerce: OnCoerceMaximum); + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register(nameof(Maximum), decimal.MaxValue, coerce: OnCoerceMaximum); /// /// Defines the property. /// - public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register(nameof(Minimum), double.MinValue, coerce: OnCoerceMinimum); + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), decimal.MinValue, coerce: OnCoerceMinimum); /// /// Defines the property. @@ -96,8 +103,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// @@ -106,7 +113,6 @@ namespace Avalonia.Controls public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register(nameof(Watermark)); - /// /// Defines the property. /// @@ -119,15 +125,9 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty DecimalPlacesProperty = - AvaloniaProperty.Register(nameof(DecimalPlaces), 2); - private IDisposable _textBoxTextChangedSubscription; - private double _value; + private decimal _value; private string _text; private bool _internalValueSet; private bool _clipValueToMinMax; @@ -135,7 +135,8 @@ namespace Avalonia.Controls private bool _isTextChangedFromUI; private CultureInfo _cultureInfo; private NumberStyles _parsingNumberStyle = NumberStyles.Any; - + private NumberFormatInfo _numberFormat; + /// /// Gets the Spinner template part. /// @@ -188,7 +189,21 @@ namespace Avalonia.Controls public CultureInfo CultureInfo { get { return _cultureInfo; } - set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } + set + { + SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); + //Set and Raise the NumberFormatProperty when CultureInfo is changed. + SetAndRaise(NumberFormatProperty, ref _numberFormat, value?.NumberFormat); + } + } + + /// + /// Gets or sets the current NumberFormatInfo + /// + public NumberFormatInfo NumberFormat + { + get { return _numberFormat; } + set { SetAndRaise(NumberFormatProperty, ref _numberFormat, value); } } /// @@ -203,7 +218,7 @@ namespace Avalonia.Controls /// /// Gets or sets the amount in which to increment the . /// - public double Increment + public decimal Increment { get { return GetValue(IncrementProperty); } set { SetValue(IncrementProperty, value); } @@ -221,7 +236,7 @@ namespace Avalonia.Controls /// /// Gets or sets the maximum allowed value. /// - public double Maximum + public decimal Maximum { get { return GetValue(MaximumProperty); } set { SetValue(MaximumProperty, value); } @@ -230,7 +245,7 @@ namespace Avalonia.Controls /// /// Gets or sets the minimum allowed value. /// - public double Minimum + public decimal Minimum { get { return GetValue(MinimumProperty); } set { SetValue(MinimumProperty, value); } @@ -257,7 +272,7 @@ namespace Avalonia.Controls /// /// Gets or sets the value. /// - public double Value + public decimal Value { get { return _value; } set @@ -267,15 +282,6 @@ namespace Avalonia.Controls } } - /// - /// Gets or sets the number of decimal places to display in the up-down control. - /// - public int DecimalPlaces - { - get { return GetValue(DecimalPlacesProperty); } - set { SetValue(DecimalPlacesProperty, value); } - } - /// /// Gets or sets the object to use as a watermark if the is null. /// @@ -285,7 +291,6 @@ namespace Avalonia.Controls set { SetValue(WatermarkProperty, value); } } - /// /// Gets or sets the horizontal alignment of the content within the control. /// @@ -326,6 +331,7 @@ namespace Avalonia.Controls static NumericUpDown() { CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); + NumberFormatProperty.Changed.Subscribe(OnNumberFormatChanged); FormatStringProperty.Changed.Subscribe(FormatStringChanged); IncrementProperty.Changed.Subscribe(IncrementChanged); IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); @@ -412,6 +418,19 @@ namespace Avalonia.Controls } } + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnNumberFormatChanged(NumberFormatInfo oldValue, NumberFormatInfo newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + /// /// Called when the property value changed. /// @@ -430,7 +449,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnIncrementChanged(double oldValue, double newValue) + protected virtual void OnIncrementChanged(decimal oldValue, decimal newValue) { if (IsInitialized) { @@ -453,7 +472,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnMaximumChanged(double oldValue, double newValue) + protected virtual void OnMaximumChanged(decimal oldValue, decimal newValue) { if (IsInitialized) { @@ -470,7 +489,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnMinimumChanged(double oldValue, double newValue) + protected virtual void OnMinimumChanged(decimal oldValue, decimal newValue) { if (IsInitialized) { @@ -500,7 +519,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnValueChanged(double oldValue, double newValue) + protected virtual void OnValueChanged(decimal oldValue, decimal newValue) { if (!_internalValueSet && IsInitialized) { @@ -516,7 +535,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceIncrement(double baseValue) + protected virtual decimal OnCoerceIncrement(decimal baseValue) { return baseValue; } @@ -525,7 +544,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceMaximum(double baseValue) + protected virtual decimal OnCoerceMaximum(decimal baseValue) { return Math.Max(baseValue, Minimum); } @@ -534,7 +553,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceMinimum(double baseValue) + protected virtual decimal OnCoerceMinimum(decimal baseValue) { return Math.Min(baseValue, Maximum); } @@ -543,7 +562,7 @@ namespace Avalonia.Controls /// Called when the property has to be coerced. /// /// The value. - protected virtual double OnCoerceValue(double baseValue) + protected virtual decimal OnCoerceValue(decimal baseValue) { return baseValue; } @@ -577,7 +596,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void RaiseValueChangedEvent(double oldValue, double newValue) + protected virtual void RaiseValueChangedEvent(decimal oldValue, decimal newValue) { var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); RaiseEvent(e); @@ -586,9 +605,9 @@ namespace Avalonia.Controls /// /// Converts the formatted text to a value. /// - private double ConvertTextToValue(string text) + private decimal ConvertTextToValue(string text) { - double result = 0; + decimal result = 0; if (string.IsNullOrEmpty(text)) { @@ -624,10 +643,10 @@ namespace Avalonia.Controls //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 string.Format(NumberFormat, FormatString, Value); } - return Value.ToString(FormatString, CultureInfo); + return Value.ToString(FormatString, NumberFormat); } /// @@ -635,7 +654,7 @@ namespace Avalonia.Controls /// private void OnIncrement() { - var result = Math.Round(Value + Increment, DecimalPlaces); + var result = Value + Increment; Value = MathUtilities.Clamp(result, Minimum, Maximum); } @@ -644,7 +663,7 @@ namespace Avalonia.Controls /// private void OnDecrement() { - var result = Math.Round(Value - Increment, DecimalPlaces); + var result = Value - Increment; Value = MathUtilities.Clamp(result, Minimum, Maximum); } @@ -689,6 +708,20 @@ namespace Avalonia.Controls } } + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnNumberFormatChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (NumberFormatInfo)e.OldValue; + var newValue = (NumberFormatInfo)e.NewValue; + upDown.OnNumberFormatChanged(oldValue, newValue); + } + } + /// /// Called when the property value changed. /// @@ -697,8 +730,8 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnIncrementChanged(oldValue, newValue); } } @@ -739,8 +772,8 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnMaximumChanged(oldValue, newValue); } } @@ -753,8 +786,8 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnMinimumChanged(oldValue, newValue); } } @@ -781,13 +814,13 @@ namespace Avalonia.Controls { if (e.Sender is NumericUpDown upDown) { - var oldValue = (double)e.OldValue; - var newValue = (double)e.NewValue; + var oldValue = (decimal)e.OldValue; + var newValue = (decimal)e.NewValue; upDown.OnValueChanged(oldValue, newValue); } } - private void SetValueInternal(double value) + private void SetValueInternal(decimal value) { _internalValueSet = true; try @@ -800,7 +833,7 @@ namespace Avalonia.Controls } } - private static double OnCoerceMaximum(IAvaloniaObject instance, double value) + private static decimal OnCoerceMaximum(IAvaloniaObject instance, decimal value) { if (instance is NumericUpDown upDown) { @@ -810,7 +843,7 @@ namespace Avalonia.Controls return value; } - private static double OnCoerceMinimum(IAvaloniaObject instance, double value) + private static decimal OnCoerceMinimum(IAvaloniaObject instance, decimal value) { if (instance is NumericUpDown upDown) { @@ -820,7 +853,7 @@ namespace Avalonia.Controls return value; } - private static double OnCoerceIncrement(IAvaloniaObject instance, double value) + private static decimal OnCoerceIncrement(IAvaloniaObject instance, decimal value) { if (instance is NumericUpDown upDown) { @@ -992,23 +1025,23 @@ namespace Avalonia.Controls return parsedTextIsValid; } - private double ConvertTextToValueCore(string currentValueText, string text) + private decimal ConvertTextToValueCore(string currentValueText, string text) { - double result; + decimal result; if (IsPercent(FormatString)) { - result = decimal.ToDouble(ParsePercent(text, CultureInfo)); + result = ParsePercent(text, NumberFormat); } else { // Problem while converting new text - if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue)) + if (!decimal.TryParse(text, ParsingNumberStyle, NumberFormat, 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 _)) + if (!decimal.TryParse(currentValueText, ParsingNumberStyle, NumberFormat, out var _)) { // extract non-digit characters var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); @@ -1021,7 +1054,7 @@ namespace Avalonia.Controls 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)) + if (decimal.TryParse(text, ParsingNumberStyle, NumberFormat, out outputValue)) { shouldThrow = false; } @@ -1038,7 +1071,7 @@ namespace Avalonia.Controls return result; } - private void ValidateMinMax(double value) + private void ValidateMinMax(decimal value) { if (value < Minimum) { diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs index e994ffdd15..9b467d682c 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs @@ -4,13 +4,13 @@ namespace Avalonia.Controls { public class NumericUpDownValueChangedEventArgs : RoutedEventArgs { - public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue) : base(routedEvent) + public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, decimal oldValue, decimal newValue) : base(routedEvent) { OldValue = oldValue; NewValue = newValue; } - public double OldValue { get; } - public double NewValue { get; } + public decimal OldValue { get; } + public decimal NewValue { get; } } } From 6c0d9e86bf175f265e5c0a5813fa6887a4c8a5d5 Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Fri, 28 May 2021 12:07:41 +0930 Subject: [PATCH 03/27] ApiCompatBaseline --- src/Avalonia.Controls/ApiCompatBaseline.txt | 26 ++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index a79b3b4d7b..2f312bb266 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -4,6 +4,30 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalon InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. +MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.NumericUpDown.ValueProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.IncrementProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.MaximumProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.MinimumProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Increment.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Increment.set(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Maximum.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Maximum.set(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Minimum.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Minimum.set(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceIncrement(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceMaximum(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceMinimum(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceValue(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnIncrementChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnMaximumChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnMinimumChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnValueChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.RaiseValueChangedEvent(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Value.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Value.set(System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChangedEventArgs..ctor(Avalonia.Interactivity.RoutedEvent, System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. @@ -11,4 +35,4 @@ EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -Total Issues: 12 +Total Issues: 36 From 7ffc527cd5f76237ae92d32d2a81be6153853759 Mon Sep 17 00:00:00 2001 From: Scott Whitney Date: Sat, 29 May 2021 07:56:11 +0930 Subject: [PATCH 04/27] Added Obsolete attribute to CultureInfo --- src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 659bc272e0..3a83d9ed7c 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -45,6 +45,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// + [Obsolete] public static readonly DirectProperty CultureInfoProperty = AvaloniaProperty.RegisterDirect(nameof(CultureInfo), o => o.CultureInfo, (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); @@ -186,6 +187,7 @@ namespace Avalonia.Controls /// /// Gets or sets the current CultureInfo. /// + [Obsolete("CultureInfo is obsolete, please use NumberFormat instead.")] public CultureInfo CultureInfo { get { return _cultureInfo; } From a6cfe88c61bf971ce20fc6c9dabde5b3553a049e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 23:40:40 +0200 Subject: [PATCH 05/27] Change ImmutableSolidColorBrush to a class. --- .../Media/Immutable/ImmutableSolidColorBrush.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs index 010184ad3b..8e93ac580e 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Immutable /// /// Fills an area with a solid color. /// - public readonly struct ImmutableSolidColorBrush : ISolidColorBrush, IEquatable + public class ImmutableSolidColorBrush : ISolidColorBrush, IEquatable { /// /// Initializes a new instance of the class. @@ -48,8 +48,9 @@ namespace Avalonia.Media.Immutable public bool Equals(ImmutableSolidColorBrush other) { - // ReSharper disable once CompareOfFloatsByEqualityOperator - return Color == other.Color && Opacity == other.Opacity; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Color.Equals(other.Color) && Opacity.Equals(other.Opacity); } public override bool Equals(object obj) @@ -67,12 +68,12 @@ namespace Avalonia.Media.Immutable public static bool operator ==(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) { - return left.Equals(right); + return Equals(left, right); } public static bool operator !=(ImmutableSolidColorBrush left, ImmutableSolidColorBrush right) { - return !left.Equals(right); + return !Equals(left, right); } /// From f9103e2c9561b614b3c94a71839c80f0ae8d4416 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 23:41:13 +0200 Subject: [PATCH 06/27] Parse brushes as immutable by default. --- src/Avalonia.Visuals/Media/Brush.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs index fb03d19a4e..cf7f5f531c 100644 --- a/src/Avalonia.Visuals/Media/Brush.cs +++ b/src/Avalonia.Visuals/Media/Brush.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using Avalonia.Animation; using Avalonia.Animation.Animators; +using Avalonia.Media.Immutable; namespace Avalonia.Media { @@ -47,7 +48,7 @@ namespace Avalonia.Media if (s[0] == '#') { - return new SolidColorBrush(Color.Parse(s)); + return new ImmutableSolidColorBrush(Color.Parse(s)); } var brush = KnownColors.GetKnownBrush(s); From e742f81a7d2b1722d301132ce452a127a70e7af5 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 8 Jun 2021 23:41:34 +0200 Subject: [PATCH 07/27] Parse brushes during compile time. --- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 14 ++++++++++++++ .../Transformers/AvaloniaXamlIlWellKnownTypes.cs | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 059b5650cb..4592b9c8b4 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -207,6 +207,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } + if (types.IBrush.IsAssignableFrom(type)) + { + if (Color.TryParse(text, out Color color)) + { + var brushTypeRef = new XamlAstClrTypeReference(node, types.ImmutableSolidColorBrush, false); + + result = new XamlAstNewClrObjectNode(node, brushTypeRef, + types.ImmutableSolidColorBrushConstructorColor, + new List { new XamlConstantNode(node, types.UInt, color.ToUint32()) }); + + return true; + } + } + result = null; return false; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index c4995b2de3..6dd3521183 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -80,7 +80,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType ColumnDefinitions { get; } public IXamlType Classes { get; } public IXamlMethod ClassesBindMethod { get; } - public IXamlProperty StyledElementClassesProperty { get; set; } + public IXamlProperty StyledElementClassesProperty { get; } + public IXamlType IBrush { get; } + public IXamlType ImmutableSolidColorBrush { get; } + public IXamlConstructor ImmutableSolidColorBrushConstructorColor { get; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -178,6 +181,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers .FindMethod( "BindClass", IDisposable, false, IStyledElement, cfg.WellKnownTypes.String, IBinding, cfg.WellKnownTypes.Object); + + IBrush = cfg.TypeSystem.GetType("Avalonia.Media.IBrush"); + ImmutableSolidColorBrush = cfg.TypeSystem.GetType("Avalonia.Media.Immutable.ImmutableSolidColorBrush"); + ImmutableSolidColorBrushConstructorColor = ImmutableSolidColorBrush.GetConstructor(new List { UInt }); } } From 82d2ee175f39f044b6cc7864899167eafb4712c9 Mon Sep 17 00:00:00 2001 From: Splitwirez Date: Sat, 19 Jun 2021 15:36:58 -0400 Subject: [PATCH 08/27] Add `Arc`...again... --- src/Avalonia.Controls/Shapes/Arc.cs | 99 +++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/Avalonia.Controls/Shapes/Arc.cs diff --git a/src/Avalonia.Controls/Shapes/Arc.cs b/src/Avalonia.Controls/Shapes/Arc.cs new file mode 100644 index 0000000000..f6738e10f2 --- /dev/null +++ b/src/Avalonia.Controls/Shapes/Arc.cs @@ -0,0 +1,99 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Controls.Shapes +{ + public class Arc : Shape + { + /// + /// Defines the property. + /// + public static readonly StyledProperty StartAngleProperty = + AvaloniaProperty.Register(nameof(StartAngle), 0.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SweepAngleProperty = + AvaloniaProperty.Register(nameof(SweepAngle), 0.0); + + static Arc() + { + StrokeThicknessProperty.OverrideDefaultValue(1); + AffectsGeometry(BoundsProperty, StrokeThicknessProperty, StartAngleProperty, SweepAngleProperty); + } + + /// + /// Gets or sets the angle at which the arc starts, in degrees. + /// + public double StartAngle + { + get { return GetValue(StartAngleProperty); } + set { SetValue(StartAngleProperty, value); } + } + + /// + /// Gets or sets the angle at which the arc ends relative to the start angle, in degrees. + /// + public double SweepAngle + { + get { return GetValue(SweepAngleProperty); } + set { SetValue(SweepAngleProperty, value); } + } + + protected override Geometry CreateDefiningGeometry() + { + double angle1 = DegreesToRad(StartAngle); + double angle2 = angle1 + DegreesToRad(SweepAngle); + + double startAngle = Math.Min(angle1, angle2); + double sweepAngle = Math.Max(angle1, angle2); + + double normStart = RadToNormRad(startAngle); + double normEnd = RadToNormRad(sweepAngle); + + var rect = new Rect(Bounds.Size); + + if ((normStart == normEnd) && (startAngle != sweepAngle)) //complete ring + { + return new EllipseGeometry(rect.Deflate(StrokeThickness / 2)); + } + else if (SweepAngle == 0) + { + return new StreamGeometry(); + } + else //partial arc + { + var deflatedRect = rect.Deflate(StrokeThickness / 2); + + double centerX = rect.Center.X; + double centerY = rect.Center.Y; + + double radiusX = deflatedRect.Width / 2; + double radiusY = deflatedRect.Height / 2; + + double angleGap = RadToNormRad(sweepAngle - startAngle); + + Point startPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, startAngle); + Point endPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, sweepAngle); + + StreamGeometry arcGeometry = new StreamGeometry(); + using (var ctx = arcGeometry.Open()) + { + ctx.BeginFigure(startPoint, false); + ctx.ArcTo(endPoint, new Size(radiusX, radiusY), angleGap, angleGap >= Math.PI, SweepDirection.Clockwise); + ctx.EndFigure(false); + } + return arcGeometry; + } + } + + static double DegreesToRad(double inAngle) => + inAngle * Math.PI / 180; + + static double RadToNormRad(double inAngle) => (0 + (inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); + + static Point GetRingPoint(double radiusX, double radiusY, double centerX, double centerY, double angle) => + new Point((radiusX * Math.Cos(angle)) + centerX, (radiusY * Math.Sin(angle)) + centerY); + } +} From 6e99f1240634c36bc164a87399333af03b513ba9 Mon Sep 17 00:00:00 2001 From: Splitwirez Date: Sat, 3 Jul 2021 11:17:19 -0400 Subject: [PATCH 09/27] Fix comments --- src/Avalonia.Controls/Shapes/Arc.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Arc.cs b/src/Avalonia.Controls/Shapes/Arc.cs index f6738e10f2..4ce72319aa 100644 --- a/src/Avalonia.Controls/Shapes/Arc.cs +++ b/src/Avalonia.Controls/Shapes/Arc.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls.Shapes } /// - /// Gets or sets the angle at which the arc ends relative to the start angle, in degrees. + /// Gets or sets the angle, in degrees, added to the defining where the arc ends. A positive value is clockwise, negative is counter-clockwise. /// public double SweepAngle { @@ -54,7 +54,7 @@ namespace Avalonia.Controls.Shapes var rect = new Rect(Bounds.Size); - if ((normStart == normEnd) && (startAngle != sweepAngle)) //complete ring + if ((normStart == normEnd) && (startAngle != sweepAngle)) // complete ring { return new EllipseGeometry(rect.Deflate(StrokeThickness / 2)); } @@ -62,7 +62,7 @@ namespace Avalonia.Controls.Shapes { return new StreamGeometry(); } - else //partial arc + else // partial arc { var deflatedRect = rect.Deflate(StrokeThickness / 2); From ea3f85e1263cfb75ce2452e205477080d1c9b531 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Jul 2021 09:34:18 +0200 Subject: [PATCH 10/27] Don't allow using a closed window as a parent/owner. --- src/Avalonia.Controls/Window.cs | 10 +++++++ .../WindowTests.cs | 30 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 700c3d9bad..2d369dae8c 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -635,6 +635,11 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot re-show a closed window."); } + if (parent != null && parent.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot Show a Window with a closed parent."); + } + if (IsVisible) { return; @@ -709,6 +714,11 @@ namespace Avalonia.Controls throw new ArgumentNullException(nameof(owner)); } + if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot Show a Window with a closed owner."); + } + if (IsVisible) { throw new InvalidOperationException("The window is already being shown."); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index e8311b79ac..2d4559ddba 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -387,6 +387,36 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Calling_Show_With_Closed_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + parent.Close(); + + var ex = Assert.Throws(() => target.Show(parent)); + Assert.Equal("Cannot Show a Window with a closed parent.", ex.Message); + } + } + + [Fact] + public async Task Calling_ShowDialog_With_Closed_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + parent.Close(); + + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); + Assert.Equal("Cannot Show a Window with a closed owner.", ex.Message); + } + } + [Fact] public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() { From 69852a56f588fe0e744aa11e7577eeb351f29670 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Jul 2021 12:16:44 +0200 Subject: [PATCH 11/27] Don't allow self as parent/owner window. --- src/Avalonia.Controls/Window.cs | 17 +++++++++++-- .../WindowTests.cs | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 2d369dae8c..9f6c605d46 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -635,9 +635,17 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot re-show a closed window."); } - if (parent != null && parent.PlatformImpl == null) + if (parent != null) { - throw new InvalidOperationException("Cannot Show a Window with a closed parent."); + if (parent.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot Show a Window with a closed parent."); + } + + if (parent == this) + { + throw new InvalidOperationException("A Window cannot be its own parent."); + } } if (IsVisible) @@ -719,6 +727,11 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot Show a Window with a closed owner."); } + if (owner == this) + { + throw new InvalidOperationException("A Window cannot be its own owner."); + } + if (IsVisible) { throw new InvalidOperationException("The window is already being shown."); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 2d4559ddba..5ba529292f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -417,6 +417,30 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Calling_Show_With_Self_As_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + + var ex = Assert.Throws(() => target.Show(target)); + Assert.Equal("A Window cannot be its own parent.", ex.Message); + } + } + + [Fact] + public async Task Calling_ShowDialog_With_Self_As_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(target)); + Assert.Equal("A Window cannot be its own owner.", ex.Message); + } + } + [Fact] public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() { From 15cdee1bee18cc7466f68951f7bfa863fee748be Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Jul 2021 13:27:01 +0200 Subject: [PATCH 12/27] Ensure parent/owner windows are visible when showing child. --- src/Avalonia.Controls/Window.cs | 24 +++++---- .../WindowTests.cs | 50 +++++++++++++++---- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 9f6c605d46..ddda083aa8 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -639,13 +639,16 @@ namespace Avalonia.Controls { if (parent.PlatformImpl == null) { - throw new InvalidOperationException("Cannot Show a Window with a closed parent."); + throw new InvalidOperationException("Cannot show a window with a closed parent."); } - - if (parent == this) + else if (parent == this) { throw new InvalidOperationException("A Window cannot be its own parent."); } + else if (!parent.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } } if (IsVisible) @@ -721,21 +724,22 @@ namespace Avalonia.Controls { throw new ArgumentNullException(nameof(owner)); } - - if (owner.PlatformImpl == null) + else if (owner.PlatformImpl == null) { - throw new InvalidOperationException("Cannot Show a Window with a closed owner."); + throw new InvalidOperationException("Cannot show a window with a closed owner."); } - - if (owner == this) + else if (owner == this) { throw new InvalidOperationException("A Window cannot be its own owner."); } - - if (IsVisible) + else if (IsVisible) { throw new InvalidOperationException("The window is already being shown."); } + else if (!owner.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 5ba529292f..88c6c86c46 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -279,10 +279,11 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = Mock.Of(); + var parent = new Window(); var renderer = new Mock(); var target = new Window(CreateImpl(renderer)); + parent.Show(); target.ShowDialog(parent); renderer.Verify(x => x.Start(), Times.Once); @@ -294,10 +295,11 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = Mock.Of(); + var parent = new Window(); var target = new Window(); var raised = false; + parent.Show(); target.Opened += (s, e) => raised = true; target.ShowDialog(parent); @@ -326,14 +328,15 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = new Mock(); + var parent = new Window(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); + parent.Show(); var target = new Window(windowImpl.Object); - var task = target.ShowDialog(parent.Object); + var task = target.ShowDialog(parent); windowImpl.Object.Closed(); @@ -366,14 +369,16 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = new Mock(); + var parent = new Window(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); + parent.Show(); + var target = new Window(windowImpl.Object); - var task = target.ShowDialog(parent.Object); + var task = target.ShowDialog(parent); windowImpl.Object.Closed(); await task; @@ -381,7 +386,7 @@ namespace Avalonia.Controls.UnitTests var openedRaised = false; target.Opened += (s, e) => openedRaised = true; - var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent.Object)); + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); Assert.Equal("Cannot re-show a closed window.", ex.Message); Assert.False(openedRaised); } @@ -398,7 +403,7 @@ namespace Avalonia.Controls.UnitTests parent.Close(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot Show a Window with a closed parent.", ex.Message); + Assert.Equal("Cannot show a window with a closed parent.", ex.Message); } } @@ -413,7 +418,33 @@ namespace Avalonia.Controls.UnitTests parent.Close(); var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); - Assert.Equal("Cannot Show a Window with a closed owner.", ex.Message); + Assert.Equal("Cannot show a window with a closed owner.", ex.Message); + } + } + + [Fact] + public void Calling_Show_With_Invisible_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + var ex = Assert.Throws(() => target.Show(parent)); + Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + } + } + + [Fact] + public async Task Calling_ShowDialog_With_Invisible_Parent_Window_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + var target = new Window(); + + var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); + Assert.Equal("Cannot show window with non-visible parent.", ex.Message); } } @@ -740,6 +771,7 @@ namespace Avalonia.Controls.UnitTests protected override void Show(Window window) { var owner = new Window(); + owner.Show(); window.ShowDialog(owner); } } From a42334d128eb919c6f29de6c7d51cd333b4221a0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Jul 2021 14:00:09 +0200 Subject: [PATCH 13/27] Hide child windows when hiding parent/owner. --- src/Avalonia.Controls/Window.cs | 8 +++++ .../WindowTests.cs | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ddda083aa8..ae314a33ce 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -592,6 +592,14 @@ namespace Avalonia.Controls owner.RemoveChild(this); } + if (_children.Count > 0) + { + foreach (var child in _children.ToArray()) + { + child.child.Hide(); + } + } + Owner = null; PlatformImpl?.Hide(); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 88c6c86c46..6b9921d83d 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -472,6 +472,42 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Hiding_Parent_Window_Should_Close_Children() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var parent = new Window(); + var child = new Window(); + + parent.Show(); + child.Show(parent); + + parent.Hide(); + + Assert.False(parent.IsVisible); + Assert.False(child.IsVisible); + } + } + + [Fact] + public void Hiding_Parent_Window_Should_Close_Dialog_Children() + { + using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + { + var parent = new Window(); + var child = new Window(); + + parent.Show(); + child.ShowDialog(parent); + + parent.Hide(); + + Assert.False(parent.IsVisible); + Assert.False(child.IsVisible); + } + } + [Fact] public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() { From f2907c4f49ff1f8b0d77e828154d49342d36178f Mon Sep 17 00:00:00 2001 From: Royce551 Date: Wed, 7 Jul 2021 22:54:10 -0500 Subject: [PATCH 14/27] Implement ItemsControl.IsTextSearchEnabled --- src/Avalonia.Controls/ItemsControl.cs | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 20d032f597..c544f753e5 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; @@ -12,6 +13,7 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -52,6 +54,11 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); + public static readonly StyledProperty IsTextSearchEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); + + private string _textSearchTerm = string.Empty; + private DispatcherTimer _textSearchTimer; private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; @@ -135,6 +142,12 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } + public bool IsTextSearchEnabled + { + get { return GetValue(IsTextSearchEnabledProperty); } + set { SetValue(IsTextSearchEnabledProperty, value); } + } + /// /// Gets the items presenter control. /// @@ -323,6 +336,36 @@ namespace Avalonia.Controls base.OnKeyDown(e); } + protected override void OnTextInput(TextInputEventArgs e) + { + if (!e.Handled && this is SelectingItemsControl selectingItemsControl) + { + if (!IsTextSearchEnabled) + return; + + StopTextSearchTimer(); + + _textSearchTerm += e.Text; + + bool match(ItemContainerInfo info) => + info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + + var info = ItemContainerGenerator.Containers.FirstOrDefault(match); + + if (info != null) + { + selectingItemsControl.SelectedIndex = info.Index; + } + + StartTextSearchTimer(); + + e.Handled = true; + } + + base.OnTextInput(e); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -456,6 +499,32 @@ namespace Avalonia.Controls } } + private void StartTextSearchTimer() + { + _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _textSearchTimer.Tick += TextSearchTimer_Tick; + _textSearchTimer.Start(); + } + + private void StopTextSearchTimer() + { + if (_textSearchTimer == null) + { + return; + } + + _textSearchTimer.Stop(); + _textSearchTimer.Tick -= TextSearchTimer_Tick; + + _textSearchTimer = null; + } + + private void TextSearchTimer_Tick(object sender, EventArgs e) + { + _textSearchTerm = string.Empty; + StopTextSearchTimer(); + } + private void UpdateItemCount() { if (Items == null) From 875655e3f3bd051fbb1840d6c8f3bab03891d255 Mon Sep 17 00:00:00 2001 From: Royce551 Date: Wed, 7 Jul 2021 23:04:43 -0500 Subject: [PATCH 15/27] Remove IsTextSearchEnabled implemenetation from ComboBox --- src/Avalonia.Controls/ComboBox.cs | 69 ------------------------------- 1 file changed, 69 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index dd2109e6cd..89cfb5fa8f 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -77,14 +77,6 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); - - private string _textSearchTerm = string.Empty; - private DispatcherTimer _textSearchTimer; private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; @@ -173,15 +165,6 @@ namespace Avalonia.Controls set { SetValue(VerticalContentAlignmentProperty, value); } } - /// - /// Gets or sets a value that specifies whether a user can jump to a value by typing. - /// - public bool IsTextSearchEnabled - { - get { return GetValue(IsTextSearchEnabledProperty); } - set { SetValue(IsTextSearchEnabledProperty, value); } - } - /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -247,32 +230,6 @@ namespace Avalonia.Controls } } - /// - protected override void OnTextInput(TextInputEventArgs e) - { - if (!IsTextSearchEnabled || e.Handled) - return; - - StopTextSearchTimer(); - - _textSearchTerm += e.Text; - - bool match(ItemContainerInfo info) => - info.ContainerControl is IContentControl control && - control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; - - var info = ItemContainerGenerator.Containers.FirstOrDefault(match); - - if (info != null) - { - SelectedIndex = info.Index; - } - - StartTextSearchTimer(); - - e.Handled = true; - } - /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { @@ -470,31 +427,5 @@ namespace Avalonia.Controls SelectedIndex = prev; } - - private void StartTextSearchTimer() - { - _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; - _textSearchTimer.Tick += TextSearchTimer_Tick; - _textSearchTimer.Start(); - } - - private void StopTextSearchTimer() - { - if (_textSearchTimer == null) - { - return; - } - - _textSearchTimer.Stop(); - _textSearchTimer.Tick -= TextSearchTimer_Tick; - - _textSearchTimer = null; - } - - private void TextSearchTimer_Tick(object sender, EventArgs e) - { - _textSearchTerm = string.Empty; - StopTextSearchTimer(); - } } } From 4653c8ff607b7d82c8535472f0d0fecfdc67f302 Mon Sep 17 00:00:00 2001 From: Royce551 Date: Wed, 7 Jul 2021 23:14:13 -0500 Subject: [PATCH 16/27] Readd xmldocs --- src/Avalonia.Controls/ItemsControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index c544f753e5..c86f2e591b 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -54,6 +54,9 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); + /// + /// Defines the property. + /// public static readonly StyledProperty IsTextSearchEnabledProperty = AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); @@ -142,6 +145,9 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } + /// + /// Gets or sets a value that specifies whether a user can jump to a value by typing. + /// public bool IsTextSearchEnabled { get { return GetValue(IsTextSearchEnabledProperty); } From f5ded2632133803632116efad9e5465f4811fee8 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:07:59 +0800 Subject: [PATCH 17/27] Update Arc.cs --- src/Avalonia.Controls/Shapes/Arc.cs | 47 +++++++++++++++-------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Arc.cs b/src/Avalonia.Controls/Shapes/Arc.cs index 4ce72319aa..dad47875c3 100644 --- a/src/Avalonia.Controls/Shapes/Arc.cs +++ b/src/Avalonia.Controls/Shapes/Arc.cs @@ -28,33 +28,34 @@ namespace Avalonia.Controls.Shapes /// public double StartAngle { - get { return GetValue(StartAngleProperty); } - set { SetValue(StartAngleProperty, value); } + get => GetValue(StartAngleProperty); + set => SetValue(StartAngleProperty, value); } /// - /// Gets or sets the angle, in degrees, added to the defining where the arc ends. A positive value is clockwise, negative is counter-clockwise. + /// Gets or sets the angle, in degrees, added to the defining where the arc ends. + /// A positive value is clockwise, negative is counter-clockwise. /// public double SweepAngle { - get { return GetValue(SweepAngleProperty); } - set { SetValue(SweepAngleProperty, value); } + get => GetValue(SweepAngleProperty); + set => SetValue(SweepAngleProperty, value); } protected override Geometry CreateDefiningGeometry() { - double angle1 = DegreesToRad(StartAngle); - double angle2 = angle1 + DegreesToRad(SweepAngle); + var angle1 = DegreesToRad(StartAngle); + var angle2 = angle1 + DegreesToRad(SweepAngle); - double startAngle = Math.Min(angle1, angle2); - double sweepAngle = Math.Max(angle1, angle2); + var startAngle = Math.Min(angle1, angle2); + var sweepAngle = Math.Max(angle1, angle2); - double normStart = RadToNormRad(startAngle); - double normEnd = RadToNormRad(sweepAngle); + var normStart = RadToNormRad(startAngle); + var normEnd = RadToNormRad(sweepAngle); var rect = new Rect(Bounds.Size); - if ((normStart == normEnd) && (startAngle != sweepAngle)) // complete ring + if ((normStart == normEnd) && (startAngle != sweepAngle)) // Complete ring. { return new EllipseGeometry(rect.Deflate(StrokeThickness / 2)); } @@ -62,28 +63,30 @@ namespace Avalonia.Controls.Shapes { return new StreamGeometry(); } - else // partial arc + else // Partial arc. { var deflatedRect = rect.Deflate(StrokeThickness / 2); - double centerX = rect.Center.X; - double centerY = rect.Center.Y; + var centerX = rect.Center.X; + var centerY = rect.Center.Y; - double radiusX = deflatedRect.Width / 2; - double radiusY = deflatedRect.Height / 2; + var radiusX = deflatedRect.Width / 2; + var radiusY = deflatedRect.Height / 2; - double angleGap = RadToNormRad(sweepAngle - startAngle); + var angleGap = RadToNormRad(sweepAngle - startAngle); - Point startPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, startAngle); - Point endPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, sweepAngle); + var startPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, startAngle); + var endPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, sweepAngle); - StreamGeometry arcGeometry = new StreamGeometry(); + var arcGeometry = new StreamGeometry(); + using (var ctx = arcGeometry.Open()) { ctx.BeginFigure(startPoint, false); ctx.ArcTo(endPoint, new Size(radiusX, radiusY), angleGap, angleGap >= Math.PI, SweepDirection.Clockwise); ctx.EndFigure(false); } + return arcGeometry; } } @@ -91,7 +94,7 @@ namespace Avalonia.Controls.Shapes static double DegreesToRad(double inAngle) => inAngle * Math.PI / 180; - static double RadToNormRad(double inAngle) => (0 + (inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); + static double RadToNormRad(double inAngle) => (inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); static Point GetRingPoint(double radiusX, double radiusY, double centerX, double centerY, double angle) => new Point((radiusX * Math.Cos(angle)) + centerX, (radiusY * Math.Sin(angle)) + centerY); From 3740c9d9733693e118454aca541c671bce110a85 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:33:18 +0800 Subject: [PATCH 18/27] Update Arc.cs --- src/Avalonia.Controls/Shapes/Arc.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Arc.cs b/src/Avalonia.Controls/Shapes/Arc.cs index dad47875c3..5ebb321f9b 100644 --- a/src/Avalonia.Controls/Shapes/Arc.cs +++ b/src/Avalonia.Controls/Shapes/Arc.cs @@ -41,12 +41,12 @@ namespace Avalonia.Controls.Shapes get => GetValue(SweepAngleProperty); set => SetValue(SweepAngleProperty, value); } - + protected override Geometry CreateDefiningGeometry() { var angle1 = DegreesToRad(StartAngle); var angle2 = angle1 + DegreesToRad(SweepAngle); - + var startAngle = Math.Min(angle1, angle2); var sweepAngle = Math.Max(angle1, angle2); @@ -72,29 +72,30 @@ namespace Avalonia.Controls.Shapes var radiusX = deflatedRect.Width / 2; var radiusY = deflatedRect.Height / 2; - + var angleGap = RadToNormRad(sweepAngle - startAngle); var startPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, startAngle); var endPoint = GetRingPoint(radiusX, radiusY, centerX, centerY, sweepAngle); var arcGeometry = new StreamGeometry(); - + using (var ctx = arcGeometry.Open()) { ctx.BeginFigure(startPoint, false); - ctx.ArcTo(endPoint, new Size(radiusX, radiusY), angleGap, angleGap >= Math.PI, SweepDirection.Clockwise); + ctx.ArcTo(endPoint, new Size(radiusX, radiusY), angleGap, angleGap >= Math.PI, + SweepDirection.Clockwise); ctx.EndFigure(false); } - + return arcGeometry; } } static double DegreesToRad(double inAngle) => inAngle * Math.PI / 180; - - static double RadToNormRad(double inAngle) => (inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); + + static double RadToNormRad(double inAngle) => ((inAngle % (Math.PI * 2)) + (Math.PI * 2)) % (Math.PI * 2); static Point GetRingPoint(double radiusX, double radiusY, double centerX, double centerY, double angle) => new Point((radiusX * Math.Cos(angle)) + centerX, (radiusY * Math.Sin(angle)) + centerY); From 975e48d3a2afb16d072a1f78c9735e5e2cb4d3bd Mon Sep 17 00:00:00 2001 From: Royce551 Date: Thu, 8 Jul 2021 08:11:27 -0500 Subject: [PATCH 19/27] Move to SelectingItemsControl --- src/Avalonia.Controls/ItemsControl.cs | 74 ------------------- .../Primitives/SelectingItemsControl.cs | 74 +++++++++++++++++++ 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index c86f2e591b..55645d4dbb 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -13,7 +13,6 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; -using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -54,14 +53,6 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsTextSearchEnabledProperty = - AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); - - private string _textSearchTerm = string.Empty; - private DispatcherTimer _textSearchTimer; private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; @@ -145,15 +136,6 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } - /// - /// Gets or sets a value that specifies whether a user can jump to a value by typing. - /// - public bool IsTextSearchEnabled - { - get { return GetValue(IsTextSearchEnabledProperty); } - set { SetValue(IsTextSearchEnabledProperty, value); } - } - /// /// Gets the items presenter control. /// @@ -342,36 +324,6 @@ namespace Avalonia.Controls base.OnKeyDown(e); } - protected override void OnTextInput(TextInputEventArgs e) - { - if (!e.Handled && this is SelectingItemsControl selectingItemsControl) - { - if (!IsTextSearchEnabled) - return; - - StopTextSearchTimer(); - - _textSearchTerm += e.Text; - - bool match(ItemContainerInfo info) => - info.ContainerControl is IContentControl control && - control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; - - var info = ItemContainerGenerator.Containers.FirstOrDefault(match); - - if (info != null) - { - selectingItemsControl.SelectedIndex = info.Index; - } - - StartTextSearchTimer(); - - e.Handled = true; - } - - base.OnTextInput(e); - } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -505,32 +457,6 @@ namespace Avalonia.Controls } } - private void StartTextSearchTimer() - { - _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; - _textSearchTimer.Tick += TextSearchTimer_Tick; - _textSearchTimer.Start(); - } - - private void StopTextSearchTimer() - { - if (_textSearchTimer == null) - { - return; - } - - _textSearchTimer.Stop(); - _textSearchTimer.Tick -= TextSearchTimer_Tick; - - _textSearchTimer = null; - } - - private void TextSearchTimer_Tick(object sender, EventArgs e) - { - _textSearchTerm = string.Empty; - StopTextSearchTimer(); - } - private void UpdateItemCount() { if (Items == null) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 34d3347434..1207b7f2dc 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -10,6 +10,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Threading; using Avalonia.VisualTree; #nullable enable @@ -91,6 +92,12 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register( nameof(SelectionMode)); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsTextSearchEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextSearchEnabled), true); + /// /// Event that should be raised by items that implement to /// notify the parent that their selection state @@ -110,6 +117,8 @@ namespace Avalonia.Controls.Primitives RoutingStrategies.Bubble); private static readonly IList Empty = Array.Empty(); + private string _textSearchTerm = string.Empty; + private DispatcherTimer? _textSearchTimer; private ISelectionModel? _selection; private int _oldSelectedIndex; private object? _oldSelectedItem; @@ -305,6 +314,15 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Gets or sets a value that specifies whether a user can jump to a value by typing. + /// + public bool IsTextSearchEnabled + { + get { return GetValue(IsTextSearchEnabledProperty); } + set { SetValue(IsTextSearchEnabledProperty, value); } + } + /// /// Gets or sets the selection mode. /// @@ -490,6 +508,36 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnTextInput(TextInputEventArgs e) + { + if (!e.Handled) + { + if (!IsTextSearchEnabled) + return; + + StopTextSearchTimer(); + + _textSearchTerm += e.Text; + + bool match(ItemContainerInfo info) => + info.ContainerControl is IContentControl control && + control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + + var info = ItemContainerGenerator.Containers.FirstOrDefault(match); + + if (info != null) + { + SelectedIndex = info.Index; + } + + StartTextSearchTimer(); + + e.Handled = true; + } + + base.OnTextInput(e); + } + protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); @@ -962,6 +1010,32 @@ namespace Avalonia.Controls.Primitives } } + private void StartTextSearchTimer() + { + _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _textSearchTimer.Tick += TextSearchTimer_Tick; + _textSearchTimer.Start(); + } + + private void StopTextSearchTimer() + { + if (_textSearchTimer == null) + { + return; + } + + _textSearchTimer.Stop(); + _textSearchTimer.Tick -= TextSearchTimer_Tick; + + _textSearchTimer = null; + } + + private void TextSearchTimer_Tick(object sender, EventArgs e) + { + _textSearchTerm = string.Empty; + StopTextSearchTimer(); + } + // When in a BeginInit..EndInit block, or when the DataContext is updating, we need to // defer changes to the selection model because we have no idea in which order properties // will be set. Consider: From 0864f3c562f70f7d9900914d388714360e18819d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Marki=C4=8D?= Date: Sat, 10 Jul 2021 14:24:32 +0200 Subject: [PATCH 20/27] Makes TextBox.UndoRedoState struct readonly --- src/Avalonia.Controls/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index c1516613b3..ddf55808ce 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -145,7 +145,7 @@ namespace Avalonia.Controls (o, v) => o.UndoLimit = v, unsetValue: -1); - struct UndoRedoState : IEquatable + readonly struct UndoRedoState : IEquatable { public string Text { get; } public int CaretPosition { get; } From cd54a9255df1e6f66c13cdf7a839df83d7b104dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Marki=C4=8D?= Date: Sat, 10 Jul 2021 14:30:28 +0200 Subject: [PATCH 21/27] Adds CodeRush directory to GIT ignored files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7d672c7755..abf7674560 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,9 @@ _NCrunch_*/ *.ncrunchsolution.user nCrunchTemp_* +# CodeRush +.cr/ + # Others sql/ *.Cache From 43083edd2f9380c39ed9d2089c57e9286c67e795 Mon Sep 17 00:00:00 2001 From: Royce551 Date: Sat, 10 Jul 2021 10:57:28 -0500 Subject: [PATCH 22/27] Unsubscribe before stopping timer --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 1207b7f2dc..2fd08ef77c 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1024,8 +1024,8 @@ namespace Avalonia.Controls.Primitives return; } - _textSearchTimer.Stop(); _textSearchTimer.Tick -= TextSearchTimer_Tick; + _textSearchTimer.Stop(); _textSearchTimer = null; } From 17d9755c3f22f3a20b09996fc7d72e2ffd3f406e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 10 Jul 2021 21:43:19 +0200 Subject: [PATCH 23/27] Update API compat. --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index c917902dc3..39a4c3004c 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -5,6 +5,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract. MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. @@ -73,4 +74,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 74 +Total Issues: 75 From e55c11ec37a1fb87fe7e376c86919f69936700ea Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 10 Jul 2021 21:57:53 +0200 Subject: [PATCH 24/27] Fix unit tests. --- .../StaticResourceExtensionTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index 0ee7384c7d..fb3fd6d7d4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -29,7 +29,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); var border = userControl.FindControl("border"); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); var border = userControl.FindControl("border"); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -119,7 +119,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); var border = userControl.FindControl("border"); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -149,7 +149,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); var border = userControl.FindControl("border"); - var brush = (SolidColorBrush)border.Background; + var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } @@ -200,7 +200,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var button = window.FindControl