From fe1f9d4c77ba5e3f723e5b3ee6e44bc425332b85 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 23 Mar 2023 09:43:30 +0000 Subject: [PATCH 01/99] add a failing unit test. --- .../TreeViewTests.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 4f533c2f78..fbcfd99743 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1091,6 +1091,46 @@ namespace Avalonia.Controls.UnitTests Assert.True(called); } } + + [Fact] + public void SelectedItem_Should_Be_Valid_When_SelectedItemChanged_Event_Raised() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + ItemsSource = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.TreeContainerFromItem(item); + + Assert.NotNull(container); + + var called = false; + target.SelectionChanged += (s, e) => + { + Assert.Same(item, e.AddedItems[0]); + Assert.Same(item, target.SelectedItem); + called = true; + }; + + _mouse.Click(container); + + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); + Assert.True(called); + } + } [Fact] public void Bound_SelectedItem_Should_Not_Be_Cleared_when_Changing_Selection() From e77de043b6286ffbe19b85fede130f693e54f483 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 23 Mar 2023 09:46:11 +0000 Subject: [PATCH 02/99] [TreeView] ensure SelectedItem field is set before Adding items to SelectedItems --- src/Avalonia.Controls/TreeView.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 8f2636a783..194ba37671 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -303,12 +303,14 @@ namespace Avalonia.Controls private void SelectSingleItem(object item) { + var oldValue = _selectedItem; _syncingSelectedItems = true; - SelectedItems.Clear(); + SelectedItems.Clear(); + _selectedItem = item; SelectedItems.Add(item); _syncingSelectedItems = false; - SetAndRaise(SelectedItemProperty, ref _selectedItem, item); + RaisePropertyChanged(SelectedItemProperty, oldValue, _selectedItem); } /// From 7c460a9bcbb47c9bc2beef4de7510b7d359d7b72 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 26 Mar 2023 13:04:32 +0000 Subject: [PATCH 03/99] add system bar color attached property --- .../ControlCatalog.Android/MainActivity.cs | 2 +- .../Resources/values-night/colors.xml | 4 ++ .../Avalonia.Android/AvaloniaMainActivity.cs | 4 -- .../Platform/AndroidInsetsManager.cs | 43 +++++++++++++++-- .../Platform/SkiaPlatform/TopLevelImpl.cs | 13 +++--- .../Platform/IInsetsManager.cs | 8 +++- src/Avalonia.Controls/TopLevel.cs | 46 ++++++++++++++++++- .../Controls/EmbeddableControlRoot.xaml | 1 + .../Controls/Window.xaml | 1 + .../Avalonia.Browser/BrowserInsetsManager.cs | 3 ++ src/iOS/Avalonia.iOS/InsetsManager.cs | 3 ++ 11 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 samples/ControlCatalog.Android/Resources/values-night/colors.xml diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index f6fa07dbde..486d14661e 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -5,7 +5,7 @@ using Avalonia.Android; namespace ControlCatalog.Android { - [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] + [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] public class MainActivity : AvaloniaMainActivity { } diff --git a/samples/ControlCatalog.Android/Resources/values-night/colors.xml b/samples/ControlCatalog.Android/Resources/values-night/colors.xml new file mode 100644 index 0000000000..3d47b6fc58 --- /dev/null +++ b/samples/ControlCatalog.Android/Resources/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #212121 + diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index eb4b6bf6a0..b2cd150933 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -32,10 +32,6 @@ namespace Avalonia.Android { lifetime.View = View; } - - Window?.ClearFlags(WindowManagerFlags.TranslucentStatus); - Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); - base.OnCreate(savedInstanceState); SetContentView(View); diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 35d1b06e6a..549815a036 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -2,11 +2,10 @@ using System.Collections.Generic; using Android.OS; using Android.Views; -using AndroidX.AppCompat.App; using AndroidX.Core.View; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls.Platform; -using static Avalonia.Controls.Platform.IInsetsManager; +using Avalonia.Media; namespace Avalonia.Android.Platform { @@ -20,6 +19,7 @@ namespace Avalonia.Android.Platform private bool? _systemUiVisibility; private SystemBarTheme? _statusBarTheme; private bool? _isDefaultSystemBarLightTheme; + private Color? _systemBarColor; public event EventHandler SafeAreaChanged; @@ -36,6 +36,16 @@ namespace Avalonia.Android.Platform } WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value); + + if(value) + { + _activity.Window.AddFlags(WindowManagerFlags.TranslucentStatus); + _activity.Window.AddFlags(WindowManagerFlags.TranslucentNavigation); + } + else + { + SystemBarColor = _systemBarColor; + } } } @@ -93,6 +103,7 @@ namespace Avalonia.Android.Platform public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) { NotifySafeAreaChanged(SafeAreaPadding); + insets = ViewCompat.OnApplyWindowInsets(v, insets); return insets; } @@ -146,8 +157,6 @@ namespace Avalonia.Android.Platform compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light; compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light; - - AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; } } @@ -190,10 +199,36 @@ namespace Avalonia.Android.Platform } } + public Color? SystemBarColor + { + get => _systemBarColor; + set + { + _systemBarColor = value; + + if (_systemBarColor is { } color && !_displayEdgeToEdge && _activity.Window != null) + { + _activity.Window.ClearFlags(WindowManagerFlags.TranslucentStatus); + _activity.Window.ClearFlags(WindowManagerFlags.TranslucentNavigation); + _activity.Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); + + var androidColor = global::Android.Graphics.Color.Argb(color.A, color.R, color.G, color.B); + _activity.Window.SetStatusBarColor(androidColor); + + if (Build.VERSION.SdkInt >= BuildVersionCodes.O) + { + // As we can only change the navigation bar's foreground api 26 and newer, we only change the background color if running on those versions + _activity.Window.SetNavigationBarColor(androidColor); + } + } + } + } + internal void ApplyStatusBarState() { IsSystemBarVisible = _systemUiVisibility; SystemBarTheme = _statusBarTheme; + SystemBarColor = _systemBarColor; } private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index b8d80a50ff..4e783a0873 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -3,9 +3,13 @@ using System.Collections.Generic; using Android.App; using Android.Content; using Android.Graphics; +using Android.Graphics.Drawables; +using Android.OS; using Android.Runtime; +using Android.Text; using Android.Views; using Android.Views.InputMethods; +using AndroidX.AppCompat.App; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Storage; @@ -22,13 +26,6 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Java.Lang; -using Java.Util; -using Math = System.Math; -using AndroidRect = Android.Graphics.Rect; -using Window = Android.Views.Window; -using Android.Graphics.Drawables; -using Android.OS; -using Android.Text; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -286,6 +283,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform _ => null, }; } + + AppCompatDelegate.DefaultNightMode = themeVariant == PlatformThemeVariant.Light ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs index 072bace154..c604b89e5c 100644 --- a/src/Avalonia.Controls/Platform/IInsetsManager.cs +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media; using Avalonia.Metadata; #nullable enable @@ -22,7 +23,12 @@ namespace Avalonia.Controls.Platform /// Gets the current safe area padding. /// Thickness SafeAreaPadding { get; } - + + /// + /// Gets or sets the color of the platform's system bars + /// + Color? SystemBarColor { get; set; } + /// /// Occurs when safe area for the current window changes. /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index bf0de1d79f..19ec9b9e0e 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -87,7 +87,15 @@ namespace Avalonia.Controls /// public static readonly StyledProperty RequestedThemeVariantProperty = ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); - + + /// + /// Defines the SystemBarColor attached property. + /// + public static readonly AttachedProperty SystemBarColorProperty = + AvaloniaProperty.RegisterAttached( + "SystemBarColor", + inherits: true); + /// /// Defines the event. /// @@ -124,6 +132,22 @@ namespace Avalonia.Controls { KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); AffectsMeasure(ClientSizeProperty); + + SystemBarColorProperty.Changed.AddClassHandler((view, e) => + { + if (e.NewValue is SolidColorBrush colorBrush) + { + if (view.Parent is TopLevel tl && tl.InsetsManager is { } insetsManager) + { + insetsManager.SystemBarColor = colorBrush.Color; + } + + if (view is TopLevel topLevel && topLevel.InsetsManager is { } insets) + { + insets.SystemBarColor = colorBrush.Color; + } + } + }); } /// @@ -379,6 +403,26 @@ namespace Avalonia.Controls set { SetValue(AccessText.ShowAccessKeyProperty, value); } } + /// + /// Helper for setting the color of the platform's system bars + /// + /// The main view attached to the toplevel, or the toplevel + /// The color to set + public static void SetSystemBarColor(Control control, SolidColorBrush? color) + { + control.SetValue(SystemBarColorProperty, color); + } + + /// + /// Helper for getting the color of the platform's system bars + /// + /// The main view attached to the toplevel, or the toplevel + /// The current color of the platform's system bars + public static SolidColorBrush? GetSystemBarColor(Control control) + { + return control.GetValue(SystemBarColorProperty); + } + /// double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1; diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index 6a2651e3f5..f60424a2dc 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -3,6 +3,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/Window.xaml b/src/Avalonia.Themes.Fluent/Controls/Window.xaml index 35cc81663f..ff27cce800 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Window.xaml @@ -3,6 +3,7 @@ + diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs index 30f80ba27c..0f64003699 100644 --- a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Avalonia.Browser.Interop; using Avalonia.Controls.Platform; +using Avalonia.Media; using static Avalonia.Controls.Platform.IInsetsManager; namespace Avalonia.Browser @@ -37,6 +38,8 @@ namespace Avalonia.Browser } } + public Color? SystemBarColor { get; set; } + public void NotifySafeAreaPaddingChanged() { SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs index 62e560ddf9..bd6f989dbd 100644 --- a/src/iOS/Avalonia.iOS/InsetsManager.cs +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls.Platform; +using Avalonia.Media; using UIKit; namespace Avalonia.iOS; @@ -80,4 +81,6 @@ internal class InsetsManager : IInsetsManager } public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default; + + public Color? SystemBarColor { get; set; } } From 12fa653ca2f8daed5244918de29c1a0af3c0d326 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 10:22:08 -0400 Subject: [PATCH 04/99] Use Transparent background in ColorPreviewer so DropShadow appears This addresses #10539. Drop shadow for now requires the Background to be set in order to work correctly. --- .../Themes/Fluent/ColorPreviewer.xaml | 1 + .../Themes/Simple/ColorPreviewer.xaml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml index 3a88d25ef1..e05fa5a907 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -64,6 +64,7 @@ diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml index 0e51a0519a..a39dd91f52 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml @@ -64,6 +64,7 @@ From df7cf037e02dd43f043addece6095be843347f2e Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 10:22:35 -0400 Subject: [PATCH 05/99] Use SetCurrentValue() in ToggleSplitButton to fix warning --- src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index 70d2983b9b..509b58833f 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -70,7 +70,7 @@ namespace Avalonia.Controls /// protected void Toggle() { - IsChecked = !IsChecked; + SetCurrentValue(IsCheckedProperty, !IsChecked); } /// From d955a777e55fcee1a771f6c81cb9a94b00505fb5 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 10:51:06 -0400 Subject: [PATCH 06/99] Switch HexInputAlphaPositionProperty default to Leading to match XAML The control themes are now responsible to set the property to match slider position and CSS standards. Doing this has better compatibility with XAML by default. --- .../ColorView/ColorView.Properties.cs | 2 +- .../Themes/Fluent/ColorPicker.xaml | 2 ++ src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml | 2 ++ .../Themes/Simple/ColorPicker.xaml | 2 ++ src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml | 2 ++ 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index e334a1d323..532e87a9fc 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls public static readonly StyledProperty HexInputAlphaPositionProperty = AvaloniaProperty.Register( nameof(HexInputAlphaPosition), - AlphaComponentPosition.Trailing); // Match CSS (and default slider order) instead of XAML/WinUI + AlphaComponentPosition.Leading); // By default match XAML and the WinUI control /// /// Defines the property. diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index a9f52b93c7..b3c7cd9f9c 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -6,6 +6,8 @@ + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index f72fb11bbe..acd2c7ff15 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -295,6 +295,8 @@ + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index 7639073775..ff4e1d93a8 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -6,6 +6,8 @@ + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml index 4e219a98af..a26d3179b5 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml @@ -257,6 +257,8 @@ + + From 0e92b5b742546edca533d785b4ee03c6f8ebb29f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 10:52:03 -0400 Subject: [PATCH 07/99] Always use AlphaComponentPosition.Leading in DevTools color property editors --- .../Diagnostics/Controls/BrushEditor.cs | 6 +++++- .../Diagnostics/Views/PropertyValueEditorView.cs | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs index b7579ed31b..ff05614667 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs @@ -34,7 +34,11 @@ namespace Avalonia.Diagnostics.Controls { case ISolidColorBrush scb: { - var colorView = new ColorView { Color = scb.Color }; + var colorView = new ColorView + { + HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML + Color = scb.Color, + }; colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index 6b52989f0b..40b0ce4ca4 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -129,7 +129,10 @@ namespace Avalonia.Diagnostics.Views IsEnabled = !Property.IsReadonly }; - var cv = new ColorView(); + var cv = new ColorView + { + HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML + }; cv.Bind( ColorView.ColorProperty, From bcf1431bf9ebd9a75959a4910e2980328c29bf70 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 26 Mar 2023 11:17:21 -0400 Subject: [PATCH 08/99] Move AlphaComponentPosition into a better directory --- .../{ColorView => }/AlphaComponentPosition.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ColorView => }/AlphaComponentPosition.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs b/src/Avalonia.Controls.ColorPicker/AlphaComponentPosition.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs rename to src/Avalonia.Controls.ColorPicker/AlphaComponentPosition.cs From a401819251cfb72d7bcf2d178e34a5da3e164c95 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 27 Mar 2023 20:51:38 -0400 Subject: [PATCH 09/99] Remove alpha in Hex color strings when IsAlphaEnabled or IsAlphaVisible is false --- .../ColorView/ColorView.cs | 6 ++- .../Converters/ColorToHexConverter.cs | 40 +++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 274e7f5851..7674b74b6a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -61,7 +61,11 @@ namespace Avalonia.Controls { if (_hexTextBox != null) { - _hexTextBox.Text = ColorToHexConverter.ToHexString(Color, HexInputAlphaPosition); + _hexTextBox.Text = ColorToHexConverter.ToHexString( + Color, + HexInputAlphaPosition, + includeAlpha: (IsAlphaEnabled && IsAlphaVisible), + includeSymbol: false); } } diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 8798f874f4..8257499d70 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -11,6 +11,18 @@ namespace Avalonia.Controls.Converters /// public class ColorToHexConverter : IValueConverter { + /// + /// Gets or sets a value indicating whether the alpha component is visible in the Hex formatted text. + /// + /// + /// When hidden the existing alpha component value is maintained. Also when hidden the user is still + /// able to input an 8-digit number with alpha. Alpha will be processed but then removed when displayed. + /// + /// Because this property only controls whether alpha is displayed (and it is still processed regardless) + /// it is termed 'Visible' instead of 'Enabled'. + /// + public bool IsAlphaVisible { get; set; } = true; + /// /// Gets or sets the position of a color's alpha component relative to all other components. /// @@ -48,7 +60,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - return ToHexString(color, AlphaPosition, includeSymbol); + return ToHexString(color, AlphaPosition, IsAlphaVisible, includeSymbol); } /// @@ -67,26 +79,40 @@ namespace Avalonia.Controls.Converters /// /// The color to represent as a hex value string. /// The output position of the alpha component. + /// Whether the alpha component will be included in the hex string. /// Whether the hex symbol '#' will be added. /// The input color converted to its hex value string. public static string ToHexString( Color color, AlphaComponentPosition alphaPosition, + bool includeAlpha = true, bool includeSymbol = false) { uint intColor; - if (alphaPosition == AlphaComponentPosition.Trailing) + string hexColor; + + if (includeAlpha) { - intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A; + if (alphaPosition == AlphaComponentPosition.Trailing) + { + intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A; + } + else + { + // Default is Leading alpha (same as XAML) + intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + } + + hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); } else { - // Default is Leading alpha - intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + // In this case the alpha position no longer matters + // Both cases are calculated the same + intColor = ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B; + hexColor = intColor.ToString("x6", CultureInfo.InvariantCulture).ToUpperInvariant(); } - string hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); - if (includeSymbol) { hexColor = '#' + hexColor; From faf1d8395006ddc29850dae9cb86a7999ac311f2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 08:18:03 +0200 Subject: [PATCH 10/99] Fix merge error. --- .../TreeViewTests.cs | 51 +++++++------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index a9b43a0d64..3ca70f96cc 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -721,45 +721,32 @@ namespace Avalonia.Controls.UnitTests target.SelectedItem = item; Assert.True(called); } - + [Fact] public void SelectedItem_Should_Be_Valid_When_SelectedItemChanged_Event_Raised() { - using (Application()) - { - var tree = CreateTestTreeData(); - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - ItemsSource = tree, - }; - - var visualRoot = new TestRoot(); - visualRoot.Child = target; + using var app = Start(); + var data = CreateTestTreeData(); + var target = CreateTarget(data: data); - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + var item = data[0].Children[1].Children[0]; + var container = Assert.IsType(target.TreeContainerFromItem(item)); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.TreeContainerFromItem(item); + Assert.NotNull(container); - Assert.NotNull(container); - - var called = false; - target.SelectionChanged += (s, e) => - { - Assert.Same(item, e.AddedItems[0]); - Assert.Same(item, target.SelectedItem); - called = true; - }; + var called = false; + target.SelectionChanged += (s, e) => + { + Assert.Same(item, e.AddedItems[0]); + Assert.Same(item, target.SelectedItem); + called = true; + }; - _mouse.Click(container); + _mouse.Click(container); - Assert.Equal(item, target.SelectedItem); - Assert.True(container.IsSelected); - Assert.True(called); - } + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); + Assert.True(called); } [Fact] @@ -796,7 +783,7 @@ namespace Avalonia.Controls.UnitTests using var app = Start(); var data = CreateTestTreeData(); var target = CreateTarget(data: data, expandAll: false); - + target.SelectedItem = data[0].Children[1]; var rootItem = Assert.IsType(target.ContainerFromIndex(0)); From c6afd9bf4fed6b29f598ff842038a966a837ce2d Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Mar 2023 13:13:06 +0200 Subject: [PATCH 11/99] Fix NRE for AvaloniaNativeTextInputMethod --- .../AvaloniaNativeTextInputMethod.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs index 796a0ced37..8438ef10b5 100644 --- a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs +++ b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs @@ -2,12 +2,14 @@ using Avalonia.Input.TextInput; using Avalonia.Native.Interop; +#nullable enable + namespace Avalonia.Native { internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable { - private ITextInputMethodClient _client; - private IAvnTextInputMethodClient _nativeClient; + private ITextInputMethodClient? _client; + private IAvnTextInputMethodClient? _nativeClient; private readonly IAvnTextInputMethod _inputMethod; public AvaloniaNativeTextInputMethod(IAvnWindowBase nativeWindow) @@ -26,7 +28,7 @@ namespace Avalonia.Native _inputMethod.Reset(); } - public void SetClient(ITextInputMethodClient client) + public void SetClient(ITextInputMethodClient? client) { if (_client is { SupportsSurroundingText: true }) { @@ -39,9 +41,9 @@ namespace Avalonia.Native _nativeClient = null; _client = client; - if (client != null) + if (_client != null) { - _nativeClient = new AvnTextInputMethodClient(client); + _nativeClient = new AvnTextInputMethodClient(_client); OnSurroundingTextChanged(this, EventArgs.Empty); OnCursorRectangleChanged(this, EventArgs.Empty); @@ -53,16 +55,28 @@ namespace Avalonia.Native _inputMethod.SetClient(_nativeClient); } - private void OnCursorRectangleChanged(object sender, EventArgs e) + private void OnCursorRectangleChanged(object? sender, EventArgs e) { if (_client == null) { return; } - var visualRoot = _client.TextViewVisual.VisualRoot; + var textViewVisual = _client.TextViewVisual; + + if(textViewVisual is null ) + { + return; + } + + var visualRoot = textViewVisual.VisualRoot; + + if(visualRoot is null) + { + return; + } - var transform = _client.TextViewVisual.TransformToVisual((Visual)visualRoot); + var transform = textViewVisual.TransformToVisual((Visual)visualRoot); if (transform == null) { @@ -74,7 +88,7 @@ namespace Avalonia.Native _inputMethod.SetCursorRect(rect.ToAvnRect()); } - private void OnSurroundingTextChanged(object sender, EventArgs e) + private void OnSurroundingTextChanged(object? sender, EventArgs e) { if (_client == null) { From 45bf804cd451cab04c9a35c6e8263f45229a125c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Mar 2023 13:45:39 +0200 Subject: [PATCH 12/99] Fix SelectableTextBlock selection --- src/Avalonia.Controls/SelectableTextBlock.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 1007001f05..8622720236 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -18,10 +18,10 @@ namespace Avalonia.Controls public class SelectableTextBlock : TextBlock, IInlineHost { public static readonly StyledProperty SelectionStartProperty = - TextBox.SelectionStartProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); + TextBox.SelectionStartProperty.AddOwner(); public static readonly StyledProperty SelectionEndProperty = - TextBox.SelectionEndProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex)); + TextBox.SelectionEndProperty.AddOwner(); public static readonly DirectProperty SelectedTextProperty = AvaloniaProperty.RegisterDirect( @@ -228,7 +228,7 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - var text = Text; + var text = HasComplexContent ? Inlines.Text : Text; var clickInfo = e.GetCurrentPoint(this); if (text != null && clickInfo.Properties.IsLeftButtonPressed) @@ -314,7 +314,7 @@ namespace Avalonia.Controls // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - var text = Text; + var text = HasComplexContent ? Inlines.Text : Text; var padding = Padding; var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); @@ -397,7 +397,10 @@ namespace Avalonia.Controls private string GetSelection() { - var textLength = Text?.Length ?? 0; + var text = HasComplexContent ? Inlines.Text : Text; + + var textLength = text?.Length ?? 0; + if (textLength == 0) { return ""; @@ -415,7 +418,7 @@ namespace Avalonia.Controls var length = Math.Max(0, end - start); - var selectedText = Text!.Substring(start, length); + var selectedText = text!.Substring(start, length); return selectedText; } From 1964afb854dc13731e19fd8366399d83c89b62d0 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Mar 2023 14:15:24 +0200 Subject: [PATCH 13/99] Fix possible NRE --- src/Avalonia.Controls/SelectableTextBlock.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 8622720236..66ae4a26bb 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -228,7 +228,7 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - var text = HasComplexContent ? Inlines.Text : Text; + var text = HasComplexContent ? Inlines?.Text : Text; var clickInfo = e.GetCurrentPoint(this); if (text != null && clickInfo.Properties.IsLeftButtonPressed) @@ -314,7 +314,7 @@ namespace Avalonia.Controls // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - var text = HasComplexContent ? Inlines.Text : Text; + var text = HasComplexContent ? Inlines?.Text : Text; var padding = Padding; var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); @@ -397,7 +397,7 @@ namespace Avalonia.Controls private string GetSelection() { - var text = HasComplexContent ? Inlines.Text : Text; + var text = HasComplexContent ? Inlines?.Text : Text; var textLength = text?.Length ?? 0; From e7aaf8a7f7120c971da687613aaf136b5469ae1e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 14:45:19 +0200 Subject: [PATCH 14/99] Ensure that the default value can be coerced. When the coercion function on a property results in the default value being coerced, ensure that state is recorded in the value store. --- src/Avalonia.Base/AvaloniaProperty.cs | 6 + src/Avalonia.Base/DirectPropertyBase.cs | 5 + .../PropertyStore/EffectiveValue.cs | 49 ++++++- .../PropertyStore/EffectiveValue`1.cs | 42 ++++-- src/Avalonia.Base/PropertyStore/ValueStore.cs | 52 ++++++-- src/Avalonia.Base/StyledProperty.cs | 5 + .../AvaloniaObjectTests_Coercion.cs | 125 +++++++++++++++++- .../AvaloniaPropertyTests.cs | 5 + 8 files changed, 260 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index e98d9f0517..c57131f7b5 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -499,6 +499,12 @@ namespace Avalonia /// The object instance. internal abstract void RouteClearValue(AvaloniaObject o); + /// + /// Routes an untyped CoerceValue call on a property with its default value to a typed call. + /// + /// The object instance. + internal abstract void RouteCoerceDefaultValue(AvaloniaObject o); + /// /// Routes an untyped GetValue call to a typed call. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 94dfaaab01..7e5a962157 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -117,6 +117,11 @@ namespace Avalonia o.ClearValue(this); } + internal override void RouteCoerceDefaultValue(AvaloniaObject o) + { + // Do nothing. + } + /// internal override object? RouteGetValue(AvaloniaObject o) { diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 7e9f9ae9ba..a00a51e694 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -36,12 +36,23 @@ namespace Avalonia.PropertyStore /// public IValueEntry? BaseValueEntry { get; private set; } + /// + /// Gets a value indicating whether the property has a coercion function. + /// + public bool HasCoercion { get; protected set; } + /// /// Gets a value indicating whether the was overridden by a call to /// . /// public bool IsOverridenCurrentValue { get; set; } + /// + /// Gets a value indicating whether the is the result of the + /// + /// + public bool IsCoercedDefaultValue { get; set; } + /// /// Begins a reevaluation pass on the effective value. /// @@ -63,10 +74,33 @@ namespace Avalonia.PropertyStore /// /// Ends a reevaluation pass on the effective value. /// + /// The associated value store. + /// The property being reevaluated. /// - /// This method unsubscribes from any unused value entries. + /// Handles coercing the default value if necessary. /// - public void EndReevaluation() + public void EndReevaluation(ValueStore owner, AvaloniaProperty property) + { + if (Priority == BindingPriority.Unset && HasCoercion) + CoerceDefaultValueAndRaise(owner, property); + } + + /// + /// Gets a value indicating whether the effective value represents the default value of the + /// property and can be removed. + /// + /// True if the effective value van be removed; otherwise false. + public bool CanRemove() + { + return Priority == BindingPriority.Unset && + !IsOverridenCurrentValue && + !IsCoercedDefaultValue; + } + + /// + /// Unsubscribes from any unused value entries. + /// + public void UnsubscribeIfNecessary() { if (Priority == BindingPriority.Unset) { @@ -130,6 +164,17 @@ namespace Avalonia.PropertyStore /// The property being cleared. public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property); + /// + /// Coerces the default value, raising + /// where necessary. + /// + /// The associated value store. + /// The property being coerced. + protected abstract void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property); + + /// + /// Gets the current effective value as a boxed value. + /// protected abstract object? GetBoxedValue(); protected void UpdateValueEntry(IValueEntry? entry, BindingPriority priority) diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 330034f51d..369cd3ea63 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Avalonia.Data; -using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; namespace Avalonia.PropertyStore { @@ -33,19 +32,16 @@ namespace Avalonia.PropertyStore if (_metadata.CoerceValue is { } coerce) { + HasCoercion = true; _uncommon = new() { _coerce = coerce, _uncoercedValue = value, _uncoercedBaseValue = value, }; - - Value = coerce(owner, value); - } - else - { - Value = value; } + + Value = value; } /// @@ -61,7 +57,7 @@ namespace Avalonia.PropertyStore Debug.Assert(priority != BindingPriority.LocalValue); UpdateValueEntry(value, priority); - SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false); + SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority); if (priority > BindingPriority.LocalValue && value.GetDataValidationState(out var state, out var error)) @@ -75,7 +71,7 @@ namespace Avalonia.PropertyStore StyledProperty property, T value) { - SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false); + SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); } public void SetCurrentValueAndRaise( @@ -83,8 +79,15 @@ namespace Avalonia.PropertyStore StyledProperty property, T value) { - IsOverridenCurrentValue = true; - SetAndRaiseCore(owner, property, value, Priority, true); + SetAndRaiseCore(owner, property, value, Priority, isOverriddenCurrentValue: true); + } + + public void SetCoercedDefaultValueAndRaise( + ValueStore owner, + StyledProperty property, + T value) + { + SetAndRaiseCore(owner, property, value, Priority, isCoercedDefaultValue: true); } public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) @@ -117,7 +120,7 @@ namespace Avalonia.PropertyStore Debug.Assert(Priority != BindingPriority.Animation); Debug.Assert(BasePriority != BindingPriority.Unset); UpdateValueEntry(null, BindingPriority.Animation); - SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority, false); + SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority); } public override void CoerceValue(ValueStore owner, AvaloniaProperty property) @@ -168,6 +171,17 @@ namespace Avalonia.PropertyStore } } + protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property) + { + Debug.Assert(_uncommon?._coerce is not null); + Debug.Assert(Priority == BindingPriority.Unset); + + var coercedDefaultValue = _uncommon!._coerce!(owner.Owner, _metadata.DefaultValue); + + if (!EqualityComparer.Default.Equals(_metadata.DefaultValue, coercedDefaultValue)) + SetCoercedDefaultValueAndRaise(owner, (StyledProperty)property, coercedDefaultValue); + } + protected override object? GetBoxedValue() => Value; private static T GetValue(IValueEntry entry) @@ -183,7 +197,8 @@ namespace Avalonia.PropertyStore StyledProperty property, T value, BindingPriority priority, - bool isOverriddenCurrentValue) + bool isOverriddenCurrentValue = false, + bool isCoercedDefaultValue = false) { var oldValue = Value; var valueChanged = false; @@ -191,6 +206,7 @@ namespace Avalonia.PropertyStore var v = value; IsOverridenCurrentValue = isOverriddenCurrentValue; + IsCoercedDefaultValue = isCoercedDefaultValue; if (_uncommon?._coerce is { } coerce) v = coerce(owner.Owner, value); diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index af31459a98..0ef14272b4 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -259,6 +259,27 @@ namespace Avalonia.PropertyStore { if (_effectiveValues.TryGetValue(property, out var v)) v.CoerceValue(this, property); + else + property.RouteCoerceDefaultValue(Owner); + } + + public void CoerceDefaultValue(StyledProperty property) + { + var metadata = property.GetMetadata(Owner.GetType()); + + if (metadata.CoerceValue is null) + return; + + var coercedDefaultValue = metadata.CoerceValue(Owner, metadata.DefaultValue); + + if (EqualityComparer.Default.Equals(metadata.DefaultValue, coercedDefaultValue)) + return; + + // We have a situation where the default value isn't valid according to the coerce + // function. In this case, we need to create an EffectiveValue entry. + var effectiveValue = CreateEffectiveValue(property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetCoercedDefaultValueAndRaise(this, property, coercedDefaultValue); } public Optional GetBaseValue(StyledProperty property) @@ -838,20 +859,25 @@ namespace Avalonia.PropertyStore goto restart; } - if (current?.Priority == BindingPriority.Unset) + if (current is not null) { - if (current.BasePriority == BindingPriority.Unset) - { - RemoveEffectiveValue(property); - current.DisposeAndRaiseUnset(this, property); - } - else + current.EndReevaluation(this, property); + + if (current.CanRemove()) { - current.RemoveAnimationAndRaise(this, property); + if (current.BasePriority == BindingPriority.Unset) + { + RemoveEffectiveValue(property); + current.DisposeAndRaiseUnset(this, property); + } + else + { + current.RemoveAnimationAndRaise(this, property); + } } - } - current?.EndReevaluation(); + current.UnsubscribeIfNecessary(); + } } finally { @@ -923,7 +949,9 @@ namespace Avalonia.PropertyStore { _effectiveValues.GetKeyValue(i, out var key, out var e); - if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue) + e.EndReevaluation(this, key); + + if (e.CanRemove()) { RemoveEffectiveValue(key, i); e.DisposeAndRaiseUnset(this, key); @@ -932,7 +960,7 @@ namespace Avalonia.PropertyStore break; } - e.EndReevaluation(); + e.UnsubscribeIfNecessary(); } } finally diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index e1b88cde49..5cb330eda9 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -176,6 +176,11 @@ namespace Avalonia o.ClearValue(this); } + internal override void RouteCoerceDefaultValue(AvaloniaObject o) + { + o.GetValueStore().CoerceDefaultValue(this); + } + /// internal override object? RouteGetValue(AvaloniaObject o) { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index 2154bb63d0..d53c055639 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Reactive.Subjects; +using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Styling; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests @@ -182,11 +185,103 @@ namespace Avalonia.Base.UnitTests Assert.Equal(-150, target.Foo); } + [Fact] + public void Default_Value_Can_Be_Coerced() + { + var target = new Class1(); + var raised = 0; + + target.MinFoo = 20; + + target.PropertyChanged += (s, e) => + { + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal(11, e.OldValue); + Assert.Equal(20, e.NewValue); + Assert.Equal(BindingPriority.Unset, e.Priority); + ++raised; + }; + + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(20, target.Foo); + Assert.Equal(1, raised); + } + + [Fact] + public void ClearValue_Respects_Coerced_Default_Value() + { + var target = new Class1(); + var raised = 0; + + target.Foo = 30; + target.MinFoo = 20; + + target.PropertyChanged += (s, e) => + { + Assert.Equal(Class1.FooProperty, e.Property); + Assert.Equal(30, e.OldValue); + Assert.Equal(20, e.NewValue); + Assert.Equal(BindingPriority.Unset, e.Priority); + ++raised; + }; + + target.ClearValue(Class1.FooProperty); + + Assert.Equal(20, target.Foo); + Assert.Equal(1, raised); + } + + [Fact] + public void Deactivating_Style_Respects_Coerced_Default_Value() + { + var target = new Control1 + { + MinFoo = 20, + }; + + var root = new TestRoot + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Control1.FooProperty, 50), + }, + }, + }, + Child = target, + }; + + var raised = 0; + + target.Classes.Add("foo"); + root.LayoutManager.ExecuteInitialLayoutPass(); + + Assert.Equal(50, target.Foo); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(Control1.FooProperty, e.Property); + Assert.Equal(50, e.OldValue); + Assert.Equal(20, e.NewValue); + Assert.Equal(BindingPriority.Unset, e.Priority); + ++raised; + }; + + target.Classes.Remove("foo"); + + Assert.Equal(20, target.Foo); + Assert.Equal(1, raised); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register( - "Qux", + "Foo", defaultValue: 11, coerce: CoerceFoo); @@ -215,13 +310,15 @@ namespace Avalonia.Base.UnitTests set => SetValue(InheritedProperty, value); } + public int MinFoo { get; set; } = 0; public int MaxFoo { get; set; } = 100; public List CoreChanges { get; } = new(); public static int CoerceFoo(AvaloniaObject instance, int value) { - return Math.Min(((Class1)instance).MaxFoo, value); + var o = (Class1)instance; + return Math.Clamp(value, o.MinFoo, o.MaxFoo); } protected override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) @@ -266,5 +363,29 @@ namespace Avalonia.Base.UnitTests return -value; } } + + private class Control1 : Control + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Foo", + defaultValue: 11, + coerce: CoerceFoo); + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public int MinFoo { get; set; } = 0; + public int MaxFoo { get; set; } = 100; + + public static int CoerceFoo(AvaloniaObject instance, int value) + { + var o = (Control1)instance; + return Math.Clamp(value, o.MinFoo, o.MaxFoo); + } + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index bde750efdc..e44c15d962 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -179,6 +179,11 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } + internal override void RouteCoerceDefaultValue(AvaloniaObject o) + { + throw new NotImplementedException(); + } + internal override object RouteGetValue(AvaloniaObject o) { throw new NotImplementedException(); From 964f30e883b31d8f8825e64f68d49d3fecf57475 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 14:50:10 +0200 Subject: [PATCH 15/99] Describe an edge-case. --- .../AvaloniaObjectTests_Coercion.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs index d53c055639..fe4262331f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -277,6 +277,21 @@ namespace Avalonia.Base.UnitTests Assert.Equal(1, raised); } + [Fact] + public void If_Initial_State_Has_Coerced_Default_Value_Then_CoerceValue_Must_Be_Called() + { + // This test is just explicitly describing an edge-case. If the initial state of the + // object results in a coerced property value then CoerceValue must be called before + // coercion takes effect. Confirmed as matching the behavior of WPF. + var target = new Class3(); + + Assert.Equal(11, target.Foo); + + target.CoerceValue(Class3.FooProperty); + + Assert.Equal(50, target.Foo); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = @@ -364,6 +379,28 @@ namespace Avalonia.Base.UnitTests } } + private class Class3: AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Foo", + defaultValue: 11, + coerce: CoerceFoo); + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + + public static int CoerceFoo(AvaloniaObject instance, int value) + { + var o = (Class3)instance; + return Math.Clamp(value, 50, 100); + } + } + private class Control1 : Control { public static readonly StyledProperty FooProperty = From 055f8940123e0d704e457f379eb5400a5484f95c Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Thu, 16 Mar 2023 21:38:03 +0100 Subject: [PATCH 16/99] Added conversion option to PropertyObservable --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 41 +++++++-- .../Data/Core/AvaloniaPropertyAccessorNode.cs | 2 +- .../AvaloniaPropertyBindingObservable.cs | 85 +++++++++++++++---- .../Reactive/AvaloniaPropertyObservable.cs | 73 +++++++++++++--- 4 files changed, 162 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 9fbf680a5c..0c22213d33 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -1,6 +1,6 @@ using System; -using Avalonia.Reactive; using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia { @@ -34,8 +34,8 @@ namespace Avalonia /// public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyObservable( - o ?? throw new ArgumentNullException(nameof(o)), + return new AvaloniaPropertyObservable( + o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } @@ -54,11 +54,23 @@ namespace Avalonia /// public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyObservable( + return new AvaloniaPropertyObservable( o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } + /// + /// + /// + /// A method which is executed to convert each property value to . + public static IObservable GetObservable(this AvaloniaObject o, AvaloniaProperty property, Func converter) + { + return new AvaloniaPropertyObservable( + o ?? throw new ArgumentNullException(nameof(o)), + property ?? throw new ArgumentNullException(nameof(property)), + converter ?? throw new ArgumentNullException(nameof(converter))); + } + /// /// Gets an observable for an . /// @@ -75,7 +87,7 @@ namespace Avalonia this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyBindingObservable( + return new AvaloniaPropertyBindingObservable( o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } @@ -97,12 +109,27 @@ namespace Avalonia this AvaloniaObject o, AvaloniaProperty property) { - return new AvaloniaPropertyBindingObservable( + return new AvaloniaPropertyBindingObservable( o ?? throw new ArgumentNullException(nameof(o)), property ?? throw new ArgumentNullException(nameof(property))); } + /// + /// + /// + /// A method which is executed to convert each property value to . + public static IObservable> GetBindingObservable( + this AvaloniaObject o, + AvaloniaProperty property, + Func converter) + { + return new AvaloniaPropertyBindingObservable( + o ?? throw new ArgumentNullException(nameof(o)), + property ?? throw new ArgumentNullException(nameof(property)), + converter ?? throw new ArgumentNullException(nameof(converter))); + } + /// /// Gets an observable that listens for property changed events for an /// . @@ -338,7 +365,7 @@ namespace Avalonia return InstancedBinding.OneWay(_source); } } - + private class ClassHandlerObserver : IObserver> { private readonly Action> _action; diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 92fc843394..4bf24e901e 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -40,7 +40,7 @@ namespace Avalonia.Data.Core { if (reference.TryGetTarget(out var target) && target is AvaloniaObject obj) { - _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); + _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); } else { diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs index 0789684eff..fd68381d55 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -4,18 +4,21 @@ using Avalonia.Data; namespace Avalonia.Reactive { - internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription + internal class AvaloniaPropertyBindingObservable : LightweightObservableBase>, IDescription { private readonly WeakReference _target; private readonly AvaloniaProperty _property; - private BindingValue _value = BindingValue.Unset; + private readonly Func? _converter; + private BindingValue _value = BindingValue.Unset; public AvaloniaPropertyBindingObservable( AvaloniaObject target, - AvaloniaProperty property) + AvaloniaProperty property, + Func? converter = null) { _target = new WeakReference(target); _property = property; + _converter = converter; } public string Description => $"{_target.GetType().Name}.{_property.Name}"; @@ -24,8 +27,17 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - _value = (T)target.GetValue(_property)!; - target.PropertyChanged += PropertyChanged; + if (_converter is { } converter) + { + var unconvertedValue = (TSource)target.GetValue(_property)!; + _value = converter(unconvertedValue); + target.PropertyChanged += PropertyChanged_WithConversion; + } + else + { + _value = (TResult)target.GetValue(_property)!; + target.PropertyChanged += PropertyChanged; + } } } @@ -33,11 +45,18 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - target.PropertyChanged -= PropertyChanged; + if (_converter is not null) + { + target.PropertyChanged -= PropertyChanged_WithConversion; + } + else + { + target.PropertyChanged -= PropertyChanged; + } } } - protected override void Subscribed(IObserver> observer, bool first) + protected override void Subscribed(IObserver> observer, bool first) { if (_value.Type != BindingValueType.UnsetValue) { @@ -49,27 +68,59 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - if (e is AvaloniaPropertyChangedEventArgs typedArgs) + if (e is AvaloniaPropertyChangedEventArgs typedArgs) { - var newValue = e.Sender.GetValue(typedArgs.Property); + PublishValue(e.Sender.GetValue(typedArgs.Property)); + } + else + { + PublishUntypedValue(e.Sender.GetValue(e.Property)); + } + } + } - if (!_value.HasValue || !EqualityComparer.Default.Equals(newValue, _value.Value)) - { - _value = newValue; - PublishNext(_value); - } + private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + if (e is AvaloniaPropertyChangedEventArgs typedArgs) + { + var newValueRaw = e.Sender.GetValue(typedArgs.Property); + + var newValue = _converter!(newValueRaw); + + PublishValue(newValue); } else { var newValue = e.Sender.GetValue(e.Property); - if (!Equals(newValue, _value)) + if (newValue is TSource source) { - _value = (T)newValue!; - PublishNext(_value); + newValue = _converter!(source); } + + PublishUntypedValue(newValue); } } } + + private void PublishValue(TResult newValue) + { + if (!_value.HasValue || !EqualityComparer.Default.Equals(newValue, _value.Value)) + { + _value = newValue; + PublishNext(_value); + } + } + + private void PublishUntypedValue(object? newValue) + { + if (!Equals(newValue, _value)) + { + _value = (TResult)newValue!; + PublishNext(_value); + } + } } } diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index a4fa587a50..0d40fa96e6 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -4,18 +4,21 @@ using Avalonia.Data; namespace Avalonia.Reactive { - internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription + internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription { private readonly WeakReference _target; private readonly AvaloniaProperty _property; - private Optional _value; + private readonly Func? _converter; + private Optional _value; public AvaloniaPropertyObservable( AvaloniaObject target, - AvaloniaProperty property) + AvaloniaProperty property, + Func? converter = null) { _target = new WeakReference(target); _property = property; + _converter = converter; } public string Description => $"{_target.GetType().Name}.{_property.Name}"; @@ -24,8 +27,17 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - _value = (T)target.GetValue(_property)!; - target.PropertyChanged += PropertyChanged; + if (_converter is { } converter) + { + var unconvertedValue = (TSource)target.GetValue(_property)!; + _value = converter(unconvertedValue); + target.PropertyChanged += PropertyChanged_WithConversion; + } + else + { + _value = (TResult)target.GetValue(_property)!; + target.PropertyChanged += PropertyChanged; + } } } @@ -33,13 +45,20 @@ namespace Avalonia.Reactive { if (_target.TryGetTarget(out var target)) { - target.PropertyChanged -= PropertyChanged; + if (_converter is not null) + { + target.PropertyChanged -= PropertyChanged_WithConversion; + } + else + { + target.PropertyChanged -= PropertyChanged; + } } _value = default; } - protected override void Subscribed(IObserver observer, bool first) + protected override void Subscribed(IObserver observer, bool first) { if (_value.HasValue) observer.OnNext(_value.Value); @@ -49,23 +68,49 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - T newValue; + TResult newValue; - if (e is AvaloniaPropertyChangedEventArgs typed) + if (e is AvaloniaPropertyChangedEventArgs typed) { newValue = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property); } else { - newValue = (T)e.Sender.GetValue(e.Property)!; + newValue = (TResult)e.Sender.GetValue(e.Property)!; } - if (!_value.HasValue || - !EqualityComparer.Default.Equals(newValue, _value.Value)) + PublishNewValue(newValue); + } + } + + private void PropertyChanged_WithConversion(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + TSource newValueRaw; + + if (e is AvaloniaPropertyChangedEventArgs typed) { - _value = newValue; - PublishNext(_value.Value!); + newValueRaw = AvaloniaObjectExtensions.GetValue(e.Sender, typed.Property); } + else + { + newValueRaw = (TSource)e.Sender.GetValue(e.Property)!; + } + + var newValue = _converter!(newValueRaw); + + PublishNewValue(newValue); + } + } + + private void PublishNewValue(TResult newValue) + { + if (!_value.HasValue || + !EqualityComparer.Default.Equals(newValue, _value.Value)) + { + _value = newValue; + PublishNext(_value.Value!); } } } From 7b6cabe6085409ad0d86bbe4bf777663c7c557ca Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 28 Mar 2023 17:24:52 +0200 Subject: [PATCH 17/99] Fix TrayIcons on Gnome --- .../Avalonia.FreeDesktop.csproj | 10 ++++----- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 21 ++++++++++--------- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 13 ++++++++++-- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index da9add1fa4..da6ae9e856 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,20 +5,20 @@ enable - + - + - + - - + + diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 3073ea580c..77f9d31273 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -38,7 +38,7 @@ namespace Avalonia.FreeDesktop private bool _resetQueued; private int _nextId = 1; - public DBusMenuExporterImpl(Connection connection, IntPtr xid) + public DBusMenuExporterImpl(Connection connection, IntPtr xid) : this() { Connection = connection; _xid = (uint)xid.ToInt32(); @@ -47,7 +47,7 @@ namespace Avalonia.FreeDesktop _ = InitializeAsync(); } - public DBusMenuExporterImpl(Connection connection, string path) + public DBusMenuExporterImpl(Connection connection, string path) : this() { Connection = connection; _appMenu = false; @@ -56,6 +56,13 @@ namespace Avalonia.FreeDesktop _ = InitializeAsync(); } + private DBusMenuExporterImpl() + { + BackingProperties.Status = string.Empty; + BackingProperties.TextDirection = string.Empty; + BackingProperties.IconThemePath = Array.Empty(); + } + protected override Connection Connection { get; } public override string Path { get; } @@ -202,15 +209,9 @@ namespace Avalonia.FreeDesktop return id; } - private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - QueueReset(); - } + private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) => QueueReset(); - private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - QueueReset(); - } + private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) => QueueReset(); private static readonly string[] s_allProperties = { "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index fed8b87bc9..43ae48341c 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -220,6 +220,16 @@ namespace Avalonia.FreeDesktop { Connection = connection; BackingProperties.Menu = dbusMenuPath; + BackingProperties.Category = string.Empty; + BackingProperties.Status = string.Empty; + BackingProperties.Id = string.Empty; + BackingProperties.Title = string.Empty; + BackingProperties.IconPixmap = Array.Empty<(int, int, byte[])>(); + BackingProperties.AttentionIconName = string.Empty; + BackingProperties.AttentionIconPixmap = Array.Empty<(int, int, byte[])>(); + BackingProperties.AttentionMovieName = string.Empty; + BackingProperties.OverlayIconName = string.Empty; + BackingProperties.OverlayIconPixmap = Array.Empty<(int, int, byte[])>(); BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty); InvalidateAll(); } @@ -234,7 +244,7 @@ namespace Avalonia.FreeDesktop protected override ValueTask OnActivateAsync(int x, int y) { - Dispatcher.UIThread.Post(() => ActivationDelegate?.Invoke()); + ActivationDelegate?.Invoke(); return new ValueTask(); } @@ -267,7 +277,6 @@ namespace Avalonia.FreeDesktop BackingProperties.Category = "ApplicationStatus"; BackingProperties.Status = text; BackingProperties.Title = text; - BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } } From 6535e299b02e3f832ad1b6d88ec1fddcf631f3fc Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Wed, 29 Mar 2023 09:49:22 +0200 Subject: [PATCH 18/99] DataGrid column resize near edge fix --- src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index b3e106a7bf..3db490d645 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -724,7 +724,7 @@ namespace Avalonia.Controls } //handle entry into reorder mode - if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) + if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null) { var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders); if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold) From f829cdcdcd5e6d3a89e8fe669843a6b3f99fc2bd Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Wed, 29 Mar 2023 10:01:39 +0200 Subject: [PATCH 19/99] cleanup --- src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 3db490d645..5250f80f77 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -440,13 +440,10 @@ namespace Avalonia.Controls } Debug.Assert(OwningGrid.Parent is InputElement); - - double distanceFromLeft = mousePosition.X; - double distanceFromRight = Bounds.Width - distanceFromLeft; - + OnMouseMove_Resize(ref handled, mousePositionHeaders); - OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight); + OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders); SetDragCursor(mousePosition); } @@ -716,7 +713,7 @@ namespace Avalonia.Controls } //TODO DragEvents - private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders, double distanceFromLeft, double distanceFromRight) + private void OnMouseMove_Reorder(ref bool handled, Point mousePosition, Point mousePositionHeaders) { if (handled) { From 13a586077fbf6df0bc5ba7f400922edc8529d023 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 10:24:52 +0200 Subject: [PATCH 20/99] Bind readonly properties one-way. Fixes #10680. --- .../Diagnostics/Views/PropertyValueEditorView.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index 6b52989f0b..2811d895c1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -64,11 +64,12 @@ namespace Avalonia.Diagnostics.Views where TControl : Control, new() { var control = new TControl(); + var bindingMode = Property.IsReadonly ? BindingMode.OneWay : BindingMode.TwoWay; init?.Invoke(control); control.Bind(valueProperty, - new Binding(nameof(Property.Value), BindingMode.TwoWay) + new Binding(nameof(Property.Value), bindingMode) { Source = Property, Converter = converter ?? new ValueConverter(), From 8b881f01b9d2210638065214ae0fbe0055a831d1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 29 Mar 2023 14:38:00 +0600 Subject: [PATCH 21/99] Writable composition properties --- .../CompositionGenerator/Generator.cs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index dfc8b45579..ced2cb369c 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -170,21 +170,26 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (manual.ServerName != null) serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); } - if (animatedServer) server = server.AddMembers( DeclareField(serverPropertyType, fieldName), PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) - .WithExpressionBody(ArrowExpressionClause( - InvocationExpression(IdentifierName("GetAnimatedValue"), - ArgumentList(SeparatedList(new[]{ - Argument(IdentifierName(CompositionPropertyField(prop))), - Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName)) - } - ))))) - .WithSemicolonToken(Semicolon()) - ); + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithExpressionBody( + ArrowExpressionClause( + InvocationExpression(IdentifierName("GetAnimatedValue"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.RefKeyword), + IdentifierName(fieldName)) + } + ))))).WithSemicolonToken(Semicolon()), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithExpressionBody(ArrowExpressionClause( + ParseExpression($"SetAnimatedValue({CompositionPropertyField(prop)}, out {PropertyBackingFieldName(prop)}, value)"))) + .WithSemicolonToken(Semicolon()))); else { server = server @@ -508,9 +513,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); code += $@" if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) "; - if (prop.Animated) - code += $"SetAnimatedValue({CompositionPropertyField(prop)}, out {PropertyBackingFieldName(prop)}, {readValueCode});"; - else code += $"{prop.Name} = {readValueCode};"; + code += $"{prop.Name} = {readValueCode};"; return body.AddStatements(ParseStatement(code)); } From b2a4e85e23b78ec031be0bb6b12fffc1b8cd5528 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 29 Mar 2023 14:20:46 +0200 Subject: [PATCH 22/99] Only bypass WM_CHAR if previous composition is not empty --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 39bdb942e1..84f27f080e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -729,7 +729,7 @@ namespace Avalonia.Win32 { case GCS.GCS_RESULTSTR: { - if(ToInt32(wParam) >= 32) + if(!string.IsNullOrEmpty(previousComposition) && ToInt32(wParam) >= 32) { Imm32InputMethod.Current.Composition = previousComposition; From 7bb6d06ac50f393c667715eb3393a36b282b78e4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 15:24:29 +0200 Subject: [PATCH 23/99] Added failing test for #10626 and #10718. --- .../MenuItemTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 1049ff2678..909b65853c 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Text; using System.Windows.Input; using Avalonia.Collections; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; @@ -348,6 +350,46 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Menu_ItemTemplate_Should_Be_Applied_To_TopLevel_MenuItem_Header() + { + using var app = Application(); + + var items = new[] + { + new MenuViewModel("Foo"), + new MenuViewModel("Bar"), + }; + + var itemTemplate = new FuncDataTemplate((x, _) => + new TextBlock { Text = x.Header }); + + var menu = new Menu + { + ItemTemplate = itemTemplate, + ItemsSource = items, + }; + + var window = new Window { Content = menu }; + window.LayoutManager.ExecuteInitialLayoutPass(); + + var panel = Assert.IsType(menu.Presenter.Panel); + Assert.Equal(2, panel.Children.Count); + + for (var i = 0; i < panel.Children.Count; i++) + { + var menuItem = Assert.IsType(panel.Children[i]); + + Assert.Equal(items[i], menuItem.Header); + + var headerPresenter = Assert.IsType(menuItem.HeaderPresenter); + Assert.Same(itemTemplate, headerPresenter.ContentTemplate); + + var headerControl = Assert.IsType(headerPresenter.Child); + Assert.Equal(items[i].Header, headerControl.Text); + } + } + private IDisposable Application() { var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100)); @@ -401,5 +443,7 @@ namespace Avalonia.Controls.UnitTests public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty); } + + private record MenuViewModel(string Header); } } From b7a249107bb9475b1aba7a942418a6fcd2d0a30e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 15:33:43 +0200 Subject: [PATCH 24/99] Make data templates work again with MenuItem. - `MenuItem` is a `HeaderedSelectingItemsControl` not a `HeaderedItemsControl` so need to separate logic for that case when preparing items - Added `HeaderTemplate` to `HeaderedSelectingItemsControl ` - Tweaked logic for selecting header templates: parent's `ItemTemplate` should be used if set (cross-checked with WPF) - Update menu templates to bind to menu item's `HeaderTemplate` Fixes #10626 Fixes #10718 --- src/Avalonia.Controls/ItemsControl.cs | 12 +++- .../Primitives/HeaderedItemsControl.cs | 16 ++--- .../HeaderedSelectingItemsControl.cs | 63 +++++++++++++++++++ src/Avalonia.Themes.Fluent/Controls/Menu.xaml | 1 + .../Controls/MenuItem.xaml | 2 +- src/Avalonia.Themes.Simple/Controls/Menu.xaml | 3 +- .../Controls/MenuItem.xaml | 2 +- .../MenuItemTests.cs | 1 + 8 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1123f42afa..06f0d54e4c 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -460,13 +460,19 @@ namespace Avalonia.Controls ic.ItemContainerTheme = ict; } - // This condition is separate because HeaderedItemsControl needs to also run the - // ItemsControl preparation. + // These conditions are separate because HeaderedItemsControl and + // HeaderedSelectingItemsControl also need to run the ItemsControl preparation. if (container is HeaderedItemsControl hic) { hic.Header = item; hic.HeaderTemplate = itemTemplate; - hic.PrepareItemContainer(); + hic.PrepareItemContainer(this); + } + else if (container is HeaderedSelectingItemsControl hsic) + { + hsic.Header = item; + hsic.HeaderTemplate = itemTemplate; + hsic.PrepareItemContainer(this); } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 55d2ec7506..273271d2ce 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Primitives public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { private IDisposable? _itemsBinding; - private bool _prepareItemContainerOnAttach; + private ItemsControl? _prepareItemContainerOnAttach; /// /// Defines the property. @@ -69,10 +69,10 @@ namespace Avalonia.Controls.Primitives { base.OnAttachedToLogicalTree(e); - if (_prepareItemContainerOnAttach) + if (_prepareItemContainerOnAttach is not null) { - PrepareItemContainer(); - _prepareItemContainerOnAttach = false; + PrepareItemContainer(_prepareItemContainerOnAttach); + _prepareItemContainerOnAttach = null; } } @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives return false; } - internal void PrepareItemContainer() + internal void PrepareItemContainer(ItemsControl parent) { _itemsBinding?.Dispose(); _itemsBinding = null; @@ -106,18 +106,18 @@ namespace Avalonia.Controls.Primitives if (item is null) { - _prepareItemContainerOnAttach = false; + _prepareItemContainerOnAttach = null; return; } - var headerTemplate = HeaderTemplate; + var headerTemplate = HeaderTemplate ?? parent.ItemTemplate; if (headerTemplate is null) { if (((ILogical)this).IsAttachedToLogicalTree) headerTemplate = this.FindDataTemplate(item); else - _prepareItemContainerOnAttach = true; + _prepareItemContainerOnAttach = parent; } if (headerTemplate is ITreeDataTemplate treeTemplate && diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index 49fc58c8f5..88ca1f1fe1 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -1,5 +1,8 @@ +using System; using Avalonia.Collections; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives @@ -9,12 +12,21 @@ namespace Avalonia.Controls.Primitives /// public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost { + private IDisposable? _itemsBinding; + private ItemsControl? _prepareItemContainerOnAttach; + /// /// Defines the property. /// public static readonly StyledProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + HeaderedItemsControl.HeaderTemplateProperty.AddOwner(); + /// /// Initializes static members of the class. /// @@ -32,6 +44,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(HeaderProperty, value); } } + /// + /// Gets or sets the data template used to display the header content of the control. + /// + public IDataTemplate? HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + /// /// Gets the header presenter from the control's template. /// @@ -50,6 +71,17 @@ namespace Avalonia.Controls.Primitives return RegisterContentPresenter(presenter); } + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + + if (_prepareItemContainerOnAttach is not null) + { + PrepareItemContainer(_prepareItemContainerOnAttach); + _prepareItemContainerOnAttach = null; + } + } + /// /// Called when an is registered with the control. /// @@ -65,6 +97,37 @@ namespace Avalonia.Controls.Primitives return false; } + internal void PrepareItemContainer(ItemsControl parent) + { + _itemsBinding?.Dispose(); + _itemsBinding = null; + + var item = Header; + + if (item is null) + { + _prepareItemContainerOnAttach = null; + return; + } + + var headerTemplate = HeaderTemplate ?? parent.ItemTemplate; + + if (headerTemplate is null) + { + if (((ILogical)this).IsAttachedToLogicalTree) + headerTemplate = this.FindDataTemplate(item); + else + _prepareItemContainerOnAttach = parent; + } + + if (headerTemplate is ITreeDataTemplate treeTemplate && + treeTemplate.Match(item) && + treeTemplate.ItemsSelector(item) is { } itemsBinding) + { + _itemsBinding = BindingOperations.Apply(this, ItemsSourceProperty, itemsBinding, null); + } + } + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) { if (e.OldValue is ILogical oldChild) diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml index c234cfd68e..e6bbbde632 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml @@ -28,6 +28,7 @@ + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding HeaderTemplate}"> diff --git a/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml b/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml index 2f09b9dc40..59ddcdf325 100644 --- a/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml @@ -43,7 +43,7 @@ Margin="{TemplateBinding Padding}" VerticalAlignment="Center" Content="{TemplateBinding Header}" - ContentTemplate="{TemplateBinding ItemTemplate}"> + ContentTemplate="{TemplateBinding HeaderTemplate}"> diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 909b65853c..6fda5209ad 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -381,6 +381,7 @@ namespace Avalonia.Controls.UnitTests var menuItem = Assert.IsType(panel.Children[i]); Assert.Equal(items[i], menuItem.Header); + Assert.Same(itemTemplate, menuItem.HeaderTemplate); var headerPresenter = Assert.IsType(menuItem.HeaderPresenter); Assert.Same(itemTemplate, headerPresenter.ContentTemplate); From 6c852f805f7d61f8a272398c9668a420e6f6c9cf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 19:13:31 +0200 Subject: [PATCH 25/99] Don't allow window zoom when CanResize=false. --- src/Avalonia.Native/WindowImpl.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 5d0e6a2d18..817fe3d080 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -21,6 +21,7 @@ namespace Avalonia.Native private DoubleClickHelper _doubleClickHelper; private readonly ITopLevelNativeMenuExporter _nativeMenuExporter; private readonly AvaloniaNativeTextInputMethod _inputMethod; + private bool _canResize = true; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature) @@ -75,6 +76,7 @@ namespace Avalonia.Native public void CanResize(bool value) { + _canResize = value; _native.SetCanResize(value.AsComBool()); } @@ -137,14 +139,10 @@ namespace Avalonia.Native { if (_doubleClickHelper.IsDoubleClick(e.Timestamp, e.Position)) { - // TOGGLE WINDOW STATE. - if (WindowState == WindowState.Maximized || WindowState == WindowState.FullScreen) + if (_canResize) { - WindowState = WindowState.Normal; - } - else - { - WindowState = WindowState.Maximized; + WindowState = WindowState is WindowState.Maximized or WindowState.FullScreen ? + WindowState.Normal : WindowState.Maximized; } } else From f432d6f0a90a87b0f7e1188f5372c7bca2b6f00d Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 29 Mar 2023 19:42:24 +0200 Subject: [PATCH 26/99] Fix GetTextBounds for combinations of EndOfParargrap and embedded RTL text --- .../Media/TextFormatting/TextLineImpl.cs | 18 +++++++++++-- .../Media/TextFormatting/TextLineTests.cs | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f5812f71ff..3264d5e88a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -701,7 +701,14 @@ namespace Avalonia.Media.TextFormatting if (directionalWidth == 0) { //In case a run only contains a linebreak we don't want to skip it. - if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + if (currentRun is ShapedTextRun shaped) + { + if(currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + { + continue; + } + } + else { continue; } @@ -840,7 +847,14 @@ namespace Avalonia.Media.TextFormatting if (directionalWidth == 0) { //In case a run only contains a linebreak we don't want to skip it. - if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + if (currentRun is ShapedTextRun shaped) + { + if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + { + continue; + } + } + else { continue; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 0ddcb2452d..d605ecbfda 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -994,6 +994,33 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_GetTextBounds_With_EndOfParagraph_RightToLeft() + { + var text = "لوحة المفاتيح العربية"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new SingleBufferTextSource(text, defaultProperties, true); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); + + var textBounds = textLine.GetTextBounds(0, 1); + + Assert.Equal(1, textBounds.Count); + + var firstBounds = textBounds.First(); + + Assert.True(firstBounds.TextRunBounds.Count > 0); + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns; From 312f1250e27a08b01e2a1a594531e560fab5820c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 30 Mar 2023 00:25:18 +0600 Subject: [PATCH 27/99] Don't use AvaloniaObject in IPlatformRenderInterface --- src/Avalonia.Base/CombinedGeometry.cs | 14 +++++--------- src/Avalonia.Base/Media/GeometryGroup.cs | 5 ++++- .../Platform/IPlatformRenderInterface.cs | 4 ++-- .../HeadlessPlatformRenderInterface.cs | 4 ++-- src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs | 7 ++++--- src/Skia/Avalonia.Skia/GeometryGroupImpl.cs | 7 ++++--- src/Skia/Avalonia.Skia/PlatformRenderInterface.cs | 4 ++-- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 4 ++-- .../Media/CombinedGeometryImpl.cs | 13 +++++++------ .../Avalonia.Direct2D1/Media/GeometryGroupImpl.cs | 7 ++++--- .../VisualTree/MockRenderInterface.cs | 4 ++-- tests/Avalonia.Benchmarks/NullRenderingPlatform.cs | 4 ++-- .../MockPlatformRenderInterface.cs | 4 ++-- 13 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Base/CombinedGeometry.cs b/src/Avalonia.Base/CombinedGeometry.cs index 8f080d05c7..4b5866519b 100644 --- a/src/Avalonia.Base/CombinedGeometry.cs +++ b/src/Avalonia.Base/CombinedGeometry.cs @@ -152,19 +152,15 @@ namespace Avalonia.Media var g1 = Geometry1; var g2 = Geometry2; - if (g1 is object && g2 is object) + if (g1?.PlatformImpl != null && g2?.PlatformImpl != null) { var factory = AvaloniaLocator.Current.GetRequiredService(); - return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2); + return factory.CreateCombinedGeometry(GeometryCombineMode, g1.PlatformImpl, g2.PlatformImpl); } - else if (GeometryCombineMode == GeometryCombineMode.Intersect) - return null; - else if (g1 is object) - return g1.PlatformImpl; - else if (g2 is object) - return g2.PlatformImpl; - else + + if (GeometryCombineMode == GeometryCombineMode.Intersect) return null; + return g1?.PlatformImpl ?? g2?.PlatformImpl; } } } diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index 0326e606f4..3e61413919 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -78,7 +78,10 @@ namespace Avalonia.Media { var factory = AvaloniaLocator.Current.GetRequiredService(); - return factory.CreateGeometryGroup(FillRule, _children); + var children = new IGeometryImpl?[_children.Count]; + for (var c = 0; c < _children.Count; c++) + children[c] = _children[c].PlatformImpl; + return factory.CreateGeometryGroup(FillRule, children!); } return null; diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index cfc7fac3ea..81fe2c046f 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -48,7 +48,7 @@ namespace Avalonia.Platform /// The fill rule. /// The geometries to group. /// A combined geometry. - IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); + IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); /// /// Creates a geometry group implementation. @@ -57,7 +57,7 @@ namespace Avalonia.Platform /// The first geometry. /// The second geometry. /// A combined geometry. - IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2); /// /// Created a geometry implementation for the glyph run. diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index f8100d3832..431989134a 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -47,8 +47,8 @@ namespace Avalonia.Headless } public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs index 1a4f467f40..1e9240ec70 100644 --- a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs @@ -1,4 +1,5 @@ using Avalonia.Media; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia @@ -15,10 +16,10 @@ namespace Avalonia.Skia Bounds = (stroke ?? fill)?.TightBounds.ToAvaloniaRect() ?? default; } - public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { - if (g1.PlatformImpl is GeometryImpl i1 - && g2.PlatformImpl is GeometryImpl i2 + if (g1 is GeometryImpl i1 + && g2 is GeometryImpl i2 && TryCreate(combineMode, i1, i2) is { } result) return result; diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs index 01be42bad0..200247095f 100644 --- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia @@ -9,7 +10,7 @@ namespace Avalonia.Skia /// internal class GeometryGroupImpl : GeometryImpl { - public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) + public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) { var fillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd; var count = children.Count; @@ -22,7 +23,7 @@ namespace Avalonia.Skia bool requiresFillPass = false; for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl geo) + if (children[i] is GeometryImpl geo) { if (geo.StrokePath != null) stroke.AddPath(geo.StrokePath); @@ -42,7 +43,7 @@ namespace Avalonia.Skia for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl { FillPath: { } fillPath }) + if (children[i] is GeometryImpl { FillPath: { } fillPath }) fill.AddPath(fillPath); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 9c4b326f14..a9a79ff0c5 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -58,12 +58,12 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { return new GeometryGroupImpl(fillRule, children); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { return CombinedGeometryImpl.ForceCreate(combineMode, g1, g2); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 287db92b4d..826296b055 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -158,8 +158,8 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2); public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => new CombinedGeometryImpl(combineMode, g1, g2); public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin) diff --git a/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs index 5a13c10bbc..b1ce160707 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs @@ -1,3 +1,4 @@ +using Avalonia.Platform; using SharpDX.Direct2D1; using AM = Avalonia.Media; @@ -13,19 +14,19 @@ namespace Avalonia.Direct2D1.Media /// public CombinedGeometryImpl( AM.GeometryCombineMode combineMode, - AM.Geometry geometry1, - AM.Geometry geometry2) + IGeometryImpl geometry1, + IGeometryImpl geometry2) : base(CreateGeometry(combineMode, geometry1, geometry2)) { } private static Geometry CreateGeometry( AM.GeometryCombineMode combineMode, - AM.Geometry geometry1, - AM.Geometry geometry2) + IGeometryImpl geometry1, + IGeometryImpl geometry2) { - var g1 = ((GeometryImpl)geometry1.PlatformImpl).Geometry; - var g2 = ((GeometryImpl)geometry2.PlatformImpl).Geometry; + var g1 = ((GeometryImpl)geometry1).Geometry; + var g2 = ((GeometryImpl)geometry2).Geometry; var dest = new PathGeometry(Direct2D1Platform.Direct2D1Factory); using var sink = dest.Open(); g1.Combine(g2, (CombineMode)combineMode, sink); diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs index 352708bf03..5e49ef6d4e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Platform; using SharpDX.Direct2D1; using AM = Avalonia.Media; @@ -12,19 +13,19 @@ namespace Avalonia.Direct2D1.Media /// /// Initializes a new instance of the class. /// - public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList geometry) + public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList geometry) : base(CreateGeometry(fillRule, geometry)) { } - private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList children) + private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList children) { var count = children.Count; var c = new Geometry[count]; for (var i = 0; i < count; ++i) { - c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry; + c[i] = ((GeometryImpl)children[i]).Geometry; } return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c); diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 481b98a0b2..d494c47a55 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -30,12 +30,12 @@ namespace Avalonia.Base.UnitTests.VisualTree return new MockStreamGeometry(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { throw new NotImplementedException(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index e5cbae4ae7..d40abd9f47 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -32,12 +32,12 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { throw new NotImplementedException(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index df128b8ae3..720755f2b0 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -72,12 +72,12 @@ namespace Avalonia.UnitTests return new MockStreamGeometryImpl(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { return Mock.Of(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { return Mock.Of(); } From 63295e817f23679cfcc3aefbb4320680d21b47ff Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 23:28:11 +0200 Subject: [PATCH 28/99] Fix ControlAutomationPeer.GetName. :facepalm: --- .../Automation/Peers/ControlAutomationPeer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index e8fb6b75ad..d04dfec3e8 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -88,10 +88,10 @@ namespace Avalonia.Automation.Peers if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy) { - return labeledBy.GetName(); + result = labeledBy.GetName(); } - return null; + return result; } protected override AutomationPeer? GetParentCore() From f8a8c4ab1facf4d795ea3b37b0874a6a90866609 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 23:29:20 +0200 Subject: [PATCH 29/99] Run check for disabled maximize button on Windows. And also check with client-side decorations. --- .../Controls/CaptionButtons.xaml | 9 ++- .../WindowTests.cs | 55 ++++++++++++++++++- .../WindowTests_MacOS.cs | 16 ------ 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml index 71ae012289..7ce775e4c2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml @@ -48,7 +48,8 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/VirtualizationDemo/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs deleted file mode 100644 index cea200dcec..0000000000 --- a/samples/VirtualizationDemo/MainWindow.xaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using VirtualizationDemo.ViewModels; - -namespace VirtualizationDemo -{ - public class MainWindow : Window - { - public MainWindow() - { - this.InitializeComponent(); - this.AttachDevTools(); - DataContext = new MainWindowViewModel(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/VirtualizationDemo/Models/Chat.cs b/samples/VirtualizationDemo/Models/Chat.cs new file mode 100644 index 0000000000..e84760135c --- /dev/null +++ b/samples/VirtualizationDemo/Models/Chat.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace VirtualizationDemo.Models; + +public class ChatFile +{ + public ChatMessage[] Chat { get; set; } + + public static ChatFile Load(string path) + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + using var s = File.OpenRead(path); + return JsonSerializer.Deserialize(s, options); + } +} + +public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp); diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs index febda46450..87212b6daa 100644 --- a/samples/VirtualizationDemo/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -1,15 +1,14 @@ using Avalonia; -namespace VirtualizationDemo +namespace VirtualizationDemo; + +class Program { - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace(); + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } + public static int Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } diff --git a/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs new file mode 100644 index 0000000000..5ade0ec9ec --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs @@ -0,0 +1,16 @@ +using System.Collections.ObjectModel; +using System.IO; +using VirtualizationDemo.Models; + +namespace VirtualizationDemo.ViewModels; + +public class ChatPageViewModel +{ + public ChatPageViewModel() + { + var chat = ChatFile.Load(Path.Combine("Assets", "chat.json")); + Messages = new(chat.Chat); + } + + public ObservableCollection Messages { get; } +} diff --git a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs deleted file mode 100644 index 9ba505ffe5..0000000000 --- a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using MiniMvvm; - -namespace VirtualizationDemo.ViewModels -{ - internal class ItemViewModel : ViewModelBase - { - private string _prefix; - private int _index; - private double _height = double.NaN; - - public ItemViewModel(int index, string prefix = "Item") - { - _prefix = prefix; - _index = index; - } - - public string Header => $"{_prefix} {_index}"; - - public double Height - { - get => _height; - set => this.RaiseAndSetIfChanged(ref _height, value); - } - } -} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 96dbbc1a83..6d3590307c 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -1,160 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Layout; -using Avalonia.Controls.Selection; -using MiniMvvm; +using MiniMvvm; -namespace VirtualizationDemo.ViewModels -{ - internal class MainWindowViewModel : ViewModelBase - { - private int _itemCount = 200; - private string _newItemString = "New Item"; - private int _newItemIndex; - private AvaloniaList _items; - private string _prefix = "Item"; - private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto; - private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto; - private Orientation _orientation = Orientation.Vertical; - - public MainWindowViewModel() - { - this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); - RecreateCommand = MiniCommand.Create(() => Recreate()); - - AddItemCommand = MiniCommand.Create(() => AddItem()); - - RemoveItemCommand = MiniCommand.Create(() => Remove()); - - SelectFirstCommand = MiniCommand.Create(() => SelectItem(0)); - - SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1)); - } - - public string NewItemString - { - get { return _newItemString; } - set { this.RaiseAndSetIfChanged(ref _newItemString, value); } - } - - public int ItemCount - { - get { return _itemCount; } - set { this.RaiseAndSetIfChanged(ref _itemCount, value); } - } - - public SelectionModel Selection { get; } = new SelectionModel(); - - public AvaloniaList Items - { - get { return _items; } - private set { this.RaiseAndSetIfChanged(ref _items, value); } - } - - public Orientation Orientation - { - get { return _orientation; } - set { this.RaiseAndSetIfChanged(ref _orientation, value); } - } - - public IEnumerable Orientations => - Enum.GetValues(typeof(Orientation)).Cast(); - - public ScrollBarVisibility HorizontalScrollBarVisibility - { - get { return _horizontalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); } - } +namespace VirtualizationDemo.ViewModels; - public ScrollBarVisibility VerticalScrollBarVisibility - { - get { return _verticalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); } - } - - public IEnumerable ScrollBarVisibilities => - Enum.GetValues(typeof(ScrollBarVisibility)).Cast(); - - public MiniCommand AddItemCommand { get; private set; } - public MiniCommand RecreateCommand { get; private set; } - public MiniCommand RemoveItemCommand { get; private set; } - public MiniCommand SelectFirstCommand { get; private set; } - public MiniCommand SelectLastCommand { get; private set; } - - public void RandomizeSize() - { - var random = new Random(); - - foreach (var i in Items) - { - i.Height = random.Next(240) + 10; - } - } - - public void ResetSize() - { - foreach (var i in Items) - { - i.Height = double.NaN; - } - } - - private void ResizeItems(int count) - { - if (Items == null) - { - var items = Enumerable.Range(0, count) - .Select(x => new ItemViewModel(x)); - Items = new AvaloniaList(items); - } - else if (count > Items.Count) - { - var items = Enumerable.Range(Items.Count, count - Items.Count) - .Select(x => new ItemViewModel(x)); - Items.AddRange(items); - } - else if (count < Items.Count) - { - Items.RemoveRange(count, Items.Count - count); - } - } - - private void AddItem() - { - var index = Items.Count; - - if (Selection.SelectedItems.Count > 0) - { - index = Selection.SelectedIndex; - } - - Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); - } - - private void Remove() - { - if (Selection.SelectedItems.Count > 0) - { - Items.RemoveAll(Selection.SelectedItems.ToList()); - } - } - - private void Recreate() - { - _prefix = _prefix == "Item" ? "Recreated" : "Item"; - var items = Enumerable.Range(0, _itemCount) - .Select(x => new ItemViewModel(x, _prefix)); - Items = new AvaloniaList(items); - } - - private void SelectItem(int index) - { - Selection.SelectedIndex = index; - } - } +internal class MainWindowViewModel : ViewModelBase +{ + public ChatPageViewModel Chat { get; } = new(); } diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml b/samples/VirtualizationDemo/Views/ChatPageView.axaml new file mode 100644 index 0000000000..fc182f15ae --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs new file mode 100644 index 0000000000..b5c90db69c --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace VirtualizationDemo.Views; + +public partial class ChatPageView : UserControl +{ + public ChatPageView() + { + InitializeComponent(); + } +} diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index 81b30c6cbe..3ac7aab589 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -1,19 +1,24 @@  - Exe + WinExe net6.0 + true + + + - - + + + + + PreserveNewest + - - - - - + + From 35d70577f48d352c9dec7ce3e9389ff2ac4bc7d4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Apr 2023 10:45:45 +0200 Subject: [PATCH 52/99] Added repro for #10367 to virtualization demo. --- samples/VirtualizationDemo/MainWindow.axaml | 3 +++ .../ViewModels/ExpanderItemViewModel.cs | 21 +++++++++++++++++++ .../ViewModels/ExpanderPageViewModel.cs | 17 +++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 1 + .../Views/ExpanderPageView.axaml | 18 ++++++++++++++++ .../Views/ExpanderPageView.axaml.cs | 13 ++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs create mode 100644 samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs create mode 100644 samples/VirtualizationDemo/Views/ExpanderPageView.axaml create mode 100644 samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs diff --git a/samples/VirtualizationDemo/MainWindow.axaml b/samples/VirtualizationDemo/MainWindow.axaml index 94e7c96a76..e064e6ab32 100644 --- a/samples/VirtualizationDemo/MainWindow.axaml +++ b/samples/VirtualizationDemo/MainWindow.axaml @@ -12,5 +12,8 @@ + + + diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs new file mode 100644 index 0000000000..a17fc2d303 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs @@ -0,0 +1,21 @@ +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class ExpanderItemViewModel : ViewModelBase +{ + private string? _header; + private bool _isExpanded; + + public string? Header + { + get => _header; + set => RaiseAndSetIfChanged(ref _header, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); + } +} diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs new file mode 100644 index 0000000000..f2807a803b --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; +using System.Linq; + +namespace VirtualizationDemo.ViewModels; + +internal class ExpanderPageViewModel +{ + public ExpanderPageViewModel() + { + Items = new(Enumerable.Range(0, 100).Select(x => new ExpanderItemViewModel + { + Header = $"Item {x}", + })); + } + + public ObservableCollection Items { get; set; } +} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 6d3590307c..6432503595 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -5,4 +5,5 @@ namespace VirtualizationDemo.ViewModels; internal class MainWindowViewModel : ViewModelBase { public ChatPageViewModel Chat { get; } = new(); + public ExpanderPageViewModel Expanders { get; } = new(); } diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml new file mode 100644 index 0000000000..972d885229 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs new file mode 100644 index 0000000000..df3689cf24 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VirtualizationDemo.Views; + +public partial class ExpanderPageView : UserControl +{ + public ExpanderPageView() + { + InitializeComponent(); + } +} \ No newline at end of file From 345fb7e1d6b8aaadf01b3f7f2f722dbff433a261 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 16:23:57 +0200 Subject: [PATCH 53/99] Register anchor candidate in panel. We need to register controls as anchor candidates in the panel instead of in `ItemsControl` because the candidate needs to be registered after arrange. Consider this scenario: - In Measure: - Container is realized and registered as an anchor candidate - Container is unrealized and unregistered - Container is recycled and registered, but it is still placed in the position from before it was recycled - In Arrange: - The container is placed in its new position - The `ScrollContentPresenter` sees it's been moved and adjusts the viewport to anchor it This is obviously incorrect, but was what was happening when `ItemsControl` was responsible for registering anchor candidates. Instead of tracking which containers have already been registered, change the list of anchor candidates in `ScrollContentPresenter` to a `HashSet` so we can just register it multiple times. --- src/Avalonia.Controls/ItemsControl.cs | 4 ---- .../Presenters/ScrollContentPresenter.cs | 5 +++-- src/Avalonia.Controls/VirtualizingStackPanel.cs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 54ffba462f..4d71cc8d4f 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -94,7 +94,6 @@ namespace Avalonia.Controls private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; private IDataTemplate? _displayMemberItemTemplate; - private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; /// @@ -457,7 +456,6 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); } @@ -629,7 +627,6 @@ namespace Avalonia.Controls internal void ItemContainerPrepared(Control container, object? item, int index) { _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); - _scrollViewer?.RegisterAnchorCandidate(container); ContainerPrepared?.Invoke(this, new(container, index)); } @@ -642,7 +639,6 @@ namespace Avalonia.Controls internal void ClearItemContainer(Control container) { - _scrollViewer?.UnregisterAnchorCandidate(container); ClearContainerForItemOverride(container); ContainerClearing?.Invoke(this, new(container)); } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index bc86558ab3..261b7d3533 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Presenters private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; private Dictionary? _scrollGestureSnapPoints; - private List? _anchorCandidates; + private HashSet? _anchorCandidates; private Control? _anchorElement; private Rect _anchorElementBounds; private bool _isAnchorElementDirty; @@ -310,7 +310,7 @@ namespace Avalonia.Controls.Presenters "An anchor control must be a visual descendent of the ScrollContentPresenter."); } - _anchorCandidates ??= new List(); + _anchorCandidates ??= new(); _anchorCandidates.Add(element); _isAnchorElementDirty = true; } @@ -410,6 +410,7 @@ namespace Avalonia.Controls.Presenters try { _arranging = true; + Offset = newOffset; } finally diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 5a766b9cd3..ad2ae9278c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -67,6 +67,7 @@ namespace Avalonia.Controls private double _lastEstimatedElementSizeU = 25; private RealizedStackElements? _measureElements; private RealizedStackElements? _realizedElements; + private ScrollViewer? _scrollViewer; private Rect _viewport = s_invalidViewport; private Stack? _recyclePool; private Control? _unrealizedFocusedElement; @@ -203,6 +204,7 @@ namespace Avalonia.Controls new Rect(u, 0, sizeU, finalSize.Height) : new Rect(0, u, finalSize.Width, sizeU); e.Arrange(rect); + _scrollViewer?.RegisterAnchorCandidate(e); u += orientation == Orientation.Horizontal ? rect.Width : rect.Height; } } @@ -217,6 +219,18 @@ namespace Avalonia.Controls } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _scrollViewer = this.FindAncestorOfType(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _scrollViewer = null; + } + protected override void OnItemsChanged(IReadOnlyList items, NotifyCollectionChangedEventArgs e) { InvalidateMeasure(); @@ -598,6 +612,8 @@ namespace Avalonia.Controls { Debug.Assert(ItemContainerGenerator is not null); + _scrollViewer?.UnregisterAnchorCandidate(element); + if (element.IsSet(ItemIsOwnContainerProperty)) { element.IsVisible = false; From 54449850cc39d9c04c13412fc3fb9f27c35c03aa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 22:56:13 +0200 Subject: [PATCH 54/99] Make thumb drag delta relative to parent. Prevents the viewport jumping around when scrolling a virtualized list with differing sizes. Instead the thumb jumps around. --- src/Avalonia.Controls/Primitives/Thumb.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index cb3195cf52..c205830bc2 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -80,20 +81,22 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { + var point = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, - Vector = e.GetPosition(this) - _lastPoint.Value, + Vector = point - _lastPoint.Value, }; RaiseEvent(ev); + _lastPoint = point; } } protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(this); + _lastPoint = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { @@ -116,7 +119,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition(this), + Vector = (Vector)e.GetPosition(this.GetVisualParent()), }; RaiseEvent(ev); From bc32c061e8fe3229cd60ff4a1767e83389dd0667 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Apr 2023 10:06:50 +0200 Subject: [PATCH 55/99] Added tests to detect scroll jumps. --- .../VirtualizingStackPanel.cs | 10 ++ .../VirtualizingStackPanelTests.cs | 146 +++++++++++++++++- 2 files changed, 149 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index ad2ae9278c..7cca0986ad 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -130,6 +130,16 @@ namespace Avalonia.Controls set { SetValue(AreVerticalSnapPointsRegularProperty, value); } } + /// + /// Gets the index of the first realized element, or -1 if no elements are realized. + /// + public int FirstRealizedIndex => _realizedElements?.FirstIndex ?? -1; + + /// + /// Gets the index of the last realized element, or -1 if no elements are realized. + /// + public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1; + protected override Size MeasureOverride(Size availableSize) { if (!IsEffectivelyVisible) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 76fc31c50d..2cdd4eaf95 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10}); - var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false); + var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null); Assert.Equal(1000, scroll.Extent.Height); @@ -252,7 +252,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var items = new ObservableCollection