From 9250ccb18afb5d538b6e300a05c606da2d38d3a6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 13:35:13 -0400 Subject: [PATCH 01/44] Add missing base constructor call --- .../ColorSpectrum/ColorSpectrum.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index bd44161a42..200a652d3b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives /// /// Initializes a new instance of the class. /// - public ColorSpectrum() + public ColorSpectrum() : base() { _shapeFromLastBitmapCreation = Shape; _componentsFromLastBitmapCreation = Components; From 5c3a56a9533eab9060a216831df2a9a8c5e08e12 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 14:25:02 -0400 Subject: [PATCH 02/44] Fix the color spectrum selection ellipse position Specifically, when updated and not part of the visual tree --- .../ColorSpectrum/ColorSpectrum.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 200a652d3b..9261e5892d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -171,6 +171,18 @@ namespace Avalonia.Controls.Primitives { base.OnAttachedToVisualTree(e); + // If the color was updated while this ColorSpectrum was not part of the visual tree, + // the selection ellipse may be in an incorrect position. This is because the spectrum + // renders based on layout scaling to avoid color banding; however, layout scale is only + // available when the control is attached to the visual tree. The ColorSpectrum's color + // may be updated from code-behind or from binding with another control when it's not + // part of the visual tree. + // + // See discussion: https://github.com/AvaloniaUI/Avalonia/discussions/9077 + // + // To work-around this issue the selection ellipse is refreshed here. + UpdateEllipse(); + // OnAttachedToVisualTree is called after OnApplyTemplate so events cannot be connected here } @@ -832,6 +844,8 @@ namespace Avalonia.Controls.Primitives } // Remember the bitmap size follows physical device pixels + // Warning: LayoutHelper.GetLayoutScale() doesn't work unless the control is visible + // This will not be true in all cases if the color is updated from another control or code-behind var scale = LayoutHelper.GetLayoutScale(this); Canvas.SetLeft(_selectionEllipsePanel, (xPosition / scale) - (_selectionEllipsePanel.Width / 2)); Canvas.SetTop(_selectionEllipsePanel, (yPosition / scale) - (_selectionEllipsePanel.Height / 2)); From 55e27213f958304679d449ef4aeb25ca9e8743ea Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 14:25:26 -0400 Subject: [PATCH 03/44] Comment additional ColorSpectrum methods --- .../ColorSpectrum/ColorSpectrum.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 9261e5892d..f272ce850e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -600,6 +600,10 @@ namespace Avalonia.Controls.Primitives RaiseColorChanged(); } + /// + /// Updates the selected and based on a point within the color spectrum. + /// + /// The point on the spectrum representing the color. private void UpdateColorFromPoint(PointerPoint point) { // If we haven't initialized our HSV value array yet, then we should just ignore any user input - @@ -676,6 +680,9 @@ namespace Avalonia.Controls.Primitives UpdateColor(hsvAtPoint); } + /// + /// Updates the position of the selection ellipse on the spectrum which indicates the selected color. + /// private void UpdateEllipse() { if (_selectionEllipsePanel == null) From 8b070919398878acda991efc0140f7a02cd2a8b7 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 9 Oct 2022 12:21:08 -0400 Subject: [PATCH 04/44] Fix formatting --- .../Themes/Fluent/ColorView.xaml | 30 +++++++++---------- .../Themes/Simple/ColorView.xaml | 30 +++++++++---------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 16bc2acdd1..cf836aff4a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -241,23 +241,21 @@ - + - + - + - + Date: Sun, 2 Oct 2022 11:51:25 -0400 Subject: [PATCH 05/44] Implement WriteableBitmap caching/reuse and disposal in ColorSlider --- .../ColorSlider/ColorSlider.cs | 42 ++++++++++++++++++- .../Helpers/ColorPickerHelpers.cs | 19 ++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index b662d20223..4e907f610d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.Utilities; namespace Avalonia.Controls.Primitives @@ -31,6 +32,8 @@ namespace Avalonia.Controls.Primitives protected bool ignorePropertyChanged = false; + private WriteableBitmap? _backgroundBitmap; + /// /// Initializes a new instance of the class. /// @@ -38,6 +41,26 @@ namespace Avalonia.Controls.Primitives { } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + // Bitmaps were released when detached from the visual tree so they must be re-built + UpdateBackground(); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + // Clean-up all bitmaps + // https://github.com/AvaloniaUI/Avalonia/issues/9051 + _backgroundBitmap?.Dispose(); + _backgroundBitmap = null; + } + /// /// Updates the visual state of the control by applying latest PseudoClasses. /// @@ -110,7 +133,19 @@ namespace Avalonia.Controls.Primitives if (bitmap != null) { - Background = new ImageBrush(ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight)); + if (_backgroundBitmap != null) + { + // Re-use the existing WriteableBitmap + // This assumes the height, width and byte counts are the same and must be set to null + // elsewhere if that assumption is ever not true. + ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bitmap); + } + else + { + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight); + } + + Background = new ImageBrush(_backgroundBitmap); } } } @@ -399,6 +434,11 @@ namespace Avalonia.Controls.Primitives } else if (change.Property == BoundsProperty) { + // If the control's overall dimensions have changed the background bitmap size also needs to change. + // This means the existing bitmap must be released to be recreated correctly in UpdateBackground(). + _backgroundBitmap?.Dispose(); + _backgroundBitmap = null; + UpdateBackground(); } else if (change.Property == ValueProperty || diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index c1904a3c30..f2683e4677 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -617,7 +617,6 @@ namespace Avalonia.Controls.Primitives PixelFormat.Bgra8888, AlphaFormat.Premul); - // Warning: This is highly questionable using (var frameBuffer = bitmap.Lock()) { Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); @@ -625,5 +624,23 @@ namespace Avalonia.Controls.Primitives return bitmap; } + + /// + /// Updates the given with new, raw BGRA pre-multiplied alpha pixel data. + /// WARNING: The bitmap's width, height and byte count MUST not have changed and MUST be enforced externally. + /// + /// The existing to update. + /// The bitmap (in raw BGRA pre-multiplied alpha pixels). + public static void UpdateBitmapFromPixelData( + WriteableBitmap bitmap, + IList bgraPixelData) + { + using (var frameBuffer = bitmap.Lock()) + { + Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + } + + return; + } } } From ca7543f1d613f9d7b9f2cfed978f8e23379d6684 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:24:10 -0400 Subject: [PATCH 06/44] Add new ArrayList to avoid an extra copy when creating spectrum bitmaps --- .../ColorSlider/ColorSlider.cs | 8 +-- .../ColorSpectrum/ColorSpectrum.cs | 61 +++++++++------- .../Helpers/ArrayList.cs | 71 +++++++++++++++++++ .../Helpers/ColorPickerHelpers.cs | 18 ++--- 4 files changed, 118 insertions(+), 40 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 4e907f610d..8bcdef5558 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -121,7 +121,7 @@ namespace Avalonia.Controls.Primitives if (pixelWidth != 0 && pixelHeight != 0) { - var bitmap = await ColorPickerHelpers.CreateComponentBitmapAsync( + ArrayList bgraPixelData = await ColorPickerHelpers.CreateComponentBitmapAsync( pixelWidth, pixelHeight, Orientation, @@ -131,18 +131,18 @@ namespace Avalonia.Controls.Primitives IsAlphaMaxForced, IsSaturationValueMaxForced); - if (bitmap != null) + if (bgraPixelData != null) { if (_backgroundBitmap != null) { // Re-use the existing WriteableBitmap // This assumes the height, width and byte counts are the same and must be set to null // elsewhere if that assumption is ever not true. - ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bitmap); + ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); } else { - _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight); + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); } Background = new ImageBrush(_backgroundBitmap); diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index f272ce850e..3604894833 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -994,13 +994,13 @@ namespace Avalonia.Controls.Primitives // The middle 4 are only needed and used in the case of hue as the third dimension. // Saturation and luminosity need only a min and max. - List bgraMinPixelData = new List(); - List bgraMiddle1PixelData = new List(); - List bgraMiddle2PixelData = new List(); - List bgraMiddle3PixelData = new List(); - List bgraMiddle4PixelData = new List(); - List bgraMaxPixelData = new List(); - List newHsvValues = new List(); + ArrayList bgraMinPixelData; + ArrayList bgraMiddle1PixelData; + ArrayList bgraMiddle2PixelData; + ArrayList bgraMiddle3PixelData; + ArrayList bgraMiddle4PixelData; + ArrayList bgraMaxPixelData; + List newHsvValues; // In Avalonia, Bounds returns the actual device-independent pixel size of a control. // However, this is not necessarily the size of the control rendered on a display. @@ -1011,20 +1011,27 @@ namespace Avalonia.Controls.Primitives int pixelDimension = (int)Math.Round(minDimension * scale); var pixelCount = pixelDimension * pixelDimension; var pixelDataSize = pixelCount * 4; - bgraMinPixelData.Capacity = pixelDataSize; + + bgraMinPixelData = new ArrayList(pixelDataSize); + bgraMaxPixelData = new ArrayList(pixelDataSize); + newHsvValues = new List(pixelCount); // We'll only save pixel data for the middle bitmaps if our third dimension is hue. if (components == ColorSpectrumComponents.ValueSaturation || components == ColorSpectrumComponents.SaturationValue) { - bgraMiddle1PixelData.Capacity = pixelDataSize; - bgraMiddle2PixelData.Capacity = pixelDataSize; - bgraMiddle3PixelData.Capacity = pixelDataSize; - bgraMiddle4PixelData.Capacity = pixelDataSize; + bgraMiddle1PixelData = new ArrayList(pixelDataSize); + bgraMiddle2PixelData = new ArrayList(pixelDataSize); + bgraMiddle3PixelData = new ArrayList(pixelDataSize); + bgraMiddle4PixelData = new ArrayList(pixelDataSize); + } + else + { + bgraMiddle1PixelData = new ArrayList(0); + bgraMiddle2PixelData = new ArrayList(0); + bgraMiddle3PixelData = new ArrayList(0); + bgraMiddle4PixelData = new ArrayList(0); } - - bgraMaxPixelData.Capacity = pixelDataSize; - newHsvValues.Capacity = pixelCount; await Task.Run(() => { @@ -1132,12 +1139,12 @@ namespace Avalonia.Controls.Primitives double maxSaturation, double minValue, double maxValue, - List bgraMinPixelData, - List bgraMiddle1PixelData, - List bgraMiddle2PixelData, - List bgraMiddle3PixelData, - List bgraMiddle4PixelData, - List bgraMaxPixelData, + ArrayList bgraMinPixelData, + ArrayList bgraMiddle1PixelData, + ArrayList bgraMiddle2PixelData, + ArrayList bgraMiddle3PixelData, + ArrayList bgraMiddle4PixelData, + ArrayList bgraMaxPixelData, List newHsvValues) { double hMin = minHue; @@ -1292,12 +1299,12 @@ namespace Avalonia.Controls.Primitives double maxSaturation, double minValue, double maxValue, - List bgraMinPixelData, - List bgraMiddle1PixelData, - List bgraMiddle2PixelData, - List bgraMiddle3PixelData, - List bgraMiddle4PixelData, - List bgraMaxPixelData, + ArrayList bgraMinPixelData, + ArrayList bgraMiddle1PixelData, + ArrayList bgraMiddle2PixelData, + ArrayList bgraMiddle3PixelData, + ArrayList bgraMiddle4PixelData, + ArrayList bgraMaxPixelData, List newHsvValues) { double hMin = minHue; diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs new file mode 100644 index 0000000000..0b4c2f8579 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs @@ -0,0 +1,71 @@ +namespace Avalonia.Controls.Primitives +{ + /// + /// A thin wrapper over an that allows some additional list-like functionality. + /// + /// + /// This is only for internal ColorPicker-related functionality and should not be used elsewhere. + /// It is added for performance to enjoy the simplicity of the IList.Add() method without requiring + /// an additional copy to turn a list into an array for bitmaps. + /// + /// The type of items in the array. + internal class ArrayList + { + private int _nextIndex = 0; + + /// + /// Initializes a new instance of the class. + /// + public ArrayList(int capacity) + { + Capacity = capacity; + Array = new T[capacity]; + } + + /// + /// Provides access to the underlying array by index. + /// This exists for simplification and the property + /// may also be used. + /// + /// The index of the item to get or set. + /// The item at the given index. + public T this[int i] + { + get => Array[i]; + set => Array[i] = value; + } + + /// + /// Gets the underlying array. + /// + public T[] Array { get; private set; } + + /// + /// Gets the fixed capacity/size of the array. + /// This must be set during construction. + /// + public int Capacity { get; private set; } + + /// + /// Adds the given item to the array at the next available index. + /// WARNING: This must be used carefully and only once, in sequence. + /// + /// The item to add. + public void Add(T item) + { + if (_nextIndex >= 0 && + _nextIndex < Capacity) + { + Array[_nextIndex] = item; + _nextIndex++; + } + else + { + // If necessary an exception could be thrown here + // throw new IndexOutOfRangeException(); + } + + return; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index f2683e4677..2378813f52 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.Primitives /// during calculation with the HSVA color model. /// This will ensure colors are always discernible regardless of saturation/value. /// A new bitmap representing a gradient of color component values. - public static async Task CreateComponentBitmapAsync( + public static async Task> CreateComponentBitmapAsync( int width, int height, Orientation orientation, @@ -49,14 +49,14 @@ namespace Avalonia.Controls.Primitives { if (width == 0 || height == 0) { - return new byte[0]; + return new ArrayList(0); } - var bitmap = await Task.Run(() => + var bitmap = await Task.Run>(() => { int pixelDataIndex = 0; double componentStep; - byte[] bgraPixelData; + ArrayList bgraPixelData; Color baseRgbColor = Colors.White; Color rgbColor; int bgraPixelDataHeight; @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives // Allocate the buffer // BGRA formatted color components 1 byte each (4 bytes in a pixel) - bgraPixelData = new byte[width * height * 4]; + bgraPixelData = new ArrayList(width * height * 4); bgraPixelDataHeight = height * 4; bgraPixelDataWidth = width * 4; @@ -604,7 +604,7 @@ namespace Avalonia.Controls.Primitives /// The pixel height of the bitmap. /// A new . public static WriteableBitmap CreateBitmapFromPixelData( - IList bgraPixelData, + ArrayList bgraPixelData, int pixelWidth, int pixelHeight) { @@ -619,7 +619,7 @@ namespace Avalonia.Controls.Primitives using (var frameBuffer = bitmap.Lock()) { - Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + Marshal.Copy(bgraPixelData.Array, 0, frameBuffer.Address, bgraPixelData.Array.Length); } return bitmap; @@ -633,11 +633,11 @@ namespace Avalonia.Controls.Primitives /// The bitmap (in raw BGRA pre-multiplied alpha pixels). public static void UpdateBitmapFromPixelData( WriteableBitmap bitmap, - IList bgraPixelData) + ArrayList bgraPixelData) { using (var frameBuffer = bitmap.Lock()) { - Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + Marshal.Copy(bgraPixelData.Array, 0, frameBuffer.Address, bgraPixelData.Array.Length); } return; From 98a58217ce22d55f9343216fd4fbc727618c7306 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:25:24 -0400 Subject: [PATCH 07/44] Fix ColorSlider update/refresh for all property changes --- .../ColorSlider/ColorSlider.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 8bcdef5558..6f20238b70 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -385,11 +385,11 @@ namespace Avalonia.Controls.Primitives return; } - // Always keep the two color properties in sync if (change.Property == ColorProperty) { ignorePropertyChanged = true; + // Always keep the two color properties in sync HsvColor = Color.ToHsv(); SetColorToSliderValues(); @@ -402,7 +402,10 @@ namespace Avalonia.Controls.Primitives ignorePropertyChanged = false; } - else if (change.Property == ColorModelProperty) + else if (change.Property == ColorComponentProperty || + change.Property == ColorModelProperty || + change.Property == IsAlphaMaxForcedProperty || + change.Property == IsSaturationValueMaxForcedProperty) { ignorePropertyChanged = true; @@ -416,6 +419,7 @@ namespace Avalonia.Controls.Primitives { ignorePropertyChanged = true; + // Always keep the two color properties in sync Color = HsvColor.ToRgb(); SetColorToSliderValues(); @@ -440,6 +444,7 @@ namespace Avalonia.Controls.Primitives _backgroundBitmap = null; UpdateBackground(); + UpdatePseudoClasses(); } else if (change.Property == ValueProperty || change.Property == MinimumProperty || From 4f1d315b4b11cf84ca7c3b4ffc65c41d35e24436 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:26:28 -0400 Subject: [PATCH 08/44] Make all ColorSpectrum bitmaps globally accessible by the control for future disposal --- .../ColorSpectrum/ColorSpectrum.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 3604894833..6b06e1eac1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -73,6 +73,8 @@ namespace Avalonia.Controls.Primitives private WriteableBitmap? _saturationMaximumBitmap; private WriteableBitmap? _valueBitmap; + private WriteableBitmap? _minBitmap; + private WriteableBitmap? _maxBitmap; // Fields used by UpdateEllipse() to ensure that it's using the data // associated with the last call to CreateBitmapsAndColorMap(), @@ -1084,28 +1086,28 @@ namespace Avalonia.Controls.Primitives ColorSpectrumComponents components2 = Components; - WriteableBitmap minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight); - WriteableBitmap maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight); + _minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight); + _maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight); switch (components2) { case ColorSpectrumComponents.HueValue: case ColorSpectrumComponents.ValueHue: - _saturationMinimumBitmap = minBitmap; - _saturationMaximumBitmap = maxBitmap; + _saturationMinimumBitmap = _minBitmap; + _saturationMaximumBitmap = _maxBitmap; break; case ColorSpectrumComponents.HueSaturation: case ColorSpectrumComponents.SaturationHue: - _valueBitmap = maxBitmap; + _valueBitmap = _maxBitmap; break; case ColorSpectrumComponents.ValueSaturation: case ColorSpectrumComponents.SaturationValue: - _hueRedBitmap = minBitmap; + _hueRedBitmap = _minBitmap; _hueYellowBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle1PixelData, pixelWidth, pixelHeight); _hueGreenBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle2PixelData, pixelWidth, pixelHeight); _hueCyanBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle3PixelData, pixelWidth, pixelHeight); _hueBlueBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle4PixelData, pixelWidth, pixelHeight); - _huePurpleBitmap = maxBitmap; + _huePurpleBitmap = _maxBitmap; break; } From 667b02d2c0d6e713980f0ad05c843d96e0c5a6f4 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:33:24 -0400 Subject: [PATCH 09/44] Disable WriteableBitmap re-use in ColorSlider due to crashes --- .../ColorSlider/ColorSlider.cs | 8 +++++++- .../Helpers/ColorPickerHelpers.cs | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 6f20238b70..8b624958dd 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -135,10 +135,16 @@ namespace Avalonia.Controls.Primitives { if (_backgroundBitmap != null) { + // CURRENTLY DISABLED DUE TO INTERMITTENT CRASHES IN SKIA/RENDERER + // // Re-use the existing WriteableBitmap // This assumes the height, width and byte counts are the same and must be set to null // elsewhere if that assumption is ever not true. - ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); + // ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); + + // ALSO DISABLED DISPOSE DUE TO INTERMITTENT CRASHES + //_backgroundBitmap?.Dispose(); + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); } else { diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index 2378813f52..01e5a0868f 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -627,6 +627,7 @@ namespace Avalonia.Controls.Primitives /// /// Updates the given with new, raw BGRA pre-multiplied alpha pixel data. + /// WARNING: THIS METHOD IS CURRENTLY PROVIDED AS REFERENCE BUT CAUSES INTERMITTENT CRASHES IF USED. /// WARNING: The bitmap's width, height and byte count MUST not have changed and MUST be enforced externally. /// /// The existing to update. From bc927d03129fa201248d2bcab78143f7a1c8f93f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 9 Oct 2022 21:07:58 -0400 Subject: [PATCH 10/44] Remove ColorSlider background disposal on attach/detach from visual tree --- .../ColorSlider/ColorSlider.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 8b624958dd..0b9f7f9146 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -45,20 +45,12 @@ namespace Avalonia.Controls.Primitives protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - - // Bitmaps were released when detached from the visual tree so they must be re-built - UpdateBackground(); } /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - - // Clean-up all bitmaps - // https://github.com/AvaloniaUI/Avalonia/issues/9051 - _backgroundBitmap?.Dispose(); - _backgroundBitmap = null; } /// From 5f9b193b311f030df6463c1188adf4e698239655 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 12 Oct 2022 22:04:13 +0100 Subject: [PATCH 11/44] fix build on net7 sdks with scoped keyword. --- src/Avalonia.Base/Utilities/IdentifierParser.cs | 2 +- .../Markup/Parsers/BindingExpressionGrammar.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index b105d0746b..ee176a6b85 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -8,7 +8,7 @@ namespace Avalonia.Utilities #endif static class IdentifierParser { - public static ReadOnlySpan ParseIdentifier(this ref CharacterReader r) + public static ReadOnlySpan ParseIdentifier(this scoped ref CharacterReader r) { if (IsValidIdentifierStart(r.Peek)) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 439bc15243..0a9fbcfacb 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -168,7 +168,7 @@ namespace Avalonia.Markup.Parsers } } - private static State ParseAttachedProperty(ref CharacterReader r, List nodes) + private static State ParseAttachedProperty(scoped ref CharacterReader r, List nodes) { var (ns, owner) = ParseTypeName(ref r); @@ -318,7 +318,7 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private static TypeName ParseTypeName(ref CharacterReader r) + private static TypeName ParseTypeName(scoped ref CharacterReader r) { ReadOnlySpan ns, typeName; ns = ReadOnlySpan.Empty; From d1e58bfde540935691c7515138982fed7ede3269 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 12 Oct 2022 22:08:18 +0100 Subject: [PATCH 12/44] use net7 rc2 sdk. --- azure-pipelines.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 33b2dc670a..903f9e3843 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -35,9 +35,9 @@ jobs: version: 6.0.401 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' + displayName: 'Use .NET Core SDK 7.0.100-rc.2.22477.23' inputs: - version: 7.0.100-rc.1.22431.12 + version: 7.0.100-rc.2.22477.23 - task: CmdLine@2 displayName: 'Install Workloads' @@ -72,9 +72,9 @@ jobs: version: 6.0.401 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' + displayName: 'Use .NET Core SDK 7.0.100-rc.2.22477.23' inputs: - version: 7.0.100-rc.1.22431.12 + version: 7.0.100-rc.2.22477.23 - task: CmdLine@2 displayName: 'Install Workloads' @@ -143,9 +143,9 @@ jobs: version: 6.0.401 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' + displayName: 'Use .NET Core SDK 7.0.100-rc.2.22477.23' inputs: - version: 7.0.100-rc.1.22431.12 + version: 7.0.100-rc.2.22477.23 - task: CmdLine@2 displayName: 'Install Workloads' From 7bf48cf7de6350dae540949573858b36ab94ffb2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 12 Oct 2022 17:15:51 +0200 Subject: [PATCH 13/44] feat: Enable CA1847 --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig b/.editorconfig index 337760636b..dfc28388fc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -141,6 +141,8 @@ dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomme dotnet_diagnostic.CA1802.severity = warning # CA1825: Avoid zero-length array allocations dotnet_diagnostic.CA1825.severity = warning +#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters +dotnet_diagnostic.CA1847.severity = warning # Wrapping preferences csharp_wrap_before_ternary_opsigns = false From 94afe971d9af03b8f5f7c36888b376c4476631dd Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 14 Oct 2022 15:13:12 +0200 Subject: [PATCH 14/44] feat: Enable Rule CA1820 --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig b/.editorconfig index 337760636b..09f0d3e6ac 100644 --- a/.editorconfig +++ b/.editorconfig @@ -141,6 +141,8 @@ dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomme dotnet_diagnostic.CA1802.severity = warning # CA1825: Avoid zero-length array allocations dotnet_diagnostic.CA1825.severity = warning +# CA1820: Test for empty strings using string length +dotnet_diagnostic.CA1820.severity = warning # Wrapping preferences csharp_wrap_before_ternary_opsigns = false From 36cfc43a596c2181278b1a86b00fb4761dca0fc8 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 14 Oct 2022 15:14:35 +0200 Subject: [PATCH 15/44] feat: Address CA1820 Rule --- src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs | 4 ++-- src/Avalonia.Controls/AppBuilderBase.cs | 4 ++-- src/Avalonia.Controls/DateTimePickers/TimePicker.cs | 2 +- src/Avalonia.Controls/DefinitionBase.cs | 2 +- .../Remote/HtmlTransport/SimpleWebSocketHttpServer.cs | 2 +- src/Avalonia.X11/X11Structs.cs | 2 +- .../Media/TextFormatting/GraphemeBreakTestDataGenerator.cs | 6 +++--- tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs | 2 +- tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs | 2 +- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs b/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs index 82670bf989..6144679b60 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs @@ -28,8 +28,8 @@ namespace Avalonia.Controls { if (targetType != null && targetType.IsNullableType()) { - String strValue = value as String; - if (strValue == String.Empty) + var strValue = value as string; + if (string.IsNullOrEmpty(strValue)) { return null; } diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 6b7101cd49..1dfad7dcc5 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -210,9 +210,9 @@ namespace Avalonia.Controls { var moduleInitializers = from assembly in AppDomain.CurrentDomain.GetAssemblies() from attribute in assembly.GetCustomAttributes() - where attribute.ForWindowingSubsystem == "" + where string.IsNullOrEmpty(attribute.ForWindowingSubsystem) || attribute.ForWindowingSubsystem == WindowingSubsystemName - where attribute.ForRenderingSubsystem == "" + where string.IsNullOrEmpty(attribute.ForRenderingSubsystem) || attribute.ForRenderingSubsystem == RenderingSubsystemName group attribute by attribute.Name into exports select (from export in exports diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index a709e4fb49..c3baa6f17f 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls get => _clockIdentifier; set { - if (!(string.IsNullOrEmpty(value) || value == "" || value == "12HourClock" || value == "24HourClock")) + if (!(string.IsNullOrEmpty(value) || value == "12HourClock" || value == "24HourClock")) throw new ArgumentException("Invalid ClockIdentifier"); SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value); SetGrid(); diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index eb09ff397a..64a02ccb46 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -366,7 +366,7 @@ namespace Avalonia.Controls string id = (string)value; - if (id != string.Empty) + if (!string.IsNullOrEmpty(id)) { int i = -1; while (++i < id.Length) diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs index 9a872df960..6bfae536c9 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs @@ -72,7 +72,7 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport while (true) { line = await ReadLineAsync(); - if (line == "") + if (string.IsNullOrEmpty(line)) break; sp = line.Split(new[] {':'}, 2); headers[sp[0]] = sp[1].TrimStart(); diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 23abd31b2c..3f0a6aeb67 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -661,7 +661,7 @@ namespace Avalonia.X11 { Type type = ev.GetType (); FieldInfo [] fields = type.GetFields (System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Instance); for (int i = 0; i < fields.Length; i++) { - if (result != string.Empty) { + if (!string.IsNullOrEmpty(result)) { result += ", "; } object value = fields [i].GetValue (ev); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakTestDataGenerator.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakTestDataGenerator.cs index f6616d74a2..9b2cb481e0 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakTestDataGenerator.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakTestDataGenerator.cs @@ -68,7 +68,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting var graphemeChars = elements[0].Replace(" × ", " ").Split(' '); - var codepoints = graphemeChars.Where(x => x != "" && x != "×") + var codepoints = graphemeChars.Where(x => !string.IsNullOrEmpty(x) && x != "×") .Select(x => Convert.ToInt32(x, 16)).ToList(); var grapheme = codepoints.ToArray(); @@ -77,10 +77,10 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting { var remainingChars = elements[1].Replace(" × ", " ").Split(' '); - var remaining = remainingChars.Where(x => x != "" && x != "×").Select(x => Convert.ToInt32(x, 16)).ToArray(); + var remaining = remainingChars.Where(x => !string.IsNullOrEmpty(x) && x != "×").Select(x => Convert.ToInt32(x, 16)).ToArray(); codepoints.AddRange(remaining); - } + } var data = new GraphemeBreakData { diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index c8bd289e54..122176fbe0 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -99,7 +99,7 @@ namespace Avalonia.Controls.UnitTests textbox.Text = String.Empty; Dispatcher.UIThread.RunJobs(); - Assert.True(control.SearchText == String.Empty); + Assert.True(string.IsNullOrEmpty(control.SearchText)); Assert.False(control.IsDropDownOpen); Assert.True(closeEvent); }); diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index d1fa522206..a2d0842028 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -559,7 +559,7 @@ namespace Avalonia.Controls.UnitTests Text = "0123456789" }; - Assert.True(target.SelectedText == ""); + Assert.True(string.IsNullOrEmpty(target.SelectedText)); target.SelectionStart = 2; target.SelectionEnd = 4; diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 23a330c96f..f2982046e5 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -473,7 +473,7 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); - Assert.True(target.SelectedText == ""); + Assert.True(string.IsNullOrEmpty(target.SelectedText)); target.SelectionStart = 2; target.SelectionEnd = 4; From 2f8273ae6484aad6712f07daf24f0caf4a36a821 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 12 Oct 2022 17:16:23 +0200 Subject: [PATCH 16/44] fix: Address CA1847 rule fix: CA1874 fix: CA1847 --- src/Avalonia.Base/AvaloniaProperty.cs | 8 +++++++- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 2 +- src/Avalonia.Base/Media/UnicodeRange.cs | 7 ++++++- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 2 +- .../NumericUpDown/NumericUpDown.cs | 4 ++-- src/Avalonia.Remote.Protocol/MetsysBson.cs | 14 +++++++------- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 8 +++++++- .../Converters/TimeSpanTypeConverter.cs | 10 ++++++++-- 8 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index fd43ced196..b3c3de919a 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -12,6 +12,12 @@ namespace Avalonia /// public abstract class AvaloniaProperty : IEquatable, IPropertyInfo { +#if NET6_0_OR_GREATER + internal const char Period = '.'; +#else + internal const string Period = "."; +#endif + /// /// Represents an unset property value. /// @@ -41,7 +47,7 @@ namespace Avalonia { _ = name ?? throw new ArgumentNullException(nameof(name)); - if (name.Contains(".")) + if (name.Contains(Period)) { throw new ArgumentException("'name' may not contain periods."); } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index d86e723b38..01c7160129 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -228,7 +228,7 @@ namespace Avalonia _ = type ?? throw new ArgumentNullException(nameof(type)); _ = name ?? throw new ArgumentNullException(nameof(name)); - if (name.Contains(".")) + if (name.Contains(AvaloniaProperty.Period)) { throw new InvalidOperationException("Attached properties not supported."); } diff --git a/src/Avalonia.Base/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs index e2338b9b26..33f7f656ba 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -104,6 +104,11 @@ namespace Avalonia.Media public readonly struct UnicodeRangeSegment { +#if NET6_0_OR_GREATER + private const char Question = '?'; +#else + private const string Question = "?"; +#endif private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$"); public UnicodeRangeSegment(int start, int end) @@ -163,7 +168,7 @@ namespace Avalonia.Media throw new FormatException("Could not parse specified Unicode range segment."); } - if (!single.Value.Contains("?")) + if (!single.Value.Contains(Question)) { start = int.Parse(single.Groups[1].Value, System.Globalization.NumberStyles.HexNumber); end = start; diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 00ebcab70e..65ec4cc54c 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -597,7 +597,7 @@ namespace Avalonia.Controls.Primitives for (int i = presenter.Classes.Count - 1; i >= 0; i--) { if (!classes.Contains(presenter.Classes[i]) && - !presenter.Classes[i].Contains(":")) + !presenter.Classes[i].Contains(':')) { presenter.Classes.RemoveAt(i); } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index b607ced10a..21373c6b76 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -1151,8 +1151,8 @@ namespace Avalonia.Controls 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("'"); + var isText = stringToTest.Substring(0, PIndex).Contains('\'') + && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains('\''); return !isText; } diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index 524ec09ca9..6abece6bf3 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -1364,13 +1364,13 @@ namespace Metsys.Bson var optionsString = ReadName(); var options = RegexOptions.None; - if (optionsString.Contains("e")) options = options | RegexOptions.ECMAScript; - if (optionsString.Contains("i")) options = options | RegexOptions.IgnoreCase; - if (optionsString.Contains("l")) options = options | RegexOptions.CultureInvariant; - if (optionsString.Contains("m")) options = options | RegexOptions.Multiline; - if (optionsString.Contains("s")) options = options | RegexOptions.Singleline; - if (optionsString.Contains("w")) options = options | RegexOptions.IgnorePatternWhitespace; - if (optionsString.Contains("x")) options = options | RegexOptions.ExplicitCapture; + if (optionsString.Contains('e')) options = options | RegexOptions.ECMAScript; + if (optionsString.Contains('i')) options = options | RegexOptions.IgnoreCase; + if (optionsString.Contains('l')) options = options | RegexOptions.CultureInvariant; + if (optionsString.Contains('m')) options = options | RegexOptions.Multiline; + if (optionsString.Contains('s')) options = options | RegexOptions.Singleline; + if (optionsString.Contains('w')) options = options | RegexOptions.IgnorePatternWhitespace; + if (optionsString.Contains('x')) options = options | RegexOptions.ExplicitCapture; return new Regex(pattern, options); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index d907bcbef9..48a7ba99e3 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -14,6 +14,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { class AvaloniaXamlIlLanguageParseIntrinsics { +#if NET6_0_OR_GREATER + private const string Colon = ":"; +#else + private const string Colon = ":"; +#endif + public static bool TryConvert(AstTransformationContext context, IXamlAstValueNode node, string text, IXamlType type, AvaloniaXamlIlWellKnownTypes types, out IXamlAstValueNode result) { if (type.FullName == "System.TimeSpan") @@ -23,7 +29,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions if (!TimeSpan.TryParse(tsText, CultureInfo.InvariantCulture, out var timeSpan)) { // // shorthand seconds format (ie. "0.25") - if (!tsText.Contains(":") && double.TryParse(tsText, + if (!tsText.Contains(Colon) && double.TryParse(tsText, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var seconds)) timeSpan = TimeSpan.FromSeconds(seconds); diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs index 1e42a46875..3fd653be55 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs @@ -7,6 +7,12 @@ namespace Avalonia.Markup.Xaml.Converters public class TimeSpanTypeConverter : System.ComponentModel.TimeSpanConverter { +#if NET6_0_OR_GREATER + private const char Colon = ':'; +#else + private const string Colon = ":"; +#endif + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return base.CanConvertFrom(context, sourceType); @@ -15,7 +21,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { var valueStr = (string)value; - if (!valueStr.Contains(":")) + if (!valueStr.Contains(Colon)) { // shorthand seconds format (ie. "0.25") var secs = double.Parse(valueStr, CultureInfo.InvariantCulture); @@ -25,4 +31,4 @@ namespace Avalonia.Markup.Xaml.Converters return base.ConvertFrom(context, culture, value); } } -} \ No newline at end of file +} From 11a73142c08af4a7c74a61c4493c7ab050a75771 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 15 Oct 2022 14:31:04 -0400 Subject: [PATCH 17/44] Add additional ColorPicker sample to control catalog created from code-behind --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 7 +++---- .../ControlCatalog/Pages/ColorPickerPage.xaml.cs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index ad54eb95fc..3cb761fb34 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -14,13 +14,15 @@ - @@ -56,8 +58,5 @@ IsAccentColorsVisible="False" HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" /> - - diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs index 6e017e381f..4671bbdb7c 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs @@ -1,6 +1,8 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Markup.Xaml; +using Avalonia.Media; namespace ControlCatalog.Pages { @@ -9,6 +11,20 @@ namespace ControlCatalog.Pages public ColorPickerPage() { InitializeComponent(); + + var layoutRoot = this.GetControl("LayoutRoot"); + + // ColorPicker added from code-behind + var colorPicker = new ColorPicker() + { + Color = Colors.Blue, + Margin = new Thickness(0, 50, 0, 0), + HorizontalAlignment = HorizontalAlignment.Center, + }; + Grid.SetColumn(colorPicker, 2); + Grid.SetRow(colorPicker, 1); + + layoutRoot.Children.Add(colorPicker); } private void InitializeComponent() From 2575a10fb09677ac3f131ab3715b20c46b588397 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 15 Oct 2022 14:33:47 -0400 Subject: [PATCH 18/44] Add and implement IsHostedInColorPicker property to ColorView This works-around some lifecycle issues by allowing the ColorView to specially handle how it handles property changes based on whether or not it is within a ColorPicker. --- .../ColorPicker/ColorPicker.cs | 3 --- .../ColorView/ColorView.Properties.cs | 24 +++++++++++++++++++ .../ColorView/ColorView.cs | 20 ++++++++++++++++ .../Themes/Fluent/ColorPicker.xaml | 4 ++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index 39369bcbdb..92d8535272 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -11,9 +11,6 @@ /// public ColorPicker() : base() { - // Completely ignore property changes here - // The ColorView in the control template is responsible to manage this - base.ignorePropertyChanged = true; } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index b76059037b..d1ee375e28 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -148,6 +148,14 @@ namespace Avalonia.Controls nameof(IsHexInputVisible), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsHostedInColorPickerProperty = + AvaloniaProperty.Register( + nameof(IsHostedInColorPicker), + false); + /// /// Defines the property. /// @@ -395,6 +403,22 @@ namespace Avalonia.Controls set => SetValue(IsHexInputVisibleProperty, value); } + /// + /// Gets or sets a value indicating whether this is hosted + /// inside a 's control template. + /// + /// + /// This is a special property to change how the internally + /// processes property updates. When a is hosted within a + /// , it does not need to handle most property changes. + /// Instead, the primary calculations are done within the itself. + /// + public bool IsHostedInColorPicker + { + get => GetValue(IsHostedInColorPickerProperty); + set => SetValue(IsHostedInColorPickerProperty, value); + } + /// public int MaxHue { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 89f1afb1ac..b0d5250c50 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -206,6 +206,26 @@ namespace Avalonia.Controls return; } + // Special handling for when hosted within a ColorPicker and the ColorPicker is + // responsible for most of the internal processing and property updates. + if (IsHostedInColorPicker) + { + if (change.Property == ColorProperty || + change.Property == HsvColorProperty) + { + ignorePropertyChanged = true; + + // The Hex text isn't currently bound with the ColorPicker and therefore + // must still be updated here. + SetColorToHexTextBox(); + + ignorePropertyChanged = false; + } + + base.OnPropertyChanged(change); + return; + } + // Always keep the two color properties in sync if (change.Property == ColorProperty) { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 74a1df4991..acdd06480f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -43,7 +43,11 @@ + Date: Sat, 15 Oct 2022 14:58:22 -0400 Subject: [PATCH 19/44] Make some todo-areas of the code more clear --- src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs | 4 ++-- .../Helpers/ColorPickerHelpers.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 0b9f7f9146..ec08e96d87 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -127,14 +127,14 @@ namespace Avalonia.Controls.Primitives { if (_backgroundBitmap != null) { - // CURRENTLY DISABLED DUE TO INTERMITTENT CRASHES IN SKIA/RENDERER + // TODO: CURRENTLY DISABLED DUE TO INTERMITTENT CRASHES IN SKIA/RENDERER // // Re-use the existing WriteableBitmap // This assumes the height, width and byte counts are the same and must be set to null // elsewhere if that assumption is ever not true. // ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); - // ALSO DISABLED DISPOSE DUE TO INTERMITTENT CRASHES + // TODO: ALSO DISABLED DISPOSE DUE TO INTERMITTENT CRASHES //_backgroundBitmap?.Dispose(); _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); } diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index 01e5a0868f..819d745772 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -627,7 +627,7 @@ namespace Avalonia.Controls.Primitives /// /// Updates the given with new, raw BGRA pre-multiplied alpha pixel data. - /// WARNING: THIS METHOD IS CURRENTLY PROVIDED AS REFERENCE BUT CAUSES INTERMITTENT CRASHES IF USED. + /// TODO: THIS METHOD IS CURRENTLY PROVIDED AS REFERENCE BUT CAUSES INTERMITTENT CRASHES IF USED. /// WARNING: The bitmap's width, height and byte count MUST not have changed and MUST be enforced externally. /// /// The existing to update. From e96772e7076713f18e6104e73f8d2658016743d2 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 15 Oct 2022 15:29:54 -0400 Subject: [PATCH 20/44] Remove ThirdComponentConverter replacing it with ColorSpectrum.ThirdComponent property --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 1 - .../ColorSpectrum/ColorSpectrum.Properties.cs | 24 +++++++++ .../ColorSpectrum/ColorSpectrum.cs | 17 ++++++ .../Converters/ThirdComponentConverter.cs | 53 ------------------- .../Themes/Fluent/ColorView.xaml | 3 +- .../Themes/Simple/ColorView.xaml | 3 +- 6 files changed, 43 insertions(+), 58 deletions(-) delete mode 100644 src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index 3cb761fb34..69ceaea328 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -11,7 +11,6 @@ x:Class="ControlCatalog.Pages.ColorPickerPage"> - + /// Defines the property. + /// + public static readonly StyledProperty ThirdComponentProperty = + AvaloniaProperty.Register( + nameof(ThirdComponent), + ColorComponent.Component3); // Value + /// /// Gets or sets the currently selected color in the RGB color model. /// @@ -218,5 +226,21 @@ namespace Avalonia.Controls.Primitives get => GetValue(ShapeProperty); set => SetValue(ShapeProperty, value); } + + /// + /// Gets the third HSV color component that is NOT displayed by the spectrum. + /// This is automatically calculated from the property. + /// + /// + /// This property should be used for any external color slider that represents the + /// third component of the color. Note that this property uses the generic + /// type instead of the more accurate + /// to allow direct usage by the generalized color sliders. + /// + public ColorComponent ThirdComponent + { + get => GetValue(ThirdComponentProperty); + private set => SetValue(ThirdComponentProperty, value); + } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 6b06e1eac1..f0ed89fb3a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -503,6 +503,23 @@ namespace Avalonia.Controls.Primitives } else if (change.Property == ComponentsProperty) { + // Calculate and update the ThirdComponent value + switch (Components) + { + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.SaturationHue: + ThirdComponent = (ColorComponent)HsvComponent.Value; + break; + case ColorSpectrumComponents.HueValue: + case ColorSpectrumComponents.ValueHue: + ThirdComponent = (ColorComponent)HsvComponent.Saturation; + break; + case ColorSpectrumComponents.SaturationValue: + case ColorSpectrumComponents.ValueSaturation: + ThirdComponent = (ColorComponent)HsvComponent.Hue; + break; + } + CreateBitmapsAndColorMap(); } diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs deleted file mode 100644 index 11e33c74f0..0000000000 --- a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Controls.Primitives.Converters -{ - /// - /// Gets the third corresponding with a given - /// that represents the other two components. - /// - /// - /// This is a highly-specialized converter for the color picker. - /// - public class ThirdComponentConverter : IValueConverter - { - /// - public object? Convert( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - if (value is ColorSpectrumComponents components) - { - // Note: Alpha is not relevant here - switch (components) - { - case ColorSpectrumComponents.HueSaturation: - case ColorSpectrumComponents.SaturationHue: - return (ColorComponent)HsvComponent.Value; - case ColorSpectrumComponents.HueValue: - case ColorSpectrumComponents.ValueHue: - return (ColorComponent)HsvComponent.Saturation; - case ColorSpectrumComponents.SaturationValue: - case ColorSpectrumComponents.ValueSaturation: - return (ColorComponent)HsvComponent.Hue; - } - } - - return AvaloniaProperty.UnsetValue; - } - - /// - public object? ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture) - { - return AvaloniaProperty.UnsetValue; - } - } -} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index cf836aff4a..b3c6d1a430 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -8,7 +8,6 @@ x:CompileBindings="True"> - @@ -368,7 +367,7 @@ IsSaturationValueMaxForced="False" Orientation="Vertical" ColorModel="Hsva" - ColorComponent="{Binding Components, ElementName=ColorSpectrum, Converter={StaticResource ThirdComponentConverter}}" + ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}" HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml index 380e655949..860dbe11d0 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml @@ -8,7 +8,6 @@ x:CompileBindings="True"> - @@ -330,7 +329,7 @@ IsSaturationValueMaxForced="False" Orientation="Vertical" ColorModel="Hsva" - ColorComponent="{Binding Components, ElementName=ColorSpectrum, Converter={StaticResource ThirdComponentConverter}}" + ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}" HsvColor="{Binding HsvColor, ElementName=ColorSpectrum}" HorizontalAlignment="Center" VerticalAlignment="Stretch" From 7b69d2ae5744f97257a56d1d09dac98b6edd5e31 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 16 Oct 2022 12:17:47 -0400 Subject: [PATCH 21/44] Revert "Add and implement IsHostedInColorPicker property to ColorView" This reverts commit 2575a10fb09677ac3f131ab3715b20c46b588397. --- .../ColorPicker/ColorPicker.cs | 3 +++ .../ColorView/ColorView.Properties.cs | 24 ------------------- .../ColorView/ColorView.cs | 20 ---------------- .../Themes/Fluent/ColorPicker.xaml | 4 ---- 4 files changed, 3 insertions(+), 48 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index 92d8535272..39369bcbdb 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -11,6 +11,9 @@ /// public ColorPicker() : base() { + // Completely ignore property changes here + // The ColorView in the control template is responsible to manage this + base.ignorePropertyChanged = true; } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index d1ee375e28..b76059037b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -148,14 +148,6 @@ namespace Avalonia.Controls nameof(IsHexInputVisible), true); - /// - /// Defines the property. - /// - public static readonly StyledProperty IsHostedInColorPickerProperty = - AvaloniaProperty.Register( - nameof(IsHostedInColorPicker), - false); - /// /// Defines the property. /// @@ -403,22 +395,6 @@ namespace Avalonia.Controls set => SetValue(IsHexInputVisibleProperty, value); } - /// - /// Gets or sets a value indicating whether this is hosted - /// inside a 's control template. - /// - /// - /// This is a special property to change how the internally - /// processes property updates. When a is hosted within a - /// , it does not need to handle most property changes. - /// Instead, the primary calculations are done within the itself. - /// - public bool IsHostedInColorPicker - { - get => GetValue(IsHostedInColorPickerProperty); - set => SetValue(IsHostedInColorPickerProperty, value); - } - /// public int MaxHue { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index b0d5250c50..89f1afb1ac 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -206,26 +206,6 @@ namespace Avalonia.Controls return; } - // Special handling for when hosted within a ColorPicker and the ColorPicker is - // responsible for most of the internal processing and property updates. - if (IsHostedInColorPicker) - { - if (change.Property == ColorProperty || - change.Property == HsvColorProperty) - { - ignorePropertyChanged = true; - - // The Hex text isn't currently bound with the ColorPicker and therefore - // must still be updated here. - SetColorToHexTextBox(); - - ignorePropertyChanged = false; - } - - base.OnPropertyChanged(change); - return; - } - // Always keep the two color properties in sync if (change.Property == ColorProperty) { diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index acdd06480f..74a1df4991 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -43,11 +43,7 @@ - Date: Sun, 16 Oct 2022 12:35:25 -0400 Subject: [PATCH 22/44] Hand-off control to the ColorView after template is applied in ColorPicker This solution doesn't require the hacky IsHostedInColorPicker property --- .../ColorPicker/ColorPicker.cs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index 39369bcbdb..29f9f3c571 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Controls +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls { /// /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. @@ -11,8 +13,33 @@ /// public ColorPicker() : base() { - // Completely ignore property changes here - // The ColorView in the control template is responsible to manage this + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + // Until this point the ColorPicker itself is responsible to process property updates. + // This, for example, syncs Color with HsvColor and updates primitive controls. + // + // However, when the template is created, hand-off this change processing to the + // ColorView within the control template itself. Remember ColorPicker derives from + // ColorView so we don't want two instances of the same logic fighting each other. + // It is best to hand-off to the ColorView in the control template because that is the + // primary point of user-interaction for the overall control. It also simplifies binding. + // + // Keep in mind this hand-off is not possible until the template controls are created + // which is done after the ColorPicker is instantiated. The ColorPicker must still + // process updates before the template is applied to ensure all property changes in + // XAML or object initializers are handled correctly. Otherwise, there can be bugs + // such as setting the Color property doesn't work because the HsvColor is never updated + // and then the Color value is lost once the template loads (and the template ColorView + // takes over). + // + // In order to complete this hand-off, completely ignore property changes here in the + // ColorPicker. This means the ColorView in the control template is now responsible to + // process property changes and handle primary calculations. base.ignorePropertyChanged = true; } } From 221bec69d4e453fdceeac34895ca583ab4aa0831 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 17 Oct 2022 12:39:06 +0200 Subject: [PATCH 23/44] Revert "fix: Address CA1847 rule" This reverts commit 2f8273ae6484aad6712f07daf24f0caf4a36a821. --- src/Avalonia.Base/AvaloniaProperty.cs | 8 +------- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 2 +- src/Avalonia.Base/Media/UnicodeRange.cs | 7 +------ src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 2 +- .../NumericUpDown/NumericUpDown.cs | 4 ++-- src/Avalonia.Remote.Protocol/MetsysBson.cs | 14 +++++++------- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 8 +------- .../Converters/TimeSpanTypeConverter.cs | 10 ++-------- 8 files changed, 16 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index b3c3de919a..fd43ced196 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -12,12 +12,6 @@ namespace Avalonia /// public abstract class AvaloniaProperty : IEquatable, IPropertyInfo { -#if NET6_0_OR_GREATER - internal const char Period = '.'; -#else - internal const string Period = "."; -#endif - /// /// Represents an unset property value. /// @@ -47,7 +41,7 @@ namespace Avalonia { _ = name ?? throw new ArgumentNullException(nameof(name)); - if (name.Contains(Period)) + if (name.Contains(".")) { throw new ArgumentException("'name' may not contain periods."); } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 01c7160129..d86e723b38 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -228,7 +228,7 @@ namespace Avalonia _ = type ?? throw new ArgumentNullException(nameof(type)); _ = name ?? throw new ArgumentNullException(nameof(name)); - if (name.Contains(AvaloniaProperty.Period)) + if (name.Contains(".")) { throw new InvalidOperationException("Attached properties not supported."); } diff --git a/src/Avalonia.Base/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs index 33f7f656ba..e2338b9b26 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -104,11 +104,6 @@ namespace Avalonia.Media public readonly struct UnicodeRangeSegment { -#if NET6_0_OR_GREATER - private const char Question = '?'; -#else - private const string Question = "?"; -#endif private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$"); public UnicodeRangeSegment(int start, int end) @@ -168,7 +163,7 @@ namespace Avalonia.Media throw new FormatException("Could not parse specified Unicode range segment."); } - if (!single.Value.Contains(Question)) + if (!single.Value.Contains("?")) { start = int.Parse(single.Groups[1].Value, System.Globalization.NumberStyles.HexNumber); end = start; diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 65ec4cc54c..00ebcab70e 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -597,7 +597,7 @@ namespace Avalonia.Controls.Primitives for (int i = presenter.Classes.Count - 1; i >= 0; i--) { if (!classes.Contains(presenter.Classes[i]) && - !presenter.Classes[i].Contains(':')) + !presenter.Classes[i].Contains(":")) { presenter.Classes.RemoveAt(i); } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 21373c6b76..b607ced10a 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -1151,8 +1151,8 @@ namespace Avalonia.Controls 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('\''); + var isText = stringToTest.Substring(0, PIndex).Contains("'") + && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'"); return !isText; } diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index 6abece6bf3..524ec09ca9 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -1364,13 +1364,13 @@ namespace Metsys.Bson var optionsString = ReadName(); var options = RegexOptions.None; - if (optionsString.Contains('e')) options = options | RegexOptions.ECMAScript; - if (optionsString.Contains('i')) options = options | RegexOptions.IgnoreCase; - if (optionsString.Contains('l')) options = options | RegexOptions.CultureInvariant; - if (optionsString.Contains('m')) options = options | RegexOptions.Multiline; - if (optionsString.Contains('s')) options = options | RegexOptions.Singleline; - if (optionsString.Contains('w')) options = options | RegexOptions.IgnorePatternWhitespace; - if (optionsString.Contains('x')) options = options | RegexOptions.ExplicitCapture; + if (optionsString.Contains("e")) options = options | RegexOptions.ECMAScript; + if (optionsString.Contains("i")) options = options | RegexOptions.IgnoreCase; + if (optionsString.Contains("l")) options = options | RegexOptions.CultureInvariant; + if (optionsString.Contains("m")) options = options | RegexOptions.Multiline; + if (optionsString.Contains("s")) options = options | RegexOptions.Singleline; + if (optionsString.Contains("w")) options = options | RegexOptions.IgnorePatternWhitespace; + if (optionsString.Contains("x")) options = options | RegexOptions.ExplicitCapture; return new Regex(pattern, options); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 48a7ba99e3..d907bcbef9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -14,12 +14,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { class AvaloniaXamlIlLanguageParseIntrinsics { -#if NET6_0_OR_GREATER - private const string Colon = ":"; -#else - private const string Colon = ":"; -#endif - public static bool TryConvert(AstTransformationContext context, IXamlAstValueNode node, string text, IXamlType type, AvaloniaXamlIlWellKnownTypes types, out IXamlAstValueNode result) { if (type.FullName == "System.TimeSpan") @@ -29,7 +23,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions if (!TimeSpan.TryParse(tsText, CultureInfo.InvariantCulture, out var timeSpan)) { // // shorthand seconds format (ie. "0.25") - if (!tsText.Contains(Colon) && double.TryParse(tsText, + if (!tsText.Contains(":") && double.TryParse(tsText, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var seconds)) timeSpan = TimeSpan.FromSeconds(seconds); diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs index 3fd653be55..1e42a46875 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs @@ -7,12 +7,6 @@ namespace Avalonia.Markup.Xaml.Converters public class TimeSpanTypeConverter : System.ComponentModel.TimeSpanConverter { -#if NET6_0_OR_GREATER - private const char Colon = ':'; -#else - private const string Colon = ":"; -#endif - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return base.CanConvertFrom(context, sourceType); @@ -21,7 +15,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { var valueStr = (string)value; - if (!valueStr.Contains(Colon)) + if (!valueStr.Contains(":")) { // shorthand seconds format (ie. "0.25") var secs = double.Parse(valueStr, CultureInfo.InvariantCulture); @@ -31,4 +25,4 @@ namespace Avalonia.Markup.Xaml.Converters return base.ConvertFrom(context, culture, value); } } -} +} \ No newline at end of file From 79754b9feb567e4f379c4dcff9e7b2f8e75e3da3 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 17 Oct 2022 14:20:35 +0200 Subject: [PATCH 24/44] fix: Address CA1847 rule --- src/Avalonia.Base/AvaloniaProperty.cs | 2 +- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 2 +- .../Compatibility/StringCompatibilityExtensions.cs | 12 ++++++++++++ src/Avalonia.Base/Media/UnicodeRange.cs | 2 +- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 2 +- .../NumericUpDown/NumericUpDown.cs | 4 ++-- src/Avalonia.Remote.Protocol/MetsysBson.cs | 14 +++++++------- .../Converters/TimeSpanTypeConverter.cs | 4 ++-- 8 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Base/Compatibility/StringCompatibilityExtensions.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index fd43ced196..46ba4082fb 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -41,7 +41,7 @@ namespace Avalonia { _ = name ?? throw new ArgumentNullException(nameof(name)); - if (name.Contains(".")) + if (name.Contains('.')) { throw new ArgumentException("'name' may not contain periods."); } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index d86e723b38..62265d3c59 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -228,7 +228,7 @@ namespace Avalonia _ = type ?? throw new ArgumentNullException(nameof(type)); _ = name ?? throw new ArgumentNullException(nameof(name)); - if (name.Contains(".")) + if (name.Contains('.')) { throw new InvalidOperationException("Attached properties not supported."); } diff --git a/src/Avalonia.Base/Compatibility/StringCompatibilityExtensions.cs b/src/Avalonia.Base/Compatibility/StringCompatibilityExtensions.cs new file mode 100644 index 0000000000..45e41b44d6 --- /dev/null +++ b/src/Avalonia.Base/Compatibility/StringCompatibilityExtensions.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; + +namespace System; + +#if !NET6_0_OR_GREATER +public static class StringCompatibilityExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Contains(this string str, char search) => + str.Contains(search.ToString()); +} +#endif diff --git a/src/Avalonia.Base/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs index e2338b9b26..344b85bae9 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -163,7 +163,7 @@ namespace Avalonia.Media throw new FormatException("Could not parse specified Unicode range segment."); } - if (!single.Value.Contains("?")) + if (!single.Value.Contains('?')) { start = int.Parse(single.Groups[1].Value, System.Globalization.NumberStyles.HexNumber); end = start; diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 00ebcab70e..65ec4cc54c 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -597,7 +597,7 @@ namespace Avalonia.Controls.Primitives for (int i = presenter.Classes.Count - 1; i >= 0; i--) { if (!classes.Contains(presenter.Classes[i]) && - !presenter.Classes[i].Contains(":")) + !presenter.Classes[i].Contains(':')) { presenter.Classes.RemoveAt(i); } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index b607ced10a..21373c6b76 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -1151,8 +1151,8 @@ namespace Avalonia.Controls 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("'"); + var isText = stringToTest.Substring(0, PIndex).Contains('\'') + && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains('\''); return !isText; } diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index 524ec09ca9..6abece6bf3 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -1364,13 +1364,13 @@ namespace Metsys.Bson var optionsString = ReadName(); var options = RegexOptions.None; - if (optionsString.Contains("e")) options = options | RegexOptions.ECMAScript; - if (optionsString.Contains("i")) options = options | RegexOptions.IgnoreCase; - if (optionsString.Contains("l")) options = options | RegexOptions.CultureInvariant; - if (optionsString.Contains("m")) options = options | RegexOptions.Multiline; - if (optionsString.Contains("s")) options = options | RegexOptions.Singleline; - if (optionsString.Contains("w")) options = options | RegexOptions.IgnorePatternWhitespace; - if (optionsString.Contains("x")) options = options | RegexOptions.ExplicitCapture; + if (optionsString.Contains('e')) options = options | RegexOptions.ECMAScript; + if (optionsString.Contains('i')) options = options | RegexOptions.IgnoreCase; + if (optionsString.Contains('l')) options = options | RegexOptions.CultureInvariant; + if (optionsString.Contains('m')) options = options | RegexOptions.Multiline; + if (optionsString.Contains('s')) options = options | RegexOptions.Singleline; + if (optionsString.Contains('w')) options = options | RegexOptions.IgnorePatternWhitespace; + if (optionsString.Contains('x')) options = options | RegexOptions.ExplicitCapture; return new Regex(pattern, options); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs index 1e42a46875..1b6fbcef5c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/TimeSpanTypeConverter.cs @@ -15,7 +15,7 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { var valueStr = (string)value; - if (!valueStr.Contains(":")) + if (!valueStr.Contains(':')) { // shorthand seconds format (ie. "0.25") var secs = double.Parse(valueStr, CultureInfo.InvariantCulture); @@ -25,4 +25,4 @@ namespace Avalonia.Markup.Xaml.Converters return base.ConvertFrom(context, culture, value); } } -} \ No newline at end of file +} From 6cc1215989ad440bc989a730956e6e51ee7d674e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 17 Oct 2022 15:13:40 +0200 Subject: [PATCH 25/44] Initialize DevTools MainWindow theme in code. Previously if no theme was included in App.xaml, then DevTools would fail to open because the `StaticResource` lookup would fail due to the window's XAML not being parsed yet. --- src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml | 3 +-- .../Diagnostics/Views/MainWindow.xaml.cs | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 1270dbaa62..d2b31fdb20 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -3,8 +3,7 @@ xmlns:views="clr-namespace:Avalonia.Diagnostics.Views" xmlns:diag="clr-namespace:Avalonia.Diagnostics" Title="Avalonia DevTools" - x:Class="Avalonia.Diagnostics.Views.MainWindow" - Theme="{StaticResource {x:Type Window}}"> + x:Class="Avalonia.Diagnostics.Views.MainWindow"> diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index c81997f2cb..e6e630112b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -28,6 +28,11 @@ namespace Avalonia.Diagnostics.Views { InitializeComponent(); + // Apply the SimpleTheme.Window theme; this must be done after the XAML is parsed as + // the theme is included in the MainWindow's XAML. + if (Theme is null && this.FindResource(typeof(Window)) is ControlTheme windowTheme) + Theme = windowTheme; + _keySubscription = InputManager.Instance?.Process .OfType() .Where(x => x.Type == RawKeyEventType.KeyDown) From 2435d2e87b585832f76fb7ad7f5b1e397dd49789 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 17 Oct 2022 15:34:22 +0200 Subject: [PATCH 26/44] Fix DataGrid cell contents lookup in DevTools. --- .../MarkupExtensions/StaticResourceExtension.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index d462a2210e..f28f7bc626 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -56,7 +56,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // We need to implement compile-time merging of resource dictionaries and this // hack can be removed. if (previousWasControlTheme && - parent is ResourceDictionary hack && + parent is IResourceProvider hack && hack.Owner?.GetType().FullName == "Avalonia.Diagnostics.Views.MainWindow" && hack.Owner.TryGetResource(ResourceKey, out value)) { From ee0c1b47781fd0ea48ec174c00552735452e1844 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 17 Oct 2022 14:19:24 +0000 Subject: [PATCH 27/44] Use span directly when available --- src/Avalonia.Base/Media/Color.cs | 5 +- src/Avalonia.Base/Utilities/SpanHelpers.cs | 31 ++++++++++ src/Avalonia.Build.Tasks/SpanCompat.cs | 61 +++++++++++++++++++ .../Markup/Parsers/SelectorGrammar.cs | 18 +++--- 4 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/SpanHelpers.cs diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index cb90404f6d..aee048cc99 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -9,6 +9,7 @@ using System; using System.Globalization; #if !BUILDTASK using Avalonia.Animation.Animators; +using static Avalonia.Utilities.SpanHelpers; #endif namespace Avalonia.Media @@ -295,9 +296,7 @@ namespace Avalonia.Media return false; } - // TODO: (netstandard 2.1) Can use allocation free parsing. - if (!uint.TryParse(input.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, - out var parsed)) + if (!input.TryParseFromHexToUInt(out var parsed)) { return false; } diff --git a/src/Avalonia.Base/Utilities/SpanHelpers.cs b/src/Avalonia.Base/Utilities/SpanHelpers.cs new file mode 100644 index 0000000000..e4fab5ac7f --- /dev/null +++ b/src/Avalonia.Base/Utilities/SpanHelpers.cs @@ -0,0 +1,31 @@ +using System; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities +{ + public static class SpanHelpers + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParseFromHexToUInt(this ReadOnlySpan span, out uint value) + { +#if NETSTANDARD2_0 + return uint.TryParse(span.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out value); +#else + return uint.TryParse(span, NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out value); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParseToInt(this ReadOnlySpan span, out int value) + { +#if NETSTANDARD2_0 + return int.TryParse(span.ToString(), out value); +#else + return int.TryParse(span, out value); +#endif + } + } +} diff --git a/src/Avalonia.Build.Tasks/SpanCompat.cs b/src/Avalonia.Build.Tasks/SpanCompat.cs index 25f8d0175a..6da1fbd07c 100644 --- a/src/Avalonia.Build.Tasks/SpanCompat.cs +++ b/src/Avalonia.Build.Tasks/SpanCompat.cs @@ -1,4 +1,7 @@ #if !NETCOREAPP3_1_OR_GREATER +using System.Globalization; +using System.Runtime.CompilerServices; + namespace System { // This is a hack to enable our span code to work inside MSBuild task without referencing System.Memory @@ -9,6 +12,8 @@ namespace System private int _length; public int Length => _length; + public static implicit operator ReadOnlySpan(string s) => new ReadOnlySpan(s); + public ReadOnlySpan(string s) : this(s, 0, s.Length) { @@ -63,8 +68,64 @@ namespace System return Slice(start); } + public ReadOnlySpan TrimEnd() + { + int end = Length - 1; + for (; end >= 0; end--) + { + if (!char.IsWhiteSpace(this[end])) + { + break; + } + } + return Slice(0, end + 1); + } + + public ReadOnlySpan Trim() + { + return TrimStart().TrimEnd(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryParseFromHexToUInt(out uint value) + { + return uint.TryParse(ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryParseToInt(out int value) + { + return int.TryParse(ToString(), out value); + } + public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length); + internal int IndexOf(string v, StringComparison ordinal, int start = 0) + { + if(Length == 0 || string.IsNullOrEmpty(v)) + { + return -1; + } + + for (var c = start; c < _length; c++) + { + if (this[c] == v[0]) + { + for(var i = 0; i < v.Length; i++) + { + if (this[c + i] != v[i]) + { + break; + } + } + return c; + } + } + + return -1; + } + public static implicit operator ReadOnlySpan(char[] arr) => new ReadOnlySpan(new string(arr)); } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 16856e674d..a5e11cd233 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -376,28 +376,28 @@ namespace Avalonia.Markup.Parsers if (r.Peek == 'o') { - var constArg = r.TakeUntil(')').ToString().Trim(); - if (constArg.Equals("odd", StringComparison.Ordinal)) + var constArg = r.TakeUntil(')').Trim(); + if (constArg.SequenceEqual("odd".AsSpan())) { step = 2; offset = 1; } else { - throw new ExpressionParseException(r.Position, $"Expected nth-child(odd). Actual '{constArg}'."); + throw new ExpressionParseException(r.Position, $"Expected nth-child(odd). Actual '{constArg.ToString()}'."); } } else if (r.Peek == 'e') { - var constArg = r.TakeUntil(')').ToString().Trim(); - if (constArg.Equals("even", StringComparison.Ordinal)) + var constArg = r.TakeUntil(')').Trim(); + if (constArg.SequenceEqual("even".AsSpan())) { step = 2; offset = 0; } else { - throw new ExpressionParseException(r.Position, $"Expected nth-child(even). Actual '{constArg}'."); + throw new ExpressionParseException(r.Position, $"Expected nth-child(even). Actual '{constArg.ToString()}'."); } } else @@ -405,7 +405,7 @@ namespace Avalonia.Markup.Parsers r.SkipWhitespace(); var stepOrOffset = 0; - var stepOrOffsetStr = r.TakeWhile(c => char.IsDigit(c) || c == '-' || c == '+').ToString(); + var stepOrOffsetStr = r.TakeWhile(c => char.IsDigit(c) || c == '-' || c == '+'); if (stepOrOffsetStr.Length == 0 || (stepOrOffsetStr.Length == 1 && stepOrOffsetStr[0] == '+')) @@ -417,7 +417,7 @@ namespace Avalonia.Markup.Parsers { stepOrOffset = -1; } - else if (!int.TryParse(stepOrOffsetStr.ToString(), out stepOrOffset)) + else if (!stepOrOffsetStr.TryParseToInt(out stepOrOffset)) { throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step or offset value. Integer was expected."); } @@ -462,7 +462,7 @@ namespace Avalonia.Markup.Parsers r.SkipWhitespace(); if (sign != 0 - && !int.TryParse(r.TakeUntil(')').ToString(), out offset)) + && !r.TakeUntil(')').TryParseToInt(out offset)) { throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected."); } From 868e5a5488e3e0d4a6954990fd61df5de08cbc2d Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 17 Oct 2022 14:45:37 +0000 Subject: [PATCH 28/44] switch to using helper in the color classes --- src/Avalonia.Base/Media/Color.cs | 44 ++++++++-------------- src/Avalonia.Base/Media/HslColor.cs | 30 ++++++--------- src/Avalonia.Base/Media/HsvColor.cs | 30 ++++++--------- src/Avalonia.Base/Utilities/SpanHelpers.cs | 24 ++++++++++++ src/Avalonia.Build.Tasks/SpanCompat.cs | 18 ++++++++- 5 files changed, 80 insertions(+), 66 deletions(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index aee048cc99..14ef357393 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -381,9 +381,9 @@ namespace Avalonia.Media if (components.Length == 3) // RGB { - if (InternalTryParseByte(components[0], out byte red) && - InternalTryParseByte(components[1], out byte green) && - InternalTryParseByte(components[2], out byte blue)) + if (InternalTryParseByte(components[0].AsSpan(), out byte red) && + InternalTryParseByte(components[1].AsSpan(), out byte green) && + InternalTryParseByte(components[2].AsSpan(), out byte blue)) { color = new Color(0xFF, red, green, blue); return true; @@ -391,10 +391,10 @@ namespace Avalonia.Media } else if (components.Length == 4) // RGBA { - if (InternalTryParseByte(components[0], out byte red) && - InternalTryParseByte(components[1], out byte green) && - InternalTryParseByte(components[2], out byte blue) && - InternalTryParseDouble(components[3], out double alpha)) + if (InternalTryParseByte(components[0].AsSpan(), out byte red) && + InternalTryParseByte(components[1].AsSpan(), out byte green) && + InternalTryParseByte(components[2].AsSpan(), out byte blue) && + InternalTryParseDouble(components[3].AsSpan(), out double alpha)) { color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue); return true; @@ -402,17 +402,14 @@ namespace Avalonia.Media } // Local function to specially parse a byte value with an optional percentage sign - bool InternalTryParseByte(string inString, out byte outByte) + bool InternalTryParseByte(ReadOnlySpan inString, out byte outByte) { // The percent sign, if it exists, must be at the end of the number - int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + int percentIndex = inString.IndexOf("%".AsSpan(), StringComparison.Ordinal); if (percentIndex >= 0) { - var result = double.TryParse( - inString.Substring(0, percentIndex), - NumberStyles.Number, - CultureInfo.InvariantCulture, + var result = inString.Slice(0, percentIndex).TryParseNumberToDouble( out double percentage); outByte = (byte)Math.Round((percentage / 100.0) * 255.0); @@ -420,37 +417,28 @@ namespace Avalonia.Media } else { - return byte.TryParse( - inString, - NumberStyles.Number, - CultureInfo.InvariantCulture, + return inString.TryParseNumberToByte( out outByte); } } // Local function to specially parse a double value with an optional percentage sign - bool InternalTryParseDouble(string inString, out double outDouble) + bool InternalTryParseDouble(ReadOnlySpan inString, out double outDouble) { // The percent sign, if it exists, must be at the end of the number - int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + int percentIndex = inString.IndexOf("%".AsSpan(), StringComparison.Ordinal); if (percentIndex >= 0) { - var result = double.TryParse( - inString.Substring(0, percentIndex), - NumberStyles.Number, - CultureInfo.InvariantCulture, - out double percentage); + var result = inString.Slice(0, percentIndex).TryParseNumberToDouble( + out double percentage); outDouble = percentage / 100.0; return result; } else { - return double.TryParse( - inString, - NumberStyles.Number, - CultureInfo.InvariantCulture, + return inString.TryParseNumberToDouble( out outDouble); } } diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index 485bb1db16..5a9e5cd54b 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -302,9 +302,9 @@ namespace Avalonia.Media if (components.Length == 3) // HSL { - if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && - TryInternalParse(components[1], out double saturation) && - TryInternalParse(components[2], out double lightness)) + if (components[0].AsSpan().TryParseNumberToDouble(out double hue) && + TryInternalParse(components[1].AsSpan(), out double saturation) && + TryInternalParse(components[2].AsSpan(), out double lightness)) { hslColor = new HslColor(1.0, hue, saturation, lightness); return true; @@ -312,10 +312,10 @@ namespace Avalonia.Media } else if (components.Length == 4) // HSLA { - if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && - TryInternalParse(components[1], out double saturation) && - TryInternalParse(components[2], out double lightness) && - TryInternalParse(components[3], out double alpha)) + if (components[0].AsSpan().TryParseNumberToDouble(out double hue) && + TryInternalParse(components[1].AsSpan(), out double saturation) && + TryInternalParse(components[2].AsSpan(), out double lightness) && + TryInternalParse(components[3].AsSpan(), out double alpha)) { hslColor = new HslColor(alpha, hue, saturation, lightness); return true; @@ -323,28 +323,22 @@ namespace Avalonia.Media } // Local function to specially parse a double value with an optional percentage sign - bool TryInternalParse(string inString, out double outDouble) + bool TryInternalParse(ReadOnlySpan inString, out double outDouble) { // The percent sign, if it exists, must be at the end of the number - int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + int percentIndex = inString.IndexOf("%".AsSpan(), StringComparison.Ordinal); if (percentIndex >= 0) { - var result = double.TryParse( - inString.Substring(0, percentIndex), - NumberStyles.Number, - CultureInfo.InvariantCulture, - out double percentage); + var result = inString.Slice(0, percentIndex).TryParseNumberToDouble( + out double percentage); outDouble = percentage / 100.0; return result; } else { - return double.TryParse( - inString, - NumberStyles.Number, - CultureInfo.InvariantCulture, + return inString.TryParseNumberToDouble( out outDouble); } } diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index 512e57ae07..30a6c7ef4e 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -302,9 +302,9 @@ namespace Avalonia.Media if (components.Length == 3) // HSV { - if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && - TryInternalParse(components[1], out double saturation) && - TryInternalParse(components[2], out double value)) + if (components[0].AsSpan().TryParseNumberToDouble(out double hue) && + TryInternalParse(components[1].AsSpan(), out double saturation) && + TryInternalParse(components[2].AsSpan(), out double value)) { hsvColor = new HsvColor(1.0, hue, saturation, value); return true; @@ -312,10 +312,10 @@ namespace Avalonia.Media } else if (components.Length == 4) // HSVA { - if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && - TryInternalParse(components[1], out double saturation) && - TryInternalParse(components[2], out double value) && - TryInternalParse(components[3], out double alpha)) + if (components[0].AsSpan().TryParseNumberToDouble(out double hue) && + TryInternalParse(components[1].AsSpan(), out double saturation) && + TryInternalParse(components[2].AsSpan(), out double value) && + TryInternalParse(components[3].AsSpan(), out double alpha)) { hsvColor = new HsvColor(alpha, hue, saturation, value); return true; @@ -323,28 +323,22 @@ namespace Avalonia.Media } // Local function to specially parse a double value with an optional percentage sign - bool TryInternalParse(string inString, out double outDouble) + bool TryInternalParse(ReadOnlySpan inString, out double outDouble) { // The percent sign, if it exists, must be at the end of the number - int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + int percentIndex = inString.IndexOf("%".AsSpan(), StringComparison.Ordinal); if (percentIndex >= 0) { - var result = double.TryParse( - inString.Substring(0, percentIndex), - NumberStyles.Number, - CultureInfo.InvariantCulture, - out double percentage); + var result = inString.Slice(0, percentIndex).TryParseNumberToDouble( + out double percentage); outDouble = percentage / 100.0; return result; } else { - return double.TryParse( - inString, - NumberStyles.Number, - CultureInfo.InvariantCulture, + return inString.TryParseNumberToDouble( out outDouble); } } diff --git a/src/Avalonia.Base/Utilities/SpanHelpers.cs b/src/Avalonia.Base/Utilities/SpanHelpers.cs index e4fab5ac7f..6b779dfd87 100644 --- a/src/Avalonia.Base/Utilities/SpanHelpers.cs +++ b/src/Avalonia.Base/Utilities/SpanHelpers.cs @@ -25,6 +25,30 @@ namespace Avalonia.Utilities return int.TryParse(span.ToString(), out value); #else return int.TryParse(span, out value); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParseNumberToDouble(this ReadOnlySpan span, out double value) + { +#if NETSTANDARD2_0 + return double.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, + out value); +#else + return double.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, + out value); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParseNumberToByte(this ReadOnlySpan span, out byte value) + { +#if NETSTANDARD2_0 + return byte.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, + out value); +#else + return byte.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, + out value); #endif } } diff --git a/src/Avalonia.Build.Tasks/SpanCompat.cs b/src/Avalonia.Build.Tasks/SpanCompat.cs index 6da1fbd07c..af78178b5e 100644 --- a/src/Avalonia.Build.Tasks/SpanCompat.cs +++ b/src/Avalonia.Build.Tasks/SpanCompat.cs @@ -99,11 +99,25 @@ namespace System return int.TryParse(ToString(), out value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryParseNumberToDouble(out double value) + { + return double.TryParse(ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, + out value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryParseNumberToByte(out byte value) + { + return byte.TryParse(ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, + out value); + } + public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length); - internal int IndexOf(string v, StringComparison ordinal, int start = 0) + internal int IndexOf(ReadOnlySpan v, StringComparison ordinal, int start = 0) { - if(Length == 0 || string.IsNullOrEmpty(v)) + if(Length == 0 || v.IsEmpty) { return -1; } From 8dab69663f649d75312e23ba173374d931f933b0 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 18 Oct 2022 00:42:48 +0800 Subject: [PATCH 29/44] fix: fix caption button fluent theme and default theme. --- .../Controls/CaptionButtons.xaml | 10 +++++----- .../Controls/CaptionButtons.xaml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml index c91e008e02..e06fa0c8d4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml @@ -41,7 +41,7 @@ Theme="{StaticResource FluentCaptionButton}" IsVisible="False"> - @@ -61,7 +61,7 @@ - @@ -80,17 +80,17 @@ - - - diff --git a/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml index 379b92ea17..1badffef33 100644 --- a/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml @@ -47,7 +47,7 @@ Theme="{StaticResource SimpleCaptionButton}"> - @@ -69,7 +69,7 @@ - @@ -89,17 +89,17 @@ - - - From 3184252633e0c7828c340eacf57920ecbda230ad Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 18 Oct 2022 01:24:35 +0800 Subject: [PATCH 30/44] fix: revert FullScreenButtonPath. --- src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml | 4 ++-- src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml index e06fa0c8d4..3b1071b36a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml @@ -41,7 +41,7 @@ Theme="{StaticResource FluentCaptionButton}" IsVisible="False"> - @@ -83,7 +83,7 @@ - diff --git a/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml index 1badffef33..8c263d0231 100644 --- a/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml @@ -47,7 +47,7 @@ Theme="{StaticResource SimpleCaptionButton}"> - @@ -92,7 +92,7 @@ - From 2f124a9a480e8a20373531d4e605d096572fc669 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 18 Oct 2022 01:31:08 +0800 Subject: [PATCH 31/44] fix: align with naming convention. --- src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml | 4 ++-- src/Avalonia.Themes.Simple/Controls/CaptionButtons.xaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml index 3b1071b36a..71ae012289 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml @@ -61,7 +61,7 @@ - @@ -80,7 +80,7 @@ -