From 746b53b388c007b23846d493ccc27cedd871f070 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 31 Jan 2023 20:26:09 +0000 Subject: [PATCH 01/71] add support for getting safe insets --- samples/ControlCatalog.Browser/app.css | 9 +- .../Avalonia.Android/AvaloniaMainActivity.cs | 12 + src/Android/Avalonia.Android/AvaloniaView.cs | 2 + .../Platform/AndroidInsetsManager.cs | 215 ++++++++++++++++++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 40 ++-- .../Platform/IInsetsManager.cs | 34 +++ src/Avalonia.Controls/TopLevel.cs | 5 +- .../Avalonia.Browser/BrowserInsetsManager.cs | 43 ++++ .../Avalonia.Browser/BrowserTopLevelImpl.cs | 11 + .../Avalonia.Browser/Interop/DomHelper.cs | 9 + .../webapp/modules/avalonia/dom.ts | 22 ++ 11 files changed, 382 insertions(+), 20 deletions(-) create mode 100644 src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs create mode 100644 src/Avalonia.Controls/Platform/IInsetsManager.cs create mode 100644 src/Browser/Avalonia.Browser/BrowserInsetsManager.cs diff --git a/samples/ControlCatalog.Browser/app.css b/samples/ControlCatalog.Browser/app.css index 27680f6e1a..0e6ab12461 100644 --- a/samples/ControlCatalog.Browser/app.css +++ b/samples/ControlCatalog.Browser/app.css @@ -1,4 +1,11 @@ -#out { +:root { + --sat: env(safe-area-inset-top); + --sar: env(safe-area-inset-right); + --sab: env(safe-area-inset-bottom); + --sal: env(safe-area-inset-left); +} + +#out { height: 100vh; width: 100vw } diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index 247008c503..5b22d2c270 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Android.App; using Android.Content; using Android.Content.PM; @@ -55,6 +56,17 @@ namespace Avalonia.Android } } + protected override void OnResume() + { + base.OnResume(); + + // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible. + if (Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + Window.Attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; + } + } + public event EventHandler BackRequested; public override void OnBackPressed() diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 2a345a857c..50de0b49de 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -67,6 +67,8 @@ namespace Avalonia.Android } _root.Renderer.Start(); + + (_view._insetsManager as AndroidInsetsManager)?.ApplyStatusBarState(); } else { diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs new file mode 100644 index 0000000000..ea8e0c0490 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using Android.OS; +using Android.Views; +using AndroidX.Core.View; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Controls.Platform; +using static Avalonia.Controls.Platform.IInsetsManager; + +namespace Avalonia.Android.Platform +{ + internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener + { + private readonly AvaloniaMainActivity _activity; + private readonly TopLevelImpl _topLevel; + private readonly InsetsAnimationCallback _callback; + private bool _displayEdgeToEdge; + private bool _usesLegacyLayouts; + private bool? _systemUiVisibility; + private SystemBarTheme? _statusBarTheme; + private bool? _isDefaultSystemBarLightTheme; + + public event EventHandler SafeAreaChanged; + + public bool DisplayEdgeToEdge + { + get => _displayEdgeToEdge; + set + { + _displayEdgeToEdge = value; + + if(Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + _activity.Window.Attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default; + } + + WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value); + } + } + + public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel) + { + _activity = activity; + _topLevel = topLevel; + _callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop); + + _callback.InsetsManager = this; + + ViewCompat.SetOnApplyWindowInsetsListener(_activity.Window.DecorView, this); + + ViewCompat.SetWindowInsetsAnimationCallback(_activity.Window.DecorView, _callback); + + if(Build.VERSION.SdkInt < BuildVersionCodes.R) + { + _usesLegacyLayouts = true; + _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this); + } + } + + public Thickness GetSafeAreaPadding() + { + var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); + + if (insets != null) + { + var renderScaling = _topLevel.RenderScaling; + + var inset = insets.GetInsets((DisplayEdgeToEdge ? WindowInsetsCompat.Type.SystemBars() | WindowInsetsCompat.Type.DisplayCutout() : 0 ) | WindowInsetsCompat.Type.Ime()); + var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); + var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + + return new Thickness(inset.Left / renderScaling, + inset.Top / renderScaling, + inset.Right / renderScaling, + (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom) / renderScaling); + } + + return default; + } + + public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) + { + NotifySafeAreaChanged(GetSafeAreaPadding()); + return insets; + } + + private void NotifySafeAreaChanged(Thickness safeAreaPadding) + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding)); + } + + public void OnGlobalLayout() + { + NotifySafeAreaChanged(GetSafeAreaPadding()); + } + + public SystemBarTheme? SystemBarTheme + { + get + { + try + { + var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); + + return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; + } + catch (Exception _) + { + return Controls.Platform.SystemBarTheme.Light; + } + } + set + { + _statusBarTheme = value; + + if (!_topLevel.View.IsShown) + { + return; + } + + var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); + + if (_isDefaultSystemBarLightTheme == null) + { + _isDefaultSystemBarLightTheme = compat.AppearanceLightStatusBars; + } + + if (value == null && _isDefaultSystemBarLightTheme != null) + { + value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; + } + + compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light; + compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light; + } + } + + public bool? IsSystemBarVisible + { + get + { + var compat = ViewCompat.GetRootWindowInsets(_topLevel.View); + + return compat?.IsVisible(WindowInsetsCompat.Type.SystemBars()); + } + set + { + _systemUiVisibility = value; + + if (!_topLevel.View.IsShown) + { + return; + } + + var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View); + + if (value == null || value.Value) + { + compat?.Show(WindowInsetsCompat.Type.SystemBars()); + } + else + { + compat?.Hide(WindowInsetsCompat.Type.SystemBars()); + + if (compat != null) + { + compat.SystemBarsBehavior = WindowInsetsControllerCompat.BehaviorShowTransientBarsBySwipe; + } + } + } + } + + internal void ApplyStatusBarState() + { + IsSystemBarVisible = _systemUiVisibility; + SystemBarTheme = _statusBarTheme; + } + + private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback + { + public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode) + { + } + + public AndroidInsetsManager InsetsManager { get; set; } + + public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList runningAnimations) + { + foreach (var anim in runningAnimations) + { + if ((anim.TypeMask & WindowInsetsCompat.Type.Ime()) != 0) + { + var renderScaling = InsetsManager._topLevel.RenderScaling; + + var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.SystemBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); + var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); + var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + + + var bottomPadding = (imeInset.Bottom > 0 && !InsetsManager.DisplayEdgeToEdge ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom); + bottomPadding = (int)(bottomPadding * anim.InterpolatedFraction); + + var padding = new Thickness(inset.Left / renderScaling, + inset.Top / renderScaling, + inset.Right / renderScaling, + bottomPadding / renderScaling); + InsetsManager?.NotifySafeAreaChanged(padding); + break; + } + } + return insets; + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 693a26f3bd..3e2cf12801 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using Android.App; using Android.Content; using Android.Graphics; -using Android.OS; using Android.Runtime; -using Android.Text; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.Specific; @@ -28,6 +26,7 @@ using Math = System.Math; using AndroidRect = Android.Graphics.Rect; using Window = Android.Views.Window; using Android.Graphics.Drawables; +using Android.OS; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -42,6 +41,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; + private readonly IInsetsManager _insetsManager; private ViewImpl _view; public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) @@ -58,6 +58,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); + if (avaloniaView.Context is AvaloniaMainActivity mainActivity) + { + _insetsManager = new AndroidInsetsManager(mainActivity, this); + } + _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); @@ -69,21 +74,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IInputRoot InputRoot { get; private set; } - public virtual Size ClientSize - { - get - { - AndroidRect rect = new AndroidRect(); - AndroidRect intersection = new AndroidRect(); - - _view.GetWindowVisibleDisplayFrame(intersection); - _view.GetGlobalVisibleRect(rect); - - intersection.Intersect(rect); - - return new PixelSize(intersection.Right - intersection.Left, intersection.Bottom - intersection.Top).ToSize(RenderScaling); - } - } + public virtual Size ClientSize => _view.Size.ToSize(RenderScaling); public Size? FrameSize => null; @@ -284,7 +275,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { - // TODO adjust status bar depending on full screen mode. + if(_insetsManager != null) + { + _insetsManager.SystemBarTheme = themeVariant switch + { + PlatformThemeVariant.Light => SystemBarTheme.Light, + PlatformThemeVariant.Dark => SystemBarTheme.Dark, + _ => null, + }; + } } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); @@ -402,6 +401,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs new file mode 100644 index 0000000000..15df684a0e --- /dev/null +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -0,0 +1,34 @@ +using System; + +namespace Avalonia.Controls.Platform +{ + [Avalonia.Metadata.Unstable] + public interface IInsetsManager + { + SystemBarTheme? SystemBarTheme { get; set; } + + bool? IsSystemBarVisible { get; set; } + + event EventHandler SafeAreaChanged; + + bool DisplayEdgeToEdge { get; set; } + + Thickness GetSafeAreaPadding(); + + public class SafeAreaChangedArgs : EventArgs + { + public SafeAreaChangedArgs(Thickness safeArePadding) + { + SafeAreaPadding = safeArePadding; + } + + public Thickness SafeAreaPadding { get; } + } + } + + public enum SystemBarTheme + { + Light, + Dark + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index cb2b8cfd1c..83655db7ae 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -14,6 +14,7 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Platform.Storage; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; @@ -393,7 +394,9 @@ namespace Avalonia.Controls ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? PlatformImpl?.TryGetFeature() ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); - + + public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature(); + /// Point IRenderRoot.PointToClient(PixelPoint p) { diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs new file mode 100644 index 0000000000..d751152a53 --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Browser.Interop; +using Avalonia.Controls.Platform; +using static Avalonia.Controls.Platform.IInsetsManager; + +namespace Avalonia.Browser +{ + internal class BrowserInsetsManager : IInsetsManager + { + public SystemBarTheme? SystemBarTheme { get; set; } + public bool? IsSystemBarVisible + { + get + { + return DomHelper.IsFullscreen(); + } + set + { + DomHelper.SetFullscreen(value != null ? !value.Value : false); + } + } + + public bool DisplayEdgeToEdge { get; set; } + + public event EventHandler? SafeAreaChanged; + + public Thickness GetSafeAreaPadding() + { + var padding = DomHelper.GetSafeAreaPadding(); + + return new Thickness(padding[0], padding[1], padding[2], padding[3]); + } + + public void NotifySafeAreaPaddingChanged() + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(GetSafeAreaPadding())); + } + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index f1cd441f45..c00fe690b0 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -31,6 +31,7 @@ namespace Avalonia.Browser private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; + private readonly IInsetsManager? _insetsManager; public BrowserTopLevelImpl(AvaloniaView avaloniaView) { @@ -40,9 +41,12 @@ namespace Avalonia.Browser AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); _touchDevice = new TouchDevice(); _penDevice = new PenDevice(); + + _insetsManager = new BrowserInsetsManager(); _nativeControlHost = _avaloniaView.GetNativeControlHostImpl(); _storageProvider = new BrowserStorageProvider(); _systemNavigationManager = new BrowserSystemNavigationManagerImpl(); + } public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; @@ -69,6 +73,8 @@ namespace Avalonia.Browser } Resized?.Invoke(newSize, PlatformResizeReason.User); + + (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged(); } } @@ -262,6 +268,11 @@ namespace Avalonia.Browser return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index d1133b8916..c1811bb117 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -11,6 +11,15 @@ internal static partial class DomHelper [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)] public static partial JSObject CreateAvaloniaHost(JSObject element); + [JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)] + public static partial bool IsFullscreen(); + + [JSImport("AvaloniaDOM.setFullscreen", AvaloniaModule.MainModuleName)] + public static partial JSObject SetFullscreen(bool isFullscreen); + + [JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)] + public static partial byte[] GetSafeAreaPadding(); + [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] public static partial void AddCssClass(JSObject element, string className); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index b99f8e7907..d9790f69e9 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -84,4 +84,26 @@ export class AvaloniaDOM { inputElement }; } + + public static isFullscreen(): boolean { + return document.fullscreenElement != null; + } + + public static async setFullscreen(isFullscreen: boolean) { + if (isFullscreen) { + const doc = document.documentElement; + await doc.requestFullscreen(); + } else { + await document.exitFullscreen(); + } + } + + public static getSafeAreaPadding(): number[] { + const top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sat")); + const bottom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sab")); + const left = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sal")); + const right = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sar")); + + return [left, top, bottom, right]; + } } From 63b931253a92881198cc189f22124b2e698a89bb Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 31 Jan 2023 20:45:47 +0000 Subject: [PATCH 02/71] add comment docs --- .../Platform/IInsetsManager.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs index 15df684a0e..028e434104 100644 --- a/src/Avalonia.Controls/Platform/IInsetsManager.cs +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -1,21 +1,40 @@ using System; +using Avalonia.Interactivity; namespace Avalonia.Controls.Platform { [Avalonia.Metadata.Unstable] public interface IInsetsManager { + /// + /// Gets or sets the theme for the system bars, if supported. + /// SystemBarTheme? SystemBarTheme { get; set; } + /// + /// Gets or sets whether the system bars are visible. + /// bool? IsSystemBarVisible { get; set; } + /// + /// Occurs when safe area for the current window changes. + /// + event EventHandler SafeAreaChanged; + + /// + /// Gets or sets whether the window draws edge to edge. behind any visibile system bars. + /// bool DisplayEdgeToEdge { get; set; } + /// + /// Gets the current safe area padding. + /// + /// Thickness GetSafeAreaPadding(); - public class SafeAreaChangedArgs : EventArgs + public class SafeAreaChangedArgs : RoutedEventArgs { public SafeAreaChangedArgs(Thickness safeArePadding) { @@ -28,7 +47,14 @@ namespace Avalonia.Controls.Platform public enum SystemBarTheme { + /// + /// Light system bar theme, with light background and a dark foreground + /// Light, + + /// + /// Bark system bar theme, with dark background and a light foreground + /// Dark } } From 5389d88b0d82e756b10a9b958fb880389a11716a Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 1 Feb 2023 08:55:16 +0000 Subject: [PATCH 03/71] fix android build --- src/Android/Avalonia.Android/AvaloniaView.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 50de0b49de..27481a598e 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -8,6 +8,7 @@ using Avalonia.Android.Platform; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; using Avalonia.Controls.Embedding; +using Avalonia.Controls.Platform; using Avalonia.Platform; using Avalonia.Rendering; @@ -68,7 +69,10 @@ namespace Avalonia.Android _root.Renderer.Start(); - (_view._insetsManager as AndroidInsetsManager)?.ApplyStatusBarState(); + if (_view.TryGetFeature(out var insetsManager) == true) + { + (insetsManager as AndroidInsetsManager)?.ApplyStatusBarState(); + } } else { From 3776151dd7f79662b4881be40208b14240c02eaa Mon Sep 17 00:00:00 2001 From: pr8x Date: Thu, 16 Feb 2023 16:10:22 +0100 Subject: [PATCH 04/71] Dedicated propert editors for DevTools --- .../Diagnostics/Controls/BrushEditor.cs | 90 +++++ .../Controls/PropertyValueEditor.cs | 325 ++++++++++++++++++ .../ViewModels/AvaloniaPropertyViewModel.cs | 8 +- .../ViewModels/ClrPropertyViewModel.cs | 8 +- .../ViewModels/PropertyViewModel.cs | 67 +--- .../Diagnostics/Views/ControlDetailsView.xaml | 9 +- .../Diagnostics/Views/MainWindow.xaml | 1 + 7 files changed, 437 insertions(+), 71 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs new file mode 100644 index 0000000000..b7579ed31b --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.cs @@ -0,0 +1,90 @@ +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Diagnostics.Controls +{ + internal sealed class BrushEditor : Control + { + /// + /// Defines the property. + /// + public static readonly DirectProperty BrushProperty = + AvaloniaProperty.RegisterDirect( + nameof(Brush), o => o.Brush, (o, v) => o.Brush = v); + + private IBrush? _brush; + + public IBrush? Brush + { + get => _brush; + set => SetAndRaise(BrushProperty, ref _brush, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == BrushProperty) + { + switch (Brush) + { + case ISolidColorBrush scb: + { + var colorView = new ColorView { Color = scb.Color }; + + colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor); + + FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = colorView }); + ToolTip.SetTip(this, $"{scb.Color} ({Brush.GetType().Name})"); + + break; + } + + default: + + FlyoutBase.SetAttachedFlyout(this, null); + ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)"); + + break; + } + + InvalidateVisual(); + } + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + FlyoutBase.ShowAttachedFlyout(this); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + if (Brush != null) + { + context.FillRectangle(Brush, Bounds); + } + else + { + context.FillRectangle(Brushes.Black, Bounds); + + var ft = new FormattedText("(null)", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + Typeface.Default, + 10, + Brushes.White); + + context.DrawText(ft, + new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2)); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs new file mode 100644 index 0000000000..74a54dd702 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs @@ -0,0 +1,325 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Reactive; + +namespace Avalonia.Diagnostics.Controls +{ + internal class PropertyValueEditor : Decorator + { + /// + /// Defines the property. + /// + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(Value), o => o.Value, (o, v) => o.Value = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ValueTypeProperty = + AvaloniaProperty.RegisterDirect( + nameof(ValueType), o => o.ValueType, (o, v) => o.ValueType = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsReadonlyProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsReadonly), o => o.IsReadonly, (o, v) => o.IsReadonly = v); + + private readonly CompositeDisposable _cleanup = new(); + + private bool _isReadonly; + private bool _needsUpdate; + private object? _value; + private Type? _valueType; + + public bool IsReadonly + { + get => _isReadonly; + set => SetAndRaise(IsReadonlyProperty, ref _isReadonly, value); + } + + public object? Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + + public Type? ValueType + { + get => _valueType; + set => SetAndRaise(ValueTypeProperty, ref _valueType, value); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _cleanup.Clear(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ValueTypeProperty) + { + _cleanup.Clear(); + + _needsUpdate = true; + } + + if (change.Property == ValueProperty && _needsUpdate) + { + _needsUpdate = false; + + Child = UpdateControl(); + } + } + + //Unfortunately we cannot use TwoWay bindings as they update the source with the target value + //This causes the source property value to be overwritten. Ideally we there would be some kind of + //"InitialBindingDirection" or something to control whether the first value is from source or target. + private static void TwoWayBindingFromSource( + AvaloniaObject source, + AvaloniaProperty sourceProperty, + AvaloniaObject target, + AvaloniaProperty targetProperty, + IValueConverter? converter, + Type targetType, + CompositeDisposable disposable) + { + bool isUpdating = false; + + source + .GetObservable(sourceProperty) + .Subscribe(value => + { + if (isUpdating) return; + + try + { + isUpdating = true; + + target[targetProperty] = converter != null ? + converter.Convert(value, typeof(object), null, CultureInfo.CurrentCulture) : + value; + } + finally + { + isUpdating = false; + } + }) + .DisposeWith(disposable); + + target + .GetObservable(targetProperty) + .Skip(1) + .Subscribe(value => + { + if (isUpdating) return; + + try + { + isUpdating = true; + + source[sourceProperty] = converter != null ? + converter.ConvertBack(value, targetType, null, CultureInfo.CurrentCulture) : + value; + } + finally + { + isUpdating = false; + } + }) + .DisposeWith(disposable); + } + + private Control? UpdateControl() + { + if (ValueType is null) return null; + + TControl CreateControl(AvaloniaProperty valueProperty, + IValueConverter? converter = null, + Action? init = null) + where TControl : Control, new() + { + var control = new TControl(); + + init?.Invoke(control); + + TwoWayBindingFromSource( + this, + ValueProperty, + control, + valueProperty, + converter, + ValueType, + _cleanup); + + control.Bind( + IsEnabledProperty, + new Binding(nameof(IsReadonly)) { Source = this, Converter = BoolConverters.Not }) + .DisposeWith(_cleanup); + + return control; + } + + bool isObjectType = ValueType == typeof(object); + + if (ValueType == typeof(bool)) + return CreateControl(ToggleButton.IsCheckedProperty); + + //TODO: Infinity, NaN not working with NumericUpDown + //if (ValueType.IsPrimitive) + // return CreateControl(NumericUpDown.ValueProperty, new ValueToDecimalConverter()); + + if (ValueType == typeof(Color)) + return CreateControl(ColorView.ColorProperty); + + if (!isObjectType && ValueType.IsAssignableFrom(typeof(IBrush))) + return CreateControl(BrushEditor.BrushProperty); + + if (!isObjectType && ValueType.IsAssignableFrom(typeof(IImage))) + return CreateControl(Image.SourceProperty, init: img => + { + img.Stretch = Stretch.Uniform; + img.HorizontalAlignment = HorizontalAlignment.Stretch; + + img.PointerPressed += (_, _) => + new Window + { + Content = new Image + { + Source = img.Source + } + }.Show(); + }); + + if (ValueType.IsEnum) + return CreateControl( + SelectingItemsControl.SelectedItemProperty, init: c => + { + c.Items = Enum.GetValues(ValueType); + }); + + var tb = CreateControl( + TextBox.TextProperty, + new TextToValueConverter(), + t => + { + t.Watermark = "(null)"; + }); + + tb.IsEnabled &= !isObjectType && + StringConversionHelper.CanConvertFromString(ValueType); + + return tb; + } + + private static class StringConversionHelper + { + private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; + private static readonly Type[] StringParameter = { typeof(string) }; + + private static readonly Type[] + StringFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; + + public static bool CanConvertFromString(Type type) + { + var converter = TypeDescriptor.GetConverter(type); + + if (converter.CanConvertFrom(typeof(string))) return true; + + return GetParseMethod(type, out _) != null; + } + + public static MethodInfo? GetParseMethod(Type type, out bool hasFormat) + { + var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null); + + if (m != null) + { + hasFormat = true; + + return m; + } + + hasFormat = false; + + return type.GetMethod("Parse", PublicStatic, null, StringParameter, null); + } + } + + private sealed class ValueToDecimalConverter : IValueConverter + { + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return System.Convert.ToDecimal(value); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return System.Convert.ChangeType(value, targetType); + } + } + + private sealed class TextToValueConverter : IValueConverter + { + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) + return null; + + var converter = TypeDescriptor.GetConverter(value); + + //CollectionConverter does not deliver any important information. It just displays "(Collection)". + if (!converter.CanConvertTo(typeof(string)) || + converter.GetType() == typeof(CollectionConverter)) + return value.ToString(); + + return converter.ConvertToString(value); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not string s) + return null; + + try + { + var converter = TypeDescriptor.GetConverter(targetType); + + return converter.CanConvertFrom(typeof(string)) ? + converter.ConvertFrom(null, CultureInfo.InvariantCulture, s) : + InvokeParse(s, targetType); + } + catch + { + return BindingOperations.DoNothing; + } + } + + private static object? InvokeParse(string s, Type targetType) + { + var m = StringConversionHelper.GetParseMethod(targetType, out bool hasFormat); + + if (m == null) throw new InvalidOperationException(); + + return m.Invoke(null, + hasFormat ? + new object[] { s, CultureInfo.InvariantCulture } : + new object[] { s }); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 0e412a2fa5..2412ea5325 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -35,15 +35,14 @@ namespace Avalonia.Diagnostics.ViewModels public override string Priority => _priority; public override Type AssignedType => _assignedType; - public override string? Value + public override object? Value { - get => ConvertToString(_value); + get => _value; set { try { - var convertedValue = ConvertFromString(value, Property.PropertyType); - _target.SetValue(Property, convertedValue); + _target.SetValue(Property, value); Update(); } catch { } @@ -54,6 +53,7 @@ namespace Avalonia.Diagnostics.ViewModels public override Type? DeclaringType { get; } public override Type PropertyType => _propertyType; + public override bool IsReadonly => Property.IsReadOnly; // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index 895ff41f7b..b7ee1459f7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -40,16 +40,16 @@ namespace Avalonia.Diagnostics.ViewModels public override Type AssignedType => _assignedType; public override Type PropertyType => _propertyType; + public override bool IsReadonly => !Property.CanWrite; - public override string? Value + public override object? Value { - get => ConvertToString(_value); + get => _value; set { try { - var convertedValue = ConvertFromString(value, Property.PropertyType); - Property.SetValue(_target, convertedValue); + Property.SetValue(_target, value); Update(); } catch { } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index a7faf35769..aa2682e376 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -7,78 +7,21 @@ namespace Avalonia.Diagnostics.ViewModels { internal abstract class PropertyViewModel : ViewModelBase { - private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; - private static readonly Type[] StringParameter = { typeof(string) }; - private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; - public abstract object Key { get; } public abstract string Name { get; } public abstract string Group { get; } public abstract Type AssignedType { get; } public abstract Type? DeclaringType { get; } - public abstract string? Value { get; set; } + public abstract object? Value { get; set; } public abstract string Priority { get; } public abstract bool? IsAttached { get; } public abstract void Update(); public abstract Type PropertyType { get; } - public string Type => PropertyType == AssignedType - ? PropertyType.GetTypeName() - : $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}"; - - - protected static string? ConvertToString(object? value) - { - if (value is null) - { - return "(null)"; - } - - var converter = TypeDescriptor.GetConverter(value); - - //CollectionConverter does not deliver any important information. It just displays "(Collection)". - if (!converter.CanConvertTo(typeof(string)) || - converter.GetType() == typeof(CollectionConverter)) - { - return value.ToString() ?? "(null)"; - } - - return converter.ConvertToString(value); - } - - private static object? InvokeParse(string s, Type targetType) - { - var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture }); - } - - method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); - - if (method != null) - { - return method.Invoke(null, new object[] { s }); - } - - throw new InvalidCastException("Unable to convert value."); - } - - protected static object? ConvertFromString(string? s, Type targetType) - { - if (s is null) - { - return null; - } - - var converter = TypeDescriptor.GetConverter(targetType); - if (converter.CanConvertFrom(typeof(string))) - { - return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); - } + public string Type => PropertyType == AssignedType ? + PropertyType.GetTypeName() : + $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}"; - return InvokeParse(s, targetType); - } + public abstract bool IsReadonly { get; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 2a69798c6c..51421a7097 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -60,7 +60,14 @@ DoubleTapped="PropertiesGrid_OnDoubleTapped"> - + + + + + + From 2402fe07e34ad591297551e80ccad2bc45c39abc Mon Sep 17 00:00:00 2001 From: pr8x Date: Thu, 16 Feb 2023 16:59:15 +0100 Subject: [PATCH 05/71] NumericUpDown for ints --- .../Diagnostics/Controls/PropertyValueEditor.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs index 74a54dd702..ac90f01fad 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs @@ -180,8 +180,14 @@ namespace Avalonia.Diagnostics.Controls return CreateControl(ToggleButton.IsCheckedProperty); //TODO: Infinity, NaN not working with NumericUpDown - //if (ValueType.IsPrimitive) - // return CreateControl(NumericUpDown.ValueProperty, new ValueToDecimalConverter()); + if (ValueType.IsPrimitive && ValueType != typeof(float) && ValueType != typeof(double)) + return CreateControl(NumericUpDown.ValueProperty, new ValueToDecimalConverter(), init: + n => + { + n.Increment = 1; + n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 }; + n.ParsingNumberStyle = NumberStyles.Integer; + }); if (ValueType == typeof(Color)) return CreateControl(ColorView.ColorProperty); From 252f02b21e393dc41eea4d0a17b6d433d186ec0a Mon Sep 17 00:00:00 2001 From: pr8x Date: Thu, 16 Feb 2023 17:54:37 +0100 Subject: [PATCH 06/71] Fix edior not loading directyl when value is null --- .../Controls/PropertyValueEditor.cs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs index ac90f01fad..a045fb09a1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs @@ -51,7 +51,16 @@ namespace Avalonia.Diagnostics.Controls public object? Value { get => _value; - set => SetAndRaise(ValueProperty, ref _value, value); + set + { + if (_needsUpdate) + { + _needsUpdate = false; + Child = UpdateControl(); + } + + SetAndRaise(ValueProperty, ref _value, value); + } } public Type? ValueType @@ -77,13 +86,6 @@ namespace Avalonia.Diagnostics.Controls _needsUpdate = true; } - - if (change.Property == ValueProperty && _needsUpdate) - { - _needsUpdate = false; - - Child = UpdateControl(); - } } //Unfortunately we cannot use TwoWay bindings as they update the source with the target value @@ -202,13 +204,7 @@ namespace Avalonia.Diagnostics.Controls img.HorizontalAlignment = HorizontalAlignment.Stretch; img.PointerPressed += (_, _) => - new Window - { - Content = new Image - { - Source = img.Source - } - }.Show(); + new Window { Content = new Image { Source = img.Source } }.Show(); }); if (ValueType.IsEnum) From 35ba4ab4a7f43c26223d0857c53ea623991588ba Mon Sep 17 00:00:00 2001 From: pr8x Date: Thu, 16 Feb 2023 19:32:04 +0100 Subject: [PATCH 07/71] Make it a custom UserControl --- .../Controls/PropertyValueEditor.cs | 327 ------------------ .../Controls/PropertyValueEditorView.cs | 243 +++++++++++++ .../Diagnostics/Views/ControlDetailsView.xaml | 5 +- 3 files changed, 244 insertions(+), 331 deletions(-) delete mode 100644 src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs deleted file mode 100644 index a045fb09a1..0000000000 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditor.cs +++ /dev/null @@ -1,327 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; -using System.Reflection; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Data.Converters; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Reactive; - -namespace Avalonia.Diagnostics.Controls -{ - internal class PropertyValueEditor : Decorator - { - /// - /// Defines the property. - /// - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect( - nameof(Value), o => o.Value, (o, v) => o.Value = v); - - /// - /// Defines the property. - /// - public static readonly DirectProperty ValueTypeProperty = - AvaloniaProperty.RegisterDirect( - nameof(ValueType), o => o.ValueType, (o, v) => o.ValueType = v); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsReadonlyProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsReadonly), o => o.IsReadonly, (o, v) => o.IsReadonly = v); - - private readonly CompositeDisposable _cleanup = new(); - - private bool _isReadonly; - private bool _needsUpdate; - private object? _value; - private Type? _valueType; - - public bool IsReadonly - { - get => _isReadonly; - set => SetAndRaise(IsReadonlyProperty, ref _isReadonly, value); - } - - public object? Value - { - get => _value; - set - { - if (_needsUpdate) - { - _needsUpdate = false; - Child = UpdateControl(); - } - - SetAndRaise(ValueProperty, ref _value, value); - } - } - - public Type? ValueType - { - get => _valueType; - set => SetAndRaise(ValueTypeProperty, ref _valueType, value); - } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - - _cleanup.Clear(); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == ValueTypeProperty) - { - _cleanup.Clear(); - - _needsUpdate = true; - } - } - - //Unfortunately we cannot use TwoWay bindings as they update the source with the target value - //This causes the source property value to be overwritten. Ideally we there would be some kind of - //"InitialBindingDirection" or something to control whether the first value is from source or target. - private static void TwoWayBindingFromSource( - AvaloniaObject source, - AvaloniaProperty sourceProperty, - AvaloniaObject target, - AvaloniaProperty targetProperty, - IValueConverter? converter, - Type targetType, - CompositeDisposable disposable) - { - bool isUpdating = false; - - source - .GetObservable(sourceProperty) - .Subscribe(value => - { - if (isUpdating) return; - - try - { - isUpdating = true; - - target[targetProperty] = converter != null ? - converter.Convert(value, typeof(object), null, CultureInfo.CurrentCulture) : - value; - } - finally - { - isUpdating = false; - } - }) - .DisposeWith(disposable); - - target - .GetObservable(targetProperty) - .Skip(1) - .Subscribe(value => - { - if (isUpdating) return; - - try - { - isUpdating = true; - - source[sourceProperty] = converter != null ? - converter.ConvertBack(value, targetType, null, CultureInfo.CurrentCulture) : - value; - } - finally - { - isUpdating = false; - } - }) - .DisposeWith(disposable); - } - - private Control? UpdateControl() - { - if (ValueType is null) return null; - - TControl CreateControl(AvaloniaProperty valueProperty, - IValueConverter? converter = null, - Action? init = null) - where TControl : Control, new() - { - var control = new TControl(); - - init?.Invoke(control); - - TwoWayBindingFromSource( - this, - ValueProperty, - control, - valueProperty, - converter, - ValueType, - _cleanup); - - control.Bind( - IsEnabledProperty, - new Binding(nameof(IsReadonly)) { Source = this, Converter = BoolConverters.Not }) - .DisposeWith(_cleanup); - - return control; - } - - bool isObjectType = ValueType == typeof(object); - - if (ValueType == typeof(bool)) - return CreateControl(ToggleButton.IsCheckedProperty); - - //TODO: Infinity, NaN not working with NumericUpDown - if (ValueType.IsPrimitive && ValueType != typeof(float) && ValueType != typeof(double)) - return CreateControl(NumericUpDown.ValueProperty, new ValueToDecimalConverter(), init: - n => - { - n.Increment = 1; - n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 }; - n.ParsingNumberStyle = NumberStyles.Integer; - }); - - if (ValueType == typeof(Color)) - return CreateControl(ColorView.ColorProperty); - - if (!isObjectType && ValueType.IsAssignableFrom(typeof(IBrush))) - return CreateControl(BrushEditor.BrushProperty); - - if (!isObjectType && ValueType.IsAssignableFrom(typeof(IImage))) - return CreateControl(Image.SourceProperty, init: img => - { - img.Stretch = Stretch.Uniform; - img.HorizontalAlignment = HorizontalAlignment.Stretch; - - img.PointerPressed += (_, _) => - new Window { Content = new Image { Source = img.Source } }.Show(); - }); - - if (ValueType.IsEnum) - return CreateControl( - SelectingItemsControl.SelectedItemProperty, init: c => - { - c.Items = Enum.GetValues(ValueType); - }); - - var tb = CreateControl( - TextBox.TextProperty, - new TextToValueConverter(), - t => - { - t.Watermark = "(null)"; - }); - - tb.IsEnabled &= !isObjectType && - StringConversionHelper.CanConvertFromString(ValueType); - - return tb; - } - - private static class StringConversionHelper - { - private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; - private static readonly Type[] StringParameter = { typeof(string) }; - - private static readonly Type[] - StringFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; - - public static bool CanConvertFromString(Type type) - { - var converter = TypeDescriptor.GetConverter(type); - - if (converter.CanConvertFrom(typeof(string))) return true; - - return GetParseMethod(type, out _) != null; - } - - public static MethodInfo? GetParseMethod(Type type, out bool hasFormat) - { - var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null); - - if (m != null) - { - hasFormat = true; - - return m; - } - - hasFormat = false; - - return type.GetMethod("Parse", PublicStatic, null, StringParameter, null); - } - } - - private sealed class ValueToDecimalConverter : IValueConverter - { - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return System.Convert.ToDecimal(value); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return System.Convert.ChangeType(value, targetType); - } - } - - private sealed class TextToValueConverter : IValueConverter - { - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is null) - return null; - - var converter = TypeDescriptor.GetConverter(value); - - //CollectionConverter does not deliver any important information. It just displays "(Collection)". - if (!converter.CanConvertTo(typeof(string)) || - converter.GetType() == typeof(CollectionConverter)) - return value.ToString(); - - return converter.ConvertToString(value); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is not string s) - return null; - - try - { - var converter = TypeDescriptor.GetConverter(targetType); - - return converter.CanConvertFrom(typeof(string)) ? - converter.ConvertFrom(null, CultureInfo.InvariantCulture, s) : - InvokeParse(s, targetType); - } - catch - { - return BindingOperations.DoNothing; - } - } - - private static object? InvokeParse(string s, Type targetType) - { - var m = StringConversionHelper.GetParseMethod(targetType, out bool hasFormat); - - if (m == null) throw new InvalidOperationException(); - - return m.Invoke(null, - hasFormat ? - new object[] { s, CultureInfo.InvariantCulture } : - new object[] { s }); - } - } - } -} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs new file mode 100644 index 0000000000..0bc237ab85 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs @@ -0,0 +1,243 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Reactive; + +namespace Avalonia.Diagnostics.Controls +{ + internal class PropertyValueEditorView : UserControl + { + private readonly CompositeDisposable _cleanup = new(); + private PropertyViewModel? Property => (PropertyViewModel?)DataContext; + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + Content = UpdateControl(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _cleanup.Clear(); + } + + private Control? UpdateControl() + { + _cleanup.Clear(); + + if (Property?.PropertyType is not { } propertyType) return null; + + TControl CreateControl(AvaloniaProperty valueProperty, + IValueConverter? converter = null, + Action? init = null) + where TControl : Control, new() + { + var control = new TControl(); + + init?.Invoke(control); + + control.Bind(valueProperty, + new Binding(nameof(Property.Value), BindingMode.TwoWay) + { + Source = Property, + Converter = converter ?? new ValueConverter() + }).DisposeWith(_cleanup); + + control.IsEnabled = !Property.IsReadonly; + + return control; + } + + bool isObjectType = propertyType == typeof(object); + + if (propertyType == typeof(bool)) + return CreateControl(ToggleButton.IsCheckedProperty); + + //TODO: Infinity, NaN not working with NumericUpDown + if (propertyType.IsPrimitive && propertyType != typeof(float) && propertyType != typeof(double)) + return CreateControl( + NumericUpDown.ValueProperty, + new ValueToDecimalConverter(), + init: n => + { + n.Increment = 1; + n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 }; + n.ParsingNumberStyle = NumberStyles.Integer; + }); + + if (propertyType == typeof(Color)) + return CreateControl(ColorView.ColorProperty); + + if (!isObjectType && propertyType.IsAssignableFrom(typeof(IBrush))) + return CreateControl(BrushEditor.BrushProperty); + + if (!isObjectType && propertyType.IsAssignableFrom(typeof(IImage))) + return CreateControl(Image.SourceProperty, init: img => + { + img.Stretch = Stretch.Uniform; + img.HorizontalAlignment = HorizontalAlignment.Stretch; + + img.PointerPressed += (_, _) => + new Window { Content = new Image { Source = img.Source } }.Show(); + }); + + if (propertyType.IsEnum) + return CreateControl( + SelectingItemsControl.SelectedItemProperty, init: c => + { + c.Items = Enum.GetValues(propertyType); + }); + + var tb = CreateControl( + TextBox.TextProperty, + new TextToValueConverter(), + t => + { + t.Watermark = "(null)"; + }); + + tb.IsEnabled &= !isObjectType && + StringConversionHelper.CanConvertFromString(propertyType); + + return tb; + } + + //HACK: ValueConverter that skips first target update + private class ValueConverter : IValueConverter + { + private bool _firstUpdate = true; + + protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value; + } + + protected virtual object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value; + } + + object? IValueConverter.Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return Convert(value, targetType, parameter, culture); + } + + object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (_firstUpdate) + { + _firstUpdate = false; + + return BindingOperations.DoNothing; + } + + return ConvertBack(value, targetType, parameter, culture); + } + } + + private static class StringConversionHelper + { + private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; + private static readonly Type[] StringParameter = { typeof(string) }; + private static readonly Type[] StringFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; + + public static bool CanConvertFromString(Type type) + { + var converter = TypeDescriptor.GetConverter(type); + + if (converter.CanConvertFrom(typeof(string))) return true; + + return GetParseMethod(type, out _) != null; + } + + public static MethodInfo? GetParseMethod(Type type, out bool hasFormat) + { + var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null); + + if (m != null) + { + hasFormat = true; + + return m; + } + + hasFormat = false; + + return type.GetMethod("Parse", PublicStatic, null, StringParameter, null); + } + } + + private sealed class ValueToDecimalConverter : ValueConverter + { + protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return System.Convert.ToDecimal(value); + } + + protected override object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return System.Convert.ChangeType(value, targetType); + } + } + + private sealed class TextToValueConverter : ValueConverter + { + protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) + return null; + + var converter = TypeDescriptor.GetConverter(value); + + //CollectionConverter does not deliver any important information. It just displays "(Collection)". + if (!converter.CanConvertTo(typeof(string)) || + converter.GetType() == typeof(CollectionConverter)) + return value.ToString(); + + return converter.ConvertToString(value); + } + + protected override object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not string s) + return null; + + try + { + var converter = TypeDescriptor.GetConverter(targetType); + + return converter.CanConvertFrom(typeof(string)) ? + converter.ConvertFrom(null, CultureInfo.InvariantCulture, s) : + InvokeParse(s, targetType); + } + catch + { + return BindingOperations.DoNothing; + } + } + + private static object? InvokeParse(string s, Type targetType) + { + var m = StringConversionHelper.GetParseMethod(targetType, out bool hasFormat); + + if (m == null) throw new InvalidOperationException(); + + return m.Invoke(null, + hasFormat ? + new object[] { s, CultureInfo.InvariantCulture } : + new object[] { s }); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 51421a7097..612626f91e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -62,10 +62,7 @@ - + Date: Fri, 17 Feb 2023 10:56:08 +0100 Subject: [PATCH 08/71] Improved image preview --- .../Controls/PropertyValueEditorView.cs | 65 +++++++++++++++---- .../ViewModels/ReactiveExtensions.cs | 57 ++++++++++++++++ 2 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs index 0bc237ab85..78df63e41f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs @@ -4,17 +4,22 @@ using System.Globalization; using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Diagnostics.ViewModels; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Reactive; +using Image = Avalonia.Controls.Image; namespace Avalonia.Diagnostics.Controls { internal class PropertyValueEditorView : UserControl { + private static Geometry ImageGeometry = Geometry.Parse( + "M12.25 6C8.79822 6 6 8.79822 6 12.25V35.75C6 37.1059 6.43174 38.3609 7.16525 39.3851L21.5252 25.0251C22.8921 23.6583 25.1081 23.6583 26.475 25.0251L40.8348 39.385C41.5683 38.3608 42 37.1058 42 35.75V12.25C42 8.79822 39.2018 6 35.75 6H12.25ZM34.5 17.5C34.5 19.7091 32.7091 21.5 30.5 21.5C28.2909 21.5 26.5 19.7091 26.5 17.5C26.5 15.2909 28.2909 13.5 30.5 13.5C32.7091 13.5 34.5 15.2909 34.5 17.5ZM39.0024 41.0881L24.7072 26.7929C24.3167 26.4024 23.6835 26.4024 23.293 26.7929L8.99769 41.0882C9.94516 41.6667 11.0587 42 12.25 42H35.75C36.9414 42 38.0549 41.6666 39.0024 41.0881Z"); + private readonly CompositeDisposable _cleanup = new(); private PropertyViewModel? Property => (PropertyViewModel?)DataContext; @@ -50,8 +55,7 @@ namespace Avalonia.Diagnostics.Controls control.Bind(valueProperty, new Binding(nameof(Property.Value), BindingMode.TwoWay) { - Source = Property, - Converter = converter ?? new ValueConverter() + Source = Property, Converter = converter ?? new ValueConverter() }).DisposeWith(_cleanup); control.IsEnabled = !Property.IsReadonly; @@ -83,14 +87,50 @@ namespace Avalonia.Diagnostics.Controls return CreateControl(BrushEditor.BrushProperty); if (!isObjectType && propertyType.IsAssignableFrom(typeof(IImage))) - return CreateControl(Image.SourceProperty, init: img => + { + var valueObservable = Property.GetObservable(x => x.Value); + + var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; + + tbl.Bind(TextBlock.TextProperty, + valueObservable.Select( + value => value is IImage image ? $"{image.Size.Width} x {image.Size.Height}" : "(null)")) + .DisposeWith(_cleanup); + + var sp = new StackPanel { - img.Stretch = Stretch.Uniform; - img.HorizontalAlignment = HorizontalAlignment.Stretch; + Background = Brushes.Transparent, + Orientation = Orientation.Horizontal, + Spacing = 2, + Children = + { + new Path + { + Data = ImageGeometry, + Fill = Brushes.Gray, + Width = 12, + Height = 12, + Stretch = Stretch.Uniform, + VerticalAlignment = VerticalAlignment.Center + }, + tbl + } + }; + + var previewImage = new Image + { + Width = 300, + Height = 300 + }; - img.PointerPressed += (_, _) => - new Window { Content = new Image { Source = img.Source } }.Show(); - }); + previewImage + .Bind(Image.SourceProperty, valueObservable) + .DisposeWith(_cleanup); + + ToolTip.SetTip(sp, previewImage); + + return sp; + } if (propertyType.IsEnum) return CreateControl( @@ -123,7 +163,8 @@ namespace Avalonia.Diagnostics.Controls return value; } - protected virtual object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + protected virtual object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) { return value; } @@ -185,7 +226,8 @@ namespace Avalonia.Diagnostics.Controls return System.Convert.ToDecimal(value); } - protected override object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + protected override object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) { return System.Convert.ChangeType(value, targetType); } @@ -208,7 +250,8 @@ namespace Avalonia.Diagnostics.Controls return converter.ConvertToString(value); } - protected override object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + protected override object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) { if (value is not string s) return null; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs new file mode 100644 index 0000000000..9425989096 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ReactiveExtensions.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using Avalonia.Reactive; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal static class ReactiveExtensions + { + public static IObservable GetObservable( + this TOwner vm, + Expression> property, + bool fireImmediately = true) + where TOwner : INotifyPropertyChanged + { + return Observable.Create(o => + { + var propertyInfo = GetPropertyInfo(property); + + void Fire() + { + o.OnNext((TValue) propertyInfo.GetValue(vm)!); + } + + void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == propertyInfo.Name) + { + Fire(); + } + } + + if (fireImmediately) + { + Fire(); + } + + vm.PropertyChanged += OnPropertyChanged; + + return Disposable.Create(() => vm.PropertyChanged -= OnPropertyChanged); + }); + } + + private static PropertyInfo GetPropertyInfo(this Expression> property) + { + if (property.Body is UnaryExpression unaryExpression) + { + return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member; + } + + var memExpr = (MemberExpression)property.Body; + + return (PropertyInfo)memExpr.Member; + } + } +} From 0c6387a0fcc35d2134f4266e71d1622703c07b77 Mon Sep 17 00:00:00 2001 From: pr8x Date: Fri, 17 Feb 2023 13:43:51 +0100 Subject: [PATCH 09/71] Add support for geometry --- .../Controls/PropertyValueEditorView.cs | 94 +++++++++++++------ 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs index 78df63e41f..071f4628a7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs @@ -11,15 +11,17 @@ using Avalonia.Diagnostics.ViewModels; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Reactive; -using Image = Avalonia.Controls.Image; namespace Avalonia.Diagnostics.Controls { internal class PropertyValueEditorView : UserControl { - private static Geometry ImageGeometry = Geometry.Parse( + private static readonly Geometry ImageIcon = Geometry.Parse( "M12.25 6C8.79822 6 6 8.79822 6 12.25V35.75C6 37.1059 6.43174 38.3609 7.16525 39.3851L21.5252 25.0251C22.8921 23.6583 25.1081 23.6583 26.475 25.0251L40.8348 39.385C41.5683 38.3608 42 37.1058 42 35.75V12.25C42 8.79822 39.2018 6 35.75 6H12.25ZM34.5 17.5C34.5 19.7091 32.7091 21.5 30.5 21.5C28.2909 21.5 26.5 19.7091 26.5 17.5C26.5 15.2909 28.2909 13.5 30.5 13.5C32.7091 13.5 34.5 15.2909 34.5 17.5ZM39.0024 41.0881L24.7072 26.7929C24.3167 26.4024 23.6835 26.4024 23.293 26.7929L8.99769 41.0882C9.94516 41.6667 11.0587 42 12.25 42H35.75C36.9414 42 38.0549 41.6666 39.0024 41.0881Z"); + private static readonly Geometry GeometryIcon = Geometry.Parse( + "M23.25 15.5H30.8529C29.8865 8.99258 24.2763 4 17.5 4C10.0442 4 4 10.0442 4 17.5C4 24.2763 8.99258 29.8865 15.5 30.8529V23.25C15.5 18.9698 18.9698 15.5 23.25 15.5ZM23.25 18C20.3505 18 18 20.3505 18 23.25V38.75C18 41.6495 20.3505 44 23.25 44H38.75C41.6495 44 44 41.6495 44 38.75V23.25C44 20.3505 41.6495 18 38.75 18H23.25Z"); + private readonly CompositeDisposable _cleanup = new(); private PropertyViewModel? Property => (PropertyViewModel?)DataContext; @@ -37,6 +39,12 @@ namespace Avalonia.Diagnostics.Controls _cleanup.Clear(); } + private static bool ImplementsInterface(Type type) + { + var interfaceType = typeof(TInterface); + return type == interfaceType || type.GetInterface(interfaceType.FullName!) != null; + } + private Control? UpdateControl() { _cleanup.Clear(); @@ -63,8 +71,6 @@ namespace Avalonia.Diagnostics.Controls return control; } - bool isObjectType = propertyType == typeof(object); - if (propertyType == typeof(bool)) return CreateControl(ToggleButton.IsCheckedProperty); @@ -80,21 +86,27 @@ namespace Avalonia.Diagnostics.Controls n.ParsingNumberStyle = NumberStyles.Integer; }); - if (propertyType == typeof(Color)) - return CreateControl(ColorView.ColorProperty); + if (propertyType == typeof(Color)) return CreateControl(ColorView.ColorProperty); - if (!isObjectType && propertyType.IsAssignableFrom(typeof(IBrush))) + if (ImplementsInterface(propertyType)) return CreateControl(BrushEditor.BrushProperty); - if (!isObjectType && propertyType.IsAssignableFrom(typeof(IImage))) + var isImage = ImplementsInterface(propertyType); + var isGeometry = propertyType == typeof(Geometry); + + if (isImage || isGeometry) { var valueObservable = Property.GetObservable(x => x.Value); - var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; tbl.Bind(TextBlock.TextProperty, valueObservable.Select( - value => value is IImage image ? $"{image.Size.Width} x {image.Size.Height}" : "(null)")) + value => value switch + { + IImage img => $"{img.Size.Width} x {img.Size.Height}", + Geometry geom => $"{geom.Bounds.Width} x {geom.Bounds.Height}", + _ => "(null)" + })) .DisposeWith(_cleanup); var sp = new StackPanel @@ -106,7 +118,7 @@ namespace Avalonia.Diagnostics.Controls { new Path { - Data = ImageGeometry, + Data = isImage ? ImageIcon : GeometryIcon, Fill = Brushes.Gray, Width = 12, Height = 12, @@ -117,17 +129,37 @@ namespace Avalonia.Diagnostics.Controls } }; - var previewImage = new Image + if (isImage) { - Width = 300, - Height = 300 - }; + var previewImage = new Image { Width = 300, Height = 300 }; - previewImage - .Bind(Image.SourceProperty, valueObservable) - .DisposeWith(_cleanup); + previewImage + .Bind(Image.SourceProperty, valueObservable) + .DisposeWith(_cleanup); + + ToolTip.SetTip(sp, previewImage); + } + else + { + var previewShape = new Path + { + Stretch = Stretch.Uniform, + Fill = Brushes.White, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center + }; - ToolTip.SetTip(sp, previewImage); + previewShape + .Bind(Path.DataProperty, valueObservable) + .DisposeWith(_cleanup); + + ToolTip.SetTip(sp, new Border + { + Child = previewShape, + Width = 300, + Height = 300 + }); + } return sp; } @@ -147,7 +179,7 @@ namespace Avalonia.Diagnostics.Controls t.Watermark = "(null)"; }); - tb.IsEnabled &= !isObjectType && + tb.IsEnabled &= propertyType != typeof(object) && StringConversionHelper.CanConvertFromString(propertyType); return tb; @@ -158,17 +190,6 @@ namespace Avalonia.Diagnostics.Controls { private bool _firstUpdate = true; - protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value; - } - - protected virtual object? ConvertBack(object? value, Type targetType, object? parameter, - CultureInfo culture) - { - return value; - } - object? IValueConverter.Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { return Convert(value, targetType, parameter, culture); @@ -185,6 +206,17 @@ namespace Avalonia.Diagnostics.Controls return ConvertBack(value, targetType, parameter, culture); } + + protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value; + } + + protected virtual object? ConvertBack(object? value, Type targetType, object? parameter, + CultureInfo culture) + { + return value; + } } private static class StringConversionHelper From 7d3dd1c933954028860d79382c79a8a7a17d04bc Mon Sep 17 00:00:00 2001 From: pr8x Date: Fri, 17 Feb 2023 14:34:25 +0100 Subject: [PATCH 10/71] FIx an issuse with wrong targetType in converter --- .../Controls/PropertyValueEditorView.cs | 82 ++++++++++--------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs index 071f4628a7..fefe642f51 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs @@ -63,7 +63,9 @@ namespace Avalonia.Diagnostics.Controls control.Bind(valueProperty, new Binding(nameof(Property.Value), BindingMode.TwoWay) { - Source = Property, Converter = converter ?? new ValueConverter() + Source = Property, + Converter = converter ?? new ValueConverter(), + ConverterParameter = propertyType }).DisposeWith(_cleanup); control.IsEnabled = !Property.IsReadonly; @@ -153,12 +155,7 @@ namespace Avalonia.Diagnostics.Controls .Bind(Path.DataProperty, valueObservable) .DisposeWith(_cleanup); - ToolTip.SetTip(sp, new Border - { - Child = previewShape, - Width = 300, - Height = 300 - }); + ToolTip.SetTip(sp, new Border { Child = previewShape, Width = 300, Height = 300 }); } return sp; @@ -186,6 +183,8 @@ namespace Avalonia.Diagnostics.Controls } //HACK: ValueConverter that skips first target update + //TODO: Would be nice to have some kind of "InitialBindingValue" option on TwoWay bindings to control + //if the first value comes from the source or target private class ValueConverter : IValueConverter { private bool _firstUpdate = true; @@ -204,7 +203,8 @@ namespace Avalonia.Diagnostics.Controls return BindingOperations.DoNothing; } - return ConvertBack(value, targetType, parameter, culture); + //Note: targetType provided by Converter is simply "object" + return ConvertBack(value, (Type)parameter!, parameter, culture); } protected virtual object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) @@ -234,7 +234,41 @@ namespace Avalonia.Diagnostics.Controls return GetParseMethod(type, out _) != null; } - public static MethodInfo? GetParseMethod(Type type, out bool hasFormat) + public static string? ToString(object o) + { + var converter = TypeDescriptor.GetConverter(o); + + //CollectionConverter does not deliver any important information. It just displays "(Collection)". + if (!converter.CanConvertTo(typeof(string)) || + converter.GetType() == typeof(CollectionConverter)) + return o.ToString(); + + return converter.ConvertToString(o); + } + + public static object? FromString(string str, Type type) + { + var converter = TypeDescriptor.GetConverter(type); + + return converter.CanConvertFrom(typeof(string)) ? + converter.ConvertFrom(null, CultureInfo.InvariantCulture, str) : + InvokeParse(str, type); + } + + private static object? InvokeParse(string s, Type targetType) + { + var m = GetParseMethod(targetType, out bool hasFormat); + + if (m == null) + throw new InvalidOperationException(); + + return m.Invoke(null, + hasFormat ? + new object[] { s, CultureInfo.InvariantCulture } : + new object[] { s }); + } + + private static MethodInfo? GetParseMethod(Type type, out bool hasFormat) { var m = type.GetMethod("Parse", PublicStatic, null, StringFormatProviderParameters, null); @@ -269,17 +303,7 @@ namespace Avalonia.Diagnostics.Controls { protected override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - if (value is null) - return null; - - var converter = TypeDescriptor.GetConverter(value); - - //CollectionConverter does not deliver any important information. It just displays "(Collection)". - if (!converter.CanConvertTo(typeof(string)) || - converter.GetType() == typeof(CollectionConverter)) - return value.ToString(); - - return converter.ConvertToString(value); + return value is null ? null : StringConversionHelper.ToString(value); } protected override object? ConvertBack(object? value, Type targetType, object? parameter, @@ -290,29 +314,13 @@ namespace Avalonia.Diagnostics.Controls try { - var converter = TypeDescriptor.GetConverter(targetType); - - return converter.CanConvertFrom(typeof(string)) ? - converter.ConvertFrom(null, CultureInfo.InvariantCulture, s) : - InvokeParse(s, targetType); + return StringConversionHelper.FromString(s, targetType); } catch { return BindingOperations.DoNothing; } } - - private static object? InvokeParse(string s, Type targetType) - { - var m = StringConversionHelper.GetParseMethod(targetType, out bool hasFormat); - - if (m == null) throw new InvalidOperationException(); - - return m.Invoke(null, - hasFormat ? - new object[] { s, CultureInfo.InvariantCulture } : - new object[] { s }); - } } } } From 1fa4ec9ac051f153436b6a838054cd335675e411 Mon Sep 17 00:00:00 2001 From: pr8x Date: Mon, 20 Feb 2023 15:23:23 +0100 Subject: [PATCH 11/71] New color editor --- .../Controls/PropertyValueEditorView.cs | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs index fefe642f51..313c197a52 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs @@ -8,7 +8,9 @@ using Avalonia.Controls.Shapes; using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; using Avalonia.Layout; +using Avalonia.Markup.Xaml.Converters; using Avalonia.Media; using Avalonia.Reactive; @@ -22,6 +24,8 @@ namespace Avalonia.Diagnostics.Controls private static readonly Geometry GeometryIcon = Geometry.Parse( "M23.25 15.5H30.8529C29.8865 8.99258 24.2763 4 17.5 4C10.0442 4 4 10.0442 4 17.5C4 24.2763 8.99258 29.8865 15.5 30.8529V23.25C15.5 18.9698 18.9698 15.5 23.25 15.5ZM23.25 18C20.3505 18 18 20.3505 18 23.25V38.75C18 41.6495 20.3505 44 23.25 44H38.75C41.6495 44 44 41.6495 44 38.75V23.25C44 20.3505 41.6495 18 38.75 18H23.25Z"); + private static readonly ColorToBrushConverter Color2Brush = new(); + private readonly CompositeDisposable _cleanup = new(); private PropertyViewModel? Property => (PropertyViewModel?)DataContext; @@ -88,7 +92,54 @@ namespace Avalonia.Diagnostics.Controls n.ParsingNumberStyle = NumberStyles.Integer; }); - if (propertyType == typeof(Color)) return CreateControl(ColorView.ColorProperty); + if (propertyType == typeof(Color)) + { + var el = new Ellipse + { + Width = 12, + Height = 12, + VerticalAlignment = VerticalAlignment.Center, + Cursor = new Cursor(StandardCursorType.Hand) + }; + + el.Bind( + Shape.FillProperty, + new Binding(nameof(Property.Value)) { Source = Property, Converter = Color2Brush }) + .DisposeWith(_cleanup); + + var tbl = new TextBlock + { + VerticalAlignment = VerticalAlignment.Center + }; + + tbl.Bind( + TextBlock.TextProperty, + new Binding(nameof(Property.Value)) { Source = Property }) + .DisposeWith(_cleanup); + + var sp = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 2, + Children = { el, tbl } + }; + + var cv = new ColorView(); + + cv.Bind( + ColorView.ColorProperty, + new Binding(nameof(Property.Value), BindingMode.TwoWay) + { + Source = Property, Converter = Color2Brush + }) + .DisposeWith(_cleanup); + + FlyoutBase.SetAttachedFlyout(sp, new Flyout { Content = cv }); + + sp.PointerPressed += (_, _) => FlyoutBase.ShowAttachedFlyout(sp); + + return sp; + } if (ImplementsInterface(propertyType)) return CreateControl(BrushEditor.BrushProperty); @@ -133,7 +184,7 @@ namespace Avalonia.Diagnostics.Controls if (isImage) { - var previewImage = new Image { Width = 300, Height = 300 }; + var previewImage = new Image { Stretch = Stretch.Uniform, Width = 300, Height = 300 }; previewImage .Bind(Image.SourceProperty, valueObservable) From b9ca5dcdc4dd731bc3b6b7d421997add31ba0bd7 Mon Sep 17 00:00:00 2001 From: pr8x Date: Mon, 20 Feb 2023 15:29:45 +0100 Subject: [PATCH 12/71] Cursor on stackpanel --- .../Diagnostics/Controls/PropertyValueEditorView.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs index 313c197a52..e85d3cc88d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/PropertyValueEditorView.cs @@ -98,8 +98,7 @@ namespace Avalonia.Diagnostics.Controls { Width = 12, Height = 12, - VerticalAlignment = VerticalAlignment.Center, - Cursor = new Cursor(StandardCursorType.Hand) + VerticalAlignment = VerticalAlignment.Center }; el.Bind( @@ -121,7 +120,9 @@ namespace Avalonia.Diagnostics.Controls { Orientation = Orientation.Horizontal, Spacing = 2, - Children = { el, tbl } + Children = { el, tbl }, + Background = Brushes.Transparent, + Cursor = new Cursor(StandardCursorType.Hand) }; var cv = new ColorView(); From 2c492a59e74757e2b36657e937084ae83088ddb1 Mon Sep 17 00:00:00 2001 From: pr8x Date: Mon, 20 Feb 2023 16:31:23 +0100 Subject: [PATCH 13/71] Add validation to textbox input & only commit value on Focus loss or Enter --- .../Diagnostics/Controls/CommitTextBox.cs | 89 +++++++++++++++++++ .../Diagnostics/Views/ControlDetailsView.xaml | 4 +- .../PropertyValueEditorView.cs | 58 +++++++----- 3 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs rename src/Avalonia.Diagnostics/Diagnostics/{Controls => Views}/PropertyValueEditorView.cs (89%) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs new file mode 100644 index 0000000000..7870febd0a --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/CommitTextBox.cs @@ -0,0 +1,89 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Styling; + +namespace Avalonia.Diagnostics.Controls +{ + //TODO: UpdateSourceTrigger & Binding.ValidationRules could help removing the need for this control. + internal sealed class CommitTextBox : TextBox, IStyleable + { + Type IStyleable.StyleKey => typeof(TextBox); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommittedTextProperty = + AvaloniaProperty.RegisterDirect( + nameof(CommittedText), o => o.CommittedText, (o, v) => o.CommittedText = v); + + private string? _committedText; + + public string? CommittedText + { + get => _committedText; + set => SetAndRaise(CommittedTextProperty, ref _committedText, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == CommittedTextProperty) + { + Text = CommittedText; + } + } + + protected override void OnKeyUp(KeyEventArgs e) + { + base.OnKeyUp(e); + + switch (e.Key) + { + case Key.Enter: + + TryCommit(); + + e.Handled = true; + + break; + + case Key.Escape: + + Cancel(); + + e.Handled = true; + + break; + } + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + + TryCommit(); + } + + private void Cancel() + { + Text = CommittedText; + DataValidationErrors.ClearErrors(this); + } + + private void TryCommit() + { + if (!DataValidationErrors.GetHasErrors(this)) + { + CommittedText = Text; + } + else + { + Text = CommittedText; + DataValidationErrors.ClearErrors(this); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 612626f91e..97f195c91b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -61,8 +61,8 @@ - - + + (AvaloniaProperty valueProperty, IValueConverter? converter = null, @@ -94,22 +97,14 @@ namespace Avalonia.Diagnostics.Controls if (propertyType == typeof(Color)) { - var el = new Ellipse - { - Width = 12, - Height = 12, - VerticalAlignment = VerticalAlignment.Center - }; + var el = new Ellipse { Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center }; el.Bind( Shape.FillProperty, new Binding(nameof(Property.Value)) { Source = Property, Converter = Color2Brush }) .DisposeWith(_cleanup); - var tbl = new TextBlock - { - VerticalAlignment = VerticalAlignment.Center - }; + var tbl = new TextBlock { VerticalAlignment = VerticalAlignment.Center }; tbl.Bind( TextBlock.TextProperty, @@ -128,11 +123,11 @@ namespace Avalonia.Diagnostics.Controls var cv = new ColorView(); cv.Bind( - ColorView.ColorProperty, - new Binding(nameof(Property.Value), BindingMode.TwoWay) - { - Source = Property, Converter = Color2Brush - }) + ColorView.ColorProperty, + new Binding(nameof(Property.Value), BindingMode.TwoWay) + { + Source = Property, Converter = Color2Brush + }) .DisposeWith(_cleanup); FlyoutBase.SetAttachedFlyout(sp, new Flyout { Content = cv }); @@ -220,8 +215,8 @@ namespace Avalonia.Diagnostics.Controls c.Items = Enum.GetValues(propertyType); }); - var tb = CreateControl( - TextBox.TextProperty, + var tb = CreateControl( + CommitTextBox.CommittedTextProperty, new TextToValueConverter(), t => { @@ -231,6 +226,26 @@ namespace Avalonia.Diagnostics.Controls tb.IsEnabled &= propertyType != typeof(object) && StringConversionHelper.CanConvertFromString(propertyType); + if (tb.IsEnabled) + { + tb.GetObservable(TextBox.TextProperty).Subscribe(t => + { + try + { + if (t != null) + { + StringConversionHelper.FromString(t, propertyType); + } + + DataValidationErrors.ClearErrors(tb); + } + catch (Exception ex) + { + DataValidationErrors.SetError(tb, ex.GetBaseException()); + } + }).DisposeWith(_cleanup); + } + return tb; } @@ -281,7 +296,8 @@ namespace Avalonia.Diagnostics.Controls { var converter = TypeDescriptor.GetConverter(type); - if (converter.CanConvertFrom(typeof(string))) return true; + if (converter.CanConvertFrom(typeof(string))) + return true; return GetParseMethod(type, out _) != null; } @@ -309,7 +325,7 @@ namespace Avalonia.Diagnostics.Controls private static object? InvokeParse(string s, Type targetType) { - var m = GetParseMethod(targetType, out bool hasFormat); + var m = GetParseMethod(targetType, out var hasFormat); if (m == null) throw new InvalidOperationException(); From b07e5a50ba1de79dfb2123af0943bb2813efe1cd Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 23 Feb 2023 07:23:37 +0000 Subject: [PATCH 14/71] fix build error in android project --- .../Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 2d547201e9..f0fc13c86f 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -28,6 +28,7 @@ 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 { From d24b3a9fbfbc866b42e11e0a0c532ef3b8fbac4b Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 23 Feb 2023 16:50:03 +0000 Subject: [PATCH 15/71] set initial value of displayEdgeToEdge on android --- src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index ea8e0c0490..2dcd3e2c4c 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -55,6 +55,8 @@ namespace Avalonia.Android.Platform _usesLegacyLayouts = true; _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this); } + + _displayEdgeToEdge = !activity.Window.DecorView.FitsSystemWindows; } public Thickness GetSafeAreaPadding() From dcf8afa1a6201ee16ce61ed6e367766394af1782 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 24 Feb 2023 13:46:39 +0000 Subject: [PATCH 16/71] set initial edge to edge sate to false. --- src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 2dcd3e2c4c..10f06c726b 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -56,7 +56,7 @@ namespace Avalonia.Android.Platform _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this); } - _displayEdgeToEdge = !activity.Window.DecorView.FitsSystemWindows; + DisplayEdgeToEdge = false; } public Thickness GetSafeAreaPadding() From f6ced0811d57246a0ea758237df0f383cde40067 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 24 Feb 2023 14:00:23 +0000 Subject: [PATCH 17/71] specify the system bars to show or hide --- .../Avalonia.Android/Platform/AndroidInsetsManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 10f06c726b..7bc3a02736 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -67,7 +67,7 @@ namespace Avalonia.Android.Platform { var renderScaling = _topLevel.RenderScaling; - var inset = insets.GetInsets((DisplayEdgeToEdge ? WindowInsetsCompat.Type.SystemBars() | WindowInsetsCompat.Type.DisplayCutout() : 0 ) | WindowInsetsCompat.Type.Ime()); + var inset = insets.GetInsets((DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0 ) | WindowInsetsCompat.Type.Ime()); var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); @@ -143,7 +143,7 @@ namespace Avalonia.Android.Platform { var compat = ViewCompat.GetRootWindowInsets(_topLevel.View); - return compat?.IsVisible(WindowInsetsCompat.Type.SystemBars()); + return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); } set { @@ -158,11 +158,11 @@ namespace Avalonia.Android.Platform if (value == null || value.Value) { - compat?.Show(WindowInsetsCompat.Type.SystemBars()); + compat?.Show(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); } else { - compat?.Hide(WindowInsetsCompat.Type.SystemBars()); + compat?.Hide(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); if (compat != null) { @@ -194,7 +194,7 @@ namespace Avalonia.Android.Platform { var renderScaling = InsetsManager._topLevel.RenderScaling; - var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.SystemBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); + var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); From a503a3e1f0315a7b12ee4b595140f13ef2b0c3fa Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 24 Feb 2023 18:51:54 +0100 Subject: [PATCH 18/71] fix(AvaloniaPropertyAnalyzer): Avoid AVP1031 when Avalonia Field Accessibily in not Public Considering following case ```bash Warning AVP1031 Missing accessor: Avalonia.Controls.DefinitionBase.PrivateSharedSizeScopeProperty is writeable, but this CLR property lacks a set accessor Avalonia.Controls (net6.0) C:\GitHub\Avalonia\src\Avalonia.Controls\DefinitionBase.cs 418 Active Warning AVP1031 Missing accessor: Avalonia.Controls.TabItem.TabStripPlacementProperty is writeable, but this CLR property lacks a set accessor Avalonia.Controls (net6.0) C:\GitHub\Avalonia\src\Avalonia.Controls\TabItem.cs 45 Active ``` --- .../AvaloniaPropertyAnalyzer.CompileAnalyzer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index 0a27602604..af9b367368 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -396,7 +396,7 @@ public partial class AvaloniaPropertyAnalyzer hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration } - var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; + var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; // assume that the property is owned by its containing type at the point of assignment, until we find its registration result.SetAssignment(s, new(s.ContainingType, Location.None)); @@ -570,7 +570,7 @@ public partial class AvaloniaPropertyAnalyzer if (_allGetSetMethods.Contains(originalMethod)) { - if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && GetReferencedProperty(invocation.Arguments[0]) is { } refProp && refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) && !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) && @@ -694,11 +694,11 @@ public partial class AvaloniaPropertyAnalyzer void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) { - if (method == null) + if (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Public && method is null) { context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); } - else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) + else if (method is not null && method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) { context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, method.Locations[0], "property accessor", avaloniaPropertyStorage)); } From db72ae64fc6040e4fe09d0ee6fab74038d99a0f1 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 27 Feb 2023 08:37:11 +0100 Subject: [PATCH 19/71] Prevent AssetLoader.GetAssets from crashing --- src/Avalonia.Base/Platform/AssetLoader.cs | 155 ++++++++++++------ .../AssetLoaderTests.cs | 13 +- 2 files changed, 116 insertions(+), 52 deletions(-) diff --git a/src/Avalonia.Base/Platform/AssetLoader.cs b/src/Avalonia.Base/Platform/AssetLoader.cs index 659cfb75df..7df446e854 100644 --- a/src/Avalonia.Base/Platform/AssetLoader.cs +++ b/src/Avalonia.Base/Platform/AssetLoader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -62,7 +63,7 @@ namespace Avalonia.Platform /// True if the asset could be found; otherwise false. public bool Exists(Uri uri, Uri? baseUri = null) { - return GetAsset(uri, baseUri) != null; + return TryGetAsset(uri, baseUri, out _); } /// @@ -94,21 +95,27 @@ namespace Avalonia.Platform /// public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) { - var asset = GetAsset(uri, baseUri); - - if (asset == null) + if (TryGetAsset(uri, baseUri, out var assetDescriptor)) { - throw new FileNotFoundException($"The resource {uri} could not be found."); + return (assetDescriptor.GetStream(), assetDescriptor.Assembly); } - return (asset.GetStream(), asset.Assembly); + throw new FileNotFoundException($"The resource {uri} could not be found."); } public Assembly? GetAssembly(Uri uri, Uri? baseUri) { if (!uri.IsAbsoluteUri && baseUri != null) + { uri = new Uri(baseUri, uri); - return GetAssembly(uri)?.Assembly; + } + + if (TryGetAssembly(uri, out var assemblyDescriptor)) + { + return assemblyDescriptor.Assembly; + } + + return null; } /// @@ -121,99 +128,145 @@ namespace Avalonia.Platform { if (uri.IsAbsoluteResm()) { - var assembly = GetAssembly(uri); + if (!TryGetAssembly(uri, out var assembly)) + { + assembly = _defaultResmAssembly; + } return assembly?.Resources? - .Where(x => x.Key.IndexOf(uri.GetUnescapeAbsolutePath(), StringComparison.Ordinal) >= 0) - .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? - Enumerable.Empty(); + .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) + .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? + Enumerable.Empty(); } uri = uri.EnsureAbsolute(baseUri); + if (uri.IsAvares()) { - var (asm, path) = GetResAsmAndPath(uri); - if (asm == null) + if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) { - throw new ArgumentException( - "No default assembly, entry assembly or explicit assembly specified; " + - "don't know where to look up for the resource, try specifying assembly explicitly."); + return Enumerable.Empty(); } - if (asm.AvaloniaResources == null) + if (assembly?.AvaloniaResources == null) + { return Enumerable.Empty(); + } - if (path[path.Length - 1] != '/') + if (path.Length > 0 && path[path.Length - 1] != '/') + { path += '/'; + } - return asm.AvaloniaResources + return assembly.AvaloniaResources .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) - .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); + .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); } return Enumerable.Empty(); } - - private IAssetDescriptor? GetAsset(Uri uri, Uri? baseUri) - { + + private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) + { + assetDescriptor = null; + if (uri.IsAbsoluteResm()) { - var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; - - if (asm == null) + if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) { - throw new ArgumentException( - "No default assembly, entry assembly or explicit assembly specified; " + - "don't know where to look up for the resource, try specifying assembly explicitly."); + assembly = _defaultResmAssembly; } - var resourceKey = uri.AbsolutePath; - IAssetDescriptor? rv = null; - asm.Resources?.TryGetValue(resourceKey, out rv); - return rv; + if (assembly?.Resources != null) + { + var resourceKey = uri.AbsolutePath; + + if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) + { + return true; + } + } } uri = uri.EnsureAbsolute(baseUri); if (uri.IsAvares()) { - var (asm, path) = GetResAsmAndPath(uri); - if (asm.AvaloniaResources == null) - return null; - asm.AvaloniaResources.TryGetValue(path, out var desc); - return desc; + if (TryGetResAsmAndPath(uri, out var assembly, out var path)) + { + if (assembly.AvaloniaResources == null) + { + return false; + } + + if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) + { + return true; + } + } } - throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); + return false; } - private static (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) + private static bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) { - var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); - return (asm, uri.GetUnescapeAbsolutePath()); + path = uri.GetUnescapeAbsolutePath(); + + if (TryLoadAssembly(uri.Authority, out assembly)) + { + return true; + } + + return false; } - - private static IAssemblyDescriptor? GetAssembly(Uri? uri) + + private static bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) { + assembly = null; + if (uri != null) { if (!uri.IsAbsoluteUri) - return null; - if (uri.IsAvares()) - return GetResAsmAndPath(uri).asm; + { + return false; + } + + if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) + { + return true; + } if (uri.IsResm()) { var assemblyName = uri.GetAssemblyNameFromQuery(); - if (assemblyName.Length > 0) - return s_assemblyDescriptorResolver.GetAssembly(assemblyName); + + if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) + { + return true; + } } } - return null; + return false; + } + + private static bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; + + try + { + assembly = s_assemblyDescriptorResolver.GetAssembly(assemblyName); + + return true; + } + catch (Exception) { } + + return false; } #endif - + public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) diff --git a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs index 28fb19e119..894b6578e3 100644 --- a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs @@ -9,7 +9,7 @@ namespace Avalonia.Base.UnitTests; public class AssetLoaderTests : IDisposable { - public class MockAssembly : Assembly {} + public class MockAssembly : Assembly { } private const string AssemblyNameWithWhitespace = "Awesome Library"; @@ -50,6 +50,17 @@ public class AssetLoaderTests : IDisposable Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName); } + [Fact] + public void Invalid_AssemblyName_Should_Yield_Empty_Enumerable() + { + var uri = new Uri($"avares://InvalidAssembly"); + var loader = new AssetLoader(); + + var assemblyActual = loader.GetAssets(uri, null); + + Assert.Empty(assemblyActual); + } + private static IAssemblyDescriptor CreateAssemblyDescriptor(string assemblyName) { var assembly = Mock.Of(); From cfd35d17d51a0d8c4136f0233285a856773f491e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 27 Feb 2023 11:18:38 +0100 Subject: [PATCH 20/71] Added failing `SetCurrentValue` test. `SetCurrent_Value_Persists_When_Toggling_Style_1` passes but `SetCurrent_Value_Persists_When_Toggling_Style_2` fails as it uses a different code-path. --- .../AvaloniaObjectTests_SetCurrentValue.cs | 81 ++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 8ad36a583e..c850fbdb08 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -1,6 +1,9 @@ using System; +using Avalonia.Controls; using Avalonia.Data; using Avalonia.Diagnostics; +using Avalonia.Styling; +using Avalonia.UnitTests; using Xunit; using Observable = Avalonia.Reactive.Observable; @@ -275,6 +278,79 @@ namespace Avalonia.Base.UnitTests Assert.Equal("style", target.Foo); } + [Fact] + public void SetCurrent_Value_Persists_When_Toggling_Style_1() + { + var target = new Class1(); + var root = new TestRoot(target) + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(Class1.BarProperty, "bar") }, + } + } + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + + target.Classes.Add("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bar", target.Bar); + + target.Classes.Remove("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + } + + [Fact] + public void SetCurrent_Value_Persists_When_Toggling_Style_2() + { + var target = new Class1(); + var root = new TestRoot(target) + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.BarProperty, "bar"), + new Setter(Class1.InheritedProperty, "inherited"), + }, + } + } + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + Assert.Equal("inheriteddefault", target.Inherited); + + target.Classes.Add("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bar", target.Bar); + Assert.Equal("inherited", target.Inherited); + + target.Classes.Remove("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + Assert.Equal("inheriteddefault", target.Inherited); + } + private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) { return target.GetDiagnostic(property).Priority; @@ -285,16 +361,19 @@ namespace Avalonia.Base.UnitTests return target.GetDiagnostic(property).IsOverriddenCurrentValue; } - private class Class1 : AvaloniaObject + private class Class1 : Control { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register(nameof(Foo), "foodefault"); + public static readonly StyledProperty BarProperty = + AvaloniaProperty.Register(nameof(Bar), "bardefault"); public static readonly StyledProperty InheritedProperty = AvaloniaProperty.Register(nameof(Inherited), "inheriteddefault", inherits: true); public static readonly StyledProperty CoercedProperty = AvaloniaProperty.Register(nameof(Coerced), coerce: Coerce); public string Foo => GetValue(FooProperty); + public string Bar => GetValue(BarProperty); public string Inherited => GetValue(InheritedProperty); public double Coerced => GetValue(CoercedProperty); public double CoerceMax { get; set; } = 100; From d84f8225bee13c50060b79304273a1d2fd4ab548 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 27 Feb 2023 11:21:01 +0100 Subject: [PATCH 21/71] Fix value re-evaluation with SetCurrentValue. `SetCurrentValue`'s value was being discarded if a style with more than one setter was was re-evaluated as `ValueStore.ReevaluationEffectiveValues` removed the effective value. --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 0887f11ec9..53cd3ff307 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -924,7 +924,7 @@ namespace Avalonia.PropertyStore { _effectiveValues.GetKeyValue(i, out var key, out var e); - if (e.Priority == BindingPriority.Unset) + if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue) { RemoveEffectiveValue(key, i); e.DisposeAndRaiseUnset(this, key); From d7b6a8e97a6d838f20d418b25e6eff429383d3d0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 27 Feb 2023 18:08:32 +0100 Subject: [PATCH 22/71] Added failing test for #10398. --- .../TreeViewTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 1a9fb33b7e..2ca3495d30 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -100,6 +100,31 @@ namespace Avalonia.Controls.UnitTests Assert.All(items, x => Assert.Same(theme, x.ItemContainerTheme)); } + [Fact] + public void Finds_Correct_DataTemplate_When_Application_DataTemplate_Is_Present() + { + // #10398 + using var app = UnitTestApplication.Start(); + + Avalonia.Application.Current.DataTemplates.Add(new FuncDataTemplate((x, _) => new Canvas())); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Avalonia.Application.Current); + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = CreateTestTreeData(), + }; + + var root = new TestRoot(target); + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); + Assert.Equal(new[] { "Child1", "Child2", "Child3" }, ExtractItemHeader(target, 1)); + Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2)); + } + [Fact] public void Root_ItemContainerGenerator_Containers_Should_Be_Root_Containers() { From 49c57a610c891d87315d4e247196ef948cd816cd Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Mon, 27 Feb 2023 20:18:44 +0100 Subject: [PATCH 23/71] Make `Avalonia.Win32.Input.KeyInterop` public --- src/Windows/Avalonia.Win32/Input/KeyInterop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs index f5b2d462ab..1ab4c0d2dc 100644 --- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs +++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs @@ -4,7 +4,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input { - static class KeyInterop + public static class KeyInterop { private static readonly Dictionary s_virtualKeyFromKey = new Dictionary { From d08083bbf32dcad732680e8d05386cd18b168cee Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Feb 2023 09:27:14 +0100 Subject: [PATCH 24/71] Introduce font collections --- samples/ControlCatalog.NetCore/Program.cs | 2 + .../ControlCatalog/Pages/ComboBoxPage.xaml.cs | 2 +- src/Avalonia.Base/Media/FontManager.cs | 109 +++++-- .../Media/Fonts/EmbeddedFontCollection.cs | 298 ++++++++++++++++++ .../Media/Fonts/FontCollectionKey.cs | 4 + .../Media/Fonts/FontFamilyLoader.cs | 58 ++-- .../Media/Fonts/IFontCollection.cs | 17 + .../Media/Fonts/SystemFontCollection.cs | 100 ++++++ src/Avalonia.Base/Media/IGlyphTypeface.cs | 20 ++ .../Media/TextFormatting/TextCharacters.cs | 15 +- src/Avalonia.Base/Media/Typeface.cs | 13 +- .../Platform/IFontManagerImpl.cs | 30 +- src/Avalonia.Base/Utilities/UriExtensions.cs | 5 +- src/Avalonia.Controls/AppBuilder.cs | 46 ++- .../InterFontCollection.cs | 14 + .../HeadlessPlatformStubs.cs | 29 +- src/Avalonia.Themes.Fluent/Accents/Base.xaml | 2 +- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 85 +++-- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 14 + .../Avalonia.Skia/SKTypefaceCollection.cs | 198 ------------ .../SKTypefaceCollectionCache.cs | 73 ----- .../Media/DWriteResourceFontLoader.cs | 13 +- .../Media/Direct2D1FontCollectionCache.cs | 13 +- .../Media/FontManagerImpl.cs | 57 +++- .../Media/GlyphTypefaceImpl.cs | 20 +- .../Media/FontManagerTests.cs | 6 +- .../Media/Fonts/FontFamilyLoaderTests.cs | 16 +- .../Media/FontManagerImplTests.cs | 66 ++-- .../Media/CustomFontManagerImpl.cs | 116 ++++++- .../Media/EmbeddedFontCollectionTests.cs | 58 ++++ .../Media/FontManagerImplTests.cs | 79 ++--- .../Media/SKTypefaceCollectionCacheTests.cs | 63 ---- .../HarfBuzzFontManagerImpl.cs | 37 +-- .../HarfBuzzGlyphTypefaceImpl.cs | 9 + .../Avalonia.UnitTests/MockFontManagerImpl.cs | 20 +- tests/Avalonia.UnitTests/MockGlyphTypeface.cs | 8 + 36 files changed, 1059 insertions(+), 656 deletions(-) create mode 100644 src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs create mode 100644 src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs create mode 100644 src/Avalonia.Base/Media/Fonts/IFontCollection.cs create mode 100644 src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs create mode 100644 src/Avalonia.Fonts.Inter/InterFontCollection.cs delete mode 100644 src/Skia/Avalonia.Skia/SKTypefaceCollection.cs delete mode 100644 src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs create mode 100644 tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs delete mode 100644 tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index e55f003133..a86d311f4e 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Fonts.Inter; using Avalonia.Headless; using Avalonia.LogicalTree; using Avalonia.Threading; @@ -124,6 +125,7 @@ namespace ControlCatalog.NetCore EnableIme = true }) .UseSkia() + .WithFonts(new InterFontCollection()) .AfterSetup(builder => { builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 6d624c9a07..6d759597b5 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Get("fontComboBox"); - fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); + fontComboBox.Items = FontManager.Current.SystemFonts; fontComboBox.SelectedIndex = 0; } } diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 2dabb29e76..27e25cf359 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Avalonia.Media.Fonts; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -13,9 +15,10 @@ namespace Avalonia.Media /// public sealed class FontManager { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - private readonly FontFamily _defaultFontFamily; + public const string FontCollectionScheme = "fonts"; + + private readonly SystemFontCollection _systemFonts; + private readonly ConcurrentDictionary _fontCollections = new ConcurrentDictionary(); private readonly IReadOnlyList? _fontFallbacks; public FontManager(IFontManagerImpl platformImpl) @@ -33,7 +36,7 @@ namespace Avalonia.Media throw new InvalidOperationException("Default font family name can't be null or empty."); } - _defaultFontFamily = new FontFamily(DefaultFontFamilyName); + _systemFonts = new SystemFontCollection(this); } public static FontManager Current @@ -57,11 +60,6 @@ namespace Avalonia.Media } } - /// - /// - /// - public IFontManagerImpl PlatformImpl { get; } - /// /// Gets the system's default font family's name. /// @@ -71,41 +69,92 @@ namespace Avalonia.Media } /// - /// Get all installed font family names. + /// Get all system fonts. /// - /// If true the font collection is updated. - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); + public IFontCollection SystemFonts => _systemFonts; + + internal IFontManagerImpl PlatformImpl { get; } /// - /// Returns a new , or an existing one if a matching exists. + /// Tries to get a glyph typeface for specified typeface. /// /// The typeface. + /// The created glyphTypeface /// - /// The . + /// True, if the could create the glyph typeface, False otherwise. /// - public IGlyphTypeface GetOrAddGlyphTypeface(Typeface typeface) + public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - while (true) + glyphTypeface = null; + + var fontFamily = typeface.FontFamily; + + if (fontFamily.Key is FontFamilyKey key) { - if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface)) + var source = key.Source; + + if (!source.IsAbsoluteUri) { - return glyphTypeface; + if (key.BaseUri == null) + { + throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null."); + } + + source = new Uri(key.BaseUri, source); } - glyphTypeface = PlatformImpl.CreateGlyphTypeface(typeface); + if (!_fontCollections.TryGetValue(source, out var fontCollection)) + { + var embeddedFonts = new EmbeddedFontCollection(source, source); + + embeddedFonts.Initialize(PlatformImpl); + + if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts)) + { + fontCollection = embeddedFonts; + } + } - if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface)) + if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, + typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { - return glyphTypeface; + return true; } + } - if (typeface.FontFamily == _defaultFontFamily) + foreach (var familyName in fontFamily.FamilyNames) + { + if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { - throw new InvalidOperationException($"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + return true; } + } + + return false; + } + + public void AddFontCollection(IFontCollection fontCollection) + { + var key = fontCollection.Key; + + if (!fontCollection.Key.IsFontCollection()) + { + throw new ArgumentException(nameof(fontCollection), "Font collection Key should follow the fontCollection: scheme."); + } - typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight); + if (!_fontCollections.TryAdd(key, fontCollection)) + { + throw new ArgumentException(nameof(fontCollection), "Font collection is already registered."); + } + + fontCollection.Initialize(PlatformImpl); + } + + public void RemoveFontCollection(Uri key) + { + if (_fontCollections.TryRemove(key, out var fontCollection)) + { + fontCollection.Dispose(); } } @@ -123,18 +172,16 @@ namespace Avalonia.Media /// True, if the could match the character to specified parameters, False otherwise. /// public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, - FontStretch fontStretch, - FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) + FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) { - if(_fontFallbacks != null) + if (_fontFallbacks != null) { foreach (var fallback in _fontFallbacks) { typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); - var glyphTypeface = GetOrAddGlyphTypeface(typeface); - - if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){ + if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { return true; } } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs new file mode 100644 index 0000000000..c242ed18db --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Media.Fonts +{ + public class EmbeddedFontCollection : IFontCollection + { + private readonly Dictionary> _glyphTypefaceCache = + new Dictionary>(); + + private readonly List _fontFamilies = new List(1); + + private readonly Uri _key; + + private readonly Uri _source; + + public EmbeddedFontCollection(Uri key, Uri source) + { + _key = key; + + if(!source.IsAvares() && !source.IsAbsoluteResm()) + { + throw new ArgumentOutOfRangeException(nameof(source), "Specified source uri does not follow the resm: or avares: scheme."); + } + + _source = source; + } + + public Uri Key => _key; + + public FontFamily this[int index] => _fontFamilies[index]; + + public int Count => _fontFamilies.Count; + + public void Initialize(IFontManagerImpl fontManager) + { + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var fontAssets = FontFamilyLoader.LoadFontAssets(_source); + + foreach (var fontAsset in fontAssets) + { + var stream = assetLoader.Open(fontAsset); + + if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface)) + { + if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) + { + glyphTypefaces = new Dictionary(); + + _glyphTypefaceCache.Add(glyphTypeface.FamilyName, glyphTypefaces); + + _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); + } + + var key = new FontCollectionKey( + glyphTypeface.Style, + glyphTypeface.Weight, + glyphTypeface.Stretch); + + if (!glyphTypefaces.ContainsKey(key)) + { + glyphTypefaces.Add(key, glyphTypeface); + } + } + } + } + + public void Dispose() + { + foreach (var fontFamily in _fontFamilies) + { + if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces)) + { + foreach (var glyphTypeface in glyphTypefaces.Values) + { + glyphTypeface.Dispose(); + } + } + } + + GC.SuppressFinalize(this); + } + + public IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + var key = new FontCollectionKey(style, weight, stretch); + + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + } + + //Try to find a partially matching font + for (var i = 0; i < Count; i++) + { + var fontFamily = _fontFamilies[i]; + + if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture))) + { + if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) && + TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + } + } + + glyphTypeface = null; + + return false; + } + + private static bool TryGetNearestMatch( + Dictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + { + return true; + } + + if (key.Style != FontStyle.Normal) + { + key = key with { Style = FontStyle.Normal }; + } + + if (key.Stretch != FontStretch.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (key.Weight != FontWeight.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) + { + return true; + } + } + + key = key with { Stretch = FontStretch.Normal }; + } + + if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + //Take the first glyph typeface we can find. + foreach (var typeface in glyphTypefaces.Values) + { + glyphTypeface = typeface; + + return true; + } + + return false; + } + + private static bool TryFindStretchFallback( + Dictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = null; + + var stretch = (int)key.Stretch; + + if (stretch < 5) + { + for (var i = 0; stretch + i < 9; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface)) + { + return true; + } + } + } + else + { + for (var i = 0; stretch - i > 1; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface)) + { + return true; + } + } + } + + return false; + } + + private static bool TryFindWeightFallback( + Dictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? typeface) + { + typeface = null; + var weight = (int)key.Weight; + + //If the target weight given is between 400 and 500 inclusive + if (weight >= 400 && weight <= 500) + { + //Look for available weights between the target and 500, in ascending order. + for (var i = 0; weight + i <= 500; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights greater than 500, in ascending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + } + + //If a weight less than 400 is given, look for available weights less than the target, in descending order. + if (weight < 400) + { + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + } + + //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. + if (weight > 500) + { + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs new file mode 100644 index 0000000000..0d0dc3016e --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs @@ -0,0 +1,4 @@ +namespace Avalonia.Media.Fonts +{ + public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch); +} diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs index 365fb6e412..39e80415fb 100644 --- a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs @@ -11,22 +11,22 @@ namespace Avalonia.Media.Fonts /// /// Loads all font assets that belong to the specified /// - /// + /// /// - public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey) => - IsFontTtfOrOtf(fontFamilyKey.Source) ? - GetFontAssetsByExpression(fontFamilyKey) : - GetFontAssetsBySource(fontFamilyKey); + public static IEnumerable LoadFontAssets(Uri source) => + IsFontTtfOrOtf(source) ? + GetFontAssetsByExpression(source) : + GetFontAssetsBySource(source); /// /// Searches for font assets at a given location and returns a quantity of found assets /// - /// + /// /// - private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) + private static IEnumerable GetFontAssetsBySource(Uri source) { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); + var availableAssets = assetLoader.GetAssets(source, null); return availableAssets.Where(x => IsFontTtfOrOtf(x)); } @@ -34,60 +34,50 @@ namespace Avalonia.Media.Fonts /// Searches for font assets at a given location and only accepts assets that fit to a given filename expression. /// File names can target multiple files with * wildcard. For example "FontFile*.ttf" /// - /// + /// /// - private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) + private static IEnumerable GetFontAssetsByExpression(Uri source) { - var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location); - var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension); + var (fileNameWithoutExtension, extension) = GetFileName(source, out var location); + var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension); var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri); + var availableResources = assetLoader.GetAssets(location, null); return availableResources.Where(x => IsContainsFile(x, filePattern, extension)); } private static (string fileNameWithoutExtension, string extension) GetFileName( - FontFamilyKey fontFamilyKey, out Uri location) + Uri source, out Uri location) { - if (fontFamilyKey.Source.IsAbsoluteResm()) + if (source.IsAbsoluteResm()) { - var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.'); + var fileName = GetFileNameAndExtension(source.GetUnescapeAbsolutePath(), '.'); - var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri() + var uriLocation = source.GetUnescapeAbsoluteUri() .Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty); location = new Uri(uriLocation, UriKind.RelativeOrAbsolute); return fileName; } - var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString); + var filename = GetFileNameAndExtension(source.OriginalString); var fullFilename = filename.fileNameWithoutExtension + filename.extension; - if (fontFamilyKey.BaseUri != null) - { - var relativePath = fontFamilyKey.Source.OriginalString - .Replace(fullFilename, string.Empty); - - location = new Uri(fontFamilyKey.BaseUri, relativePath); - } - else - { - var uriString = fontFamilyKey.Source - .GetUnescapeAbsoluteUri() - .Replace(fullFilename, string.Empty); - location = new Uri(uriString); - } + var uriString = source + .GetUnescapeAbsoluteUri() + .Replace(fullFilename, string.Empty); + location = new Uri(uriString); return filename; } private static string CreateFilePattern( - FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension) + Uri source, Uri location, string fileNameWithoutExtension) { var path = location.GetUnescapeAbsolutePath(); var file = GetSubString(fileNameWithoutExtension, '*'); - return fontFamilyKey.Source.IsAbsoluteResm() + return source.IsAbsoluteResm() ? path + "." + file : path + file; } diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs new file mode 100644 index 0000000000..27b3378513 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + public interface IFontCollection : IReadOnlyList, IDisposable + { + Uri Key { get; } + + void Initialize(IFontManagerImpl fontManager); + + bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + } +} diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs new file mode 100644 index 0000000000..ce0deb21e4 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + internal class SystemFontCollection : IFontCollection + { + private readonly Dictionary> _glyphTypefaceCache = + new Dictionary>(); + + private readonly FontManager _fontManager; + private readonly string[] _familyNames; + + public SystemFontCollection(FontManager fontManager) + { + _fontManager = fontManager; + _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames(); + } + + public Uri Key => new Uri("fontCollection:SystemFonts"); + + public FontFamily this[int index] + { + get + { + var familyName = _familyNames[index]; + + return new FontFamily(familyName); + } + } + + public int Count => _familyNames.Length; + + public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (familyName == FontFamily.DefaultFontFamilyName) + { + familyName = _fontManager.DefaultFontFamilyName; + } + + var key = new FontCollectionKey(style, weight, stretch); + + if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + glyphTypefaces = new Dictionary(); + + _glyphTypefaceCache.Add(familyName, glyphTypefaces); + } + + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + { + return true; + } + + if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + { + glyphTypefaces.Add(key, glyphTypeface); + + return true; + } + + return false; + } + + public void Initialize(IFontManagerImpl fontManager) + { + //We initialize the system font collection during construction. + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + foreach (var familyName in _familyNames) + { + yield return new FontFamily(familyName); + } + } + + void IDisposable.Dispose() + { + foreach (var glyphTypefaces in _glyphTypefaceCache.Values) + { + foreach (var pair in glyphTypefaces) + { + pair.Value.Dispose(); + } + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Avalonia.Base/Media/IGlyphTypeface.cs b/src/Avalonia.Base/Media/IGlyphTypeface.cs index 9e1e52cb73..09740aac81 100644 --- a/src/Avalonia.Base/Media/IGlyphTypeface.cs +++ b/src/Avalonia.Base/Media/IGlyphTypeface.cs @@ -6,6 +6,26 @@ namespace Avalonia.Media [Unstable] public interface IGlyphTypeface : IDisposable { + /// + /// Gets the family name for the object. + /// + string FamilyName { get; } + + /// + /// Gets the designed weight of the font represented by the object. + /// + FontWeight Weight { get; } + + /// + /// Gets the style for the object. + /// + FontStyle Style { get; } + + /// + /// Gets the value for the object. + /// + FontStretch Stretch { get; } + /// /// Gets the number of glyphs held by this glyph typeface. /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index b4734d702b..253c7075fa 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -122,13 +122,14 @@ namespace Avalonia.Media.TextFormatting if (matchFound) { // Fallback found - var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - - if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) - { - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), - biDiLevel); - } + if(fontManager.TryGetGlyphTypeface(fallbackTypeface, out var fallbackGlyphTypeface)) + { + if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) + { + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), + biDiLevel); + } + } } // no fallback found diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index 1e744c30c8..e2729c9158 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/src/Avalonia.Base/Media/Typeface.cs @@ -80,7 +80,18 @@ namespace Avalonia.Media /// /// The glyph typeface. /// - public IGlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this); + public IGlyphTypeface GlyphTypeface + { + get + { + if(FontManager.Current.TryGetGlyphTypeface(this, out var glyphTypeface)) + { + return glyphTypeface; + } + + throw new InvalidOperationException("Could not create glyphTypeface."); + } + } public static bool operator !=(Typeface a, Typeface b) { diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index cd6e64abaf..116f7cd6e2 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Metadata; @@ -17,7 +18,7 @@ namespace Avalonia.Platform /// Get all installed fonts in the system. /// If true the font collection is updated. /// - IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + string[] GetInstalledFontFamilyNames(bool checkForUpdates = false); /// /// Tries to match a specified character to a typeface that supports specified font properties. @@ -37,12 +38,27 @@ namespace Avalonia.Platform FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface); /// - /// Creates a glyph typeface. + /// Tries to get a glyph typeface for specified parameters. /// - /// The typeface. - /// 0 - /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface. + /// The family name. + /// The font style. + /// The font weiht. + /// The font stretch. + /// The created glyphTypeface + /// + /// True, if the could create the glyph typeface, False otherwise. + /// + bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); + + /// + /// Tries to create a glyph typeface from specified stream. + /// + /// A stream that holds the font's data. + /// The created glyphTypeface + /// + /// True, if the could create the glyph typeface, False otherwise. /// - IGlyphTypeface CreateGlyphTypeface(Typeface typeface); + bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface); } } diff --git a/src/Avalonia.Base/Utilities/UriExtensions.cs b/src/Avalonia.Base/Utilities/UriExtensions.cs index c706f72a63..1f9c694eab 100644 --- a/src/Avalonia.Base/Utilities/UriExtensions.cs +++ b/src/Avalonia.Base/Utilities/UriExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media; namespace Avalonia.Utilities; @@ -10,7 +11,9 @@ internal static class UriExtensions public static bool IsResm(this Uri uri) => uri.Scheme == "resm"; public static bool IsAvares(this Uri uri) => uri.Scheme == "avares"; - + + public static bool IsFontCollection(this Uri uri) => uri.Scheme == FontManager.FontCollectionScheme; + public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri) { if (uri.IsAbsoluteUri) diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index cf79fcd1a8..9e1222de6d 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -4,6 +4,9 @@ using System.Reflection; using System.Linq; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; +using Avalonia.Media.Fonts; +using Avalonia.Media; +using System.Xml.Linq; namespace Avalonia { @@ -16,7 +19,7 @@ namespace Avalonia private Action? _optionsInitializers; private Func? _appFactory; private IApplicationLifetime? _lifetime; - + /// /// Gets or sets the instance. /// @@ -31,12 +34,12 @@ namespace Avalonia /// Gets the instance being initialized. /// public Application? Instance { get; private set; } - + /// /// Gets the type of the Instance (even if it's not created yet) /// public Type? ApplicationType { get; private set; } - + /// /// Gets or sets a method to call the initialize the windowing subsystem. /// @@ -64,7 +67,7 @@ namespace Avalonia public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; - + /// /// Initializes a new instance of the class. /// @@ -73,7 +76,7 @@ namespace Avalonia builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly)) { } - + /// /// Initializes a new instance of the class. /// @@ -123,8 +126,8 @@ namespace Avalonia AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); return Self; } - - + + public AppBuilder AfterPlatformServicesSetup(Action callback) { AfterPlatformServicesSetupCallback = (Action)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); @@ -132,7 +135,7 @@ namespace Avalonia } public delegate void AppMainDelegate(Application app, string[] args); - + public void Start(AppMainDelegate main, string[] args) { Setup(); @@ -160,7 +163,7 @@ namespace Avalonia Setup(); return Self; } - + /// /// Specifies a windowing subsystem to use. /// @@ -195,7 +198,7 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); }; return Self; } - + /// /// Configures platform-specific options /// @@ -204,7 +207,28 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToFunc(options); }; return Self; } - + + /// + /// Registers a custom font collection with the font manager. + /// + /// The font collection. + /// An instance. + /// + public AppBuilder WithFonts(IFontCollection fontCollection) + { + if(fontCollection == null) + { + throw new ArgumentNullException(nameof(fontCollection), "Font collection can't be null."); + } + + return AfterSetup(appBuilder => + { + var fontManager = FontManager.Current; + + fontManager.AddFontCollection(fontCollection); + }); + } + /// /// Sets up the platform-specific services for the . /// diff --git a/src/Avalonia.Fonts.Inter/InterFontCollection.cs b/src/Avalonia.Fonts.Inter/InterFontCollection.cs new file mode 100644 index 0000000000..0ed1779a03 --- /dev/null +++ b/src/Avalonia.Fonts.Inter/InterFontCollection.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Media.Fonts; + +namespace Avalonia.Fonts.Inter +{ + public sealed class InterFontCollection : EmbeddedFontCollection + { + public InterFontCollection() : base( + new Uri("fonts:Inter", UriKind.Absolute), + new Uri("avares://Avalonia.Fonts.Inter/Assets", UriKind.Absolute)) + { + } + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 46e3515d11..ee4cd5af98 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -84,6 +84,14 @@ namespace Avalonia.Headless public FontSimulations FontSimulations { get; } + public string FamilyName => "Arial"; + + public FontWeight Weight => FontWeight.Normal; + + public FontStyle Style => FontStyle.Normal; + + public FontStretch Stretch => FontStretch.Normal; + public void Dispose() { } @@ -147,19 +155,28 @@ namespace Avalonia.Headless class HeadlessFontManagerStub : IFontManagerImpl { - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public string GetDefaultFontFamilyName() { - return new HeadlessGlyphTypefaceImpl(); + return "Arial"; } - public string GetDefaultFontFamilyName() + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { - return "Arial"; + return new string[] { "Arial" }; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface) { - return new List { "Arial" }; + glyphTypeface= new HeadlessGlyphTypefaceImpl(); + + return true; + } + + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + { + glyphTypeface = new HeadlessGlyphTypefaceImpl(); + + return true; } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 82e48851b5..c19a4f5c09 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -3,7 +3,7 @@ xmlns:sys="using:System" xmlns:converters="using:Avalonia.Controls.Converters"> - avares://Avalonia.Fonts.Inter/Assets#Inter, $Default + fonts:Inter#Inter, $Default 14 diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 90a2f9169b..29e5687423 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,6 +1,7 @@ using System; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -16,14 +17,14 @@ namespace Avalonia.Skia return SKTypeface.Default.FamilyName; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { if (checkForUpdates) { _skFontManager = SKFontManager.CreateDefault(); } - return _skFontManager.FontFamilies; + return _skFontManager.GetFontFamilies(); } [ThreadStatic] private static string[]? t_languageTagBuffer; @@ -95,72 +96,58 @@ namespace Avalonia.Skia return false; } - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - SKTypeface? skTypeface = null; + glyphTypeface = null; - if(typeface.FontFamily.Key is not null) - { - var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - - skTypeface = fontCollection.Get(typeface); + var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch, + (SKFontStyleSlant)style); - if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks) - { - throw new InvalidOperationException( - $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); - } - } + var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle); if (skTypeface is null) { - var defaultName = SKTypeface.Default.FamilyName; - - var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, (SKFontStyleWidth)typeface.Stretch, - (SKFontStyleSlant)typeface.Style); - - foreach (var familyName in typeface.FontFamily.FamilyNames) - { - if(familyName == FontFamily.DefaultFontFamilyName) - { - continue; - } - - skTypeface = _skFontManager.MatchFamily(familyName, fontStyle); - - if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)) - { - continue; - } - - break; - } - - // MatchTypeface can return "null" if matched typeface wasn't found for the style - // Fallback to the default typeface and styles instead. - skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle) - ?? SKTypeface.Default; + return false; } - - if (skTypeface == null) + + //MatchFamily can return a font other than we requested so we have to verify we got the expected. + if (!skTypeface.FamilyName.ToLower(CultureInfo.InvariantCulture).Equals(familyName.ToLower(CultureInfo.InvariantCulture), StringComparison.Ordinal)) { - throw new InvalidOperationException( - $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + return false; } var fontSimulations = FontSimulations.None; - if((int)typeface.Weight >= 600 && !skTypeface.IsBold) + if ((int)weight >= 600 && !skTypeface.IsBold) { fontSimulations |= FontSimulations.Bold; } - if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic) + if (style == FontStyle.Italic && !skTypeface.IsItalic) { fontSimulations |= FontSimulations.Oblique; } - return new GlyphTypefaceImpl(skTypeface, fontSimulations); + glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations); + + return true; + } + + public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + var skTypeface = SKTypeface.FromStream(stream); + + if (skTypeface != null) + { + glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + + return true; + } + + glyphTypeface = null; + + return false; } } } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 3093455bec..43e10e3e96 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -51,6 +51,12 @@ namespace Avalonia.Skia GlyphCount = Typeface.GlyphCount; FontSimulations = fontSimulations; + + Weight = (FontWeight)Typeface.FontWeight; + + Style = Typeface.FontSlant.ToAvalonia(); + + Stretch = (FontStretch)Typeface.FontStyle.Width; } public Face Face { get; } @@ -67,6 +73,14 @@ namespace Avalonia.Skia public int GlyphCount { get; } + public string FamilyName => Typeface.FamilyName; + + public FontWeight Weight { get; } + + public FontStyle Style { get; } + + public FontStretch Stretch { get; } + public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) { metrics = default; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs deleted file mode 100644 index 9ee17a09d6..0000000000 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal class SKTypefaceCollection - { - private readonly ConcurrentDictionary _typefaces = new(); - - public void AddTypeface(Typeface key, SKTypeface typeface) - { - _typefaces.TryAdd(key, typeface); - } - - public SKTypeface? Get(Typeface typeface) - { - return GetNearestMatch(typeface); - } - - private SKTypeface? GetNearestMatch(Typeface key) - { - if (_typefaces.Count == 0) - { - return null; - } - - if (_typefaces.TryGetValue(key, out var typeface)) - { - return typeface; - } - - if(key.Style != FontStyle.Normal) - { - key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch); - } - - if(key.Stretch != FontStretch.Normal) - { - if(TryFindStretchFallback(key, out typeface)) - { - return typeface; - } - - if(key.Weight != FontWeight.Normal) - { - if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface)) - { - return typeface; - } - } - - key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal); - } - - if(TryFindWeightFallback(key, out typeface)) - { - return typeface; - } - - if (TryFindStretchFallback(key, out typeface)) - { - return typeface; - } - - //Nothing was found so we try some regular typeface. - if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface)) - { - return typeface; - } - - SKTypeface? skTypeface = null; - - foreach(var pair in _typefaces) - { - skTypeface = pair.Value; - - if (skTypeface.FamilyName.Contains(key.FontFamily.Name)) - { - return skTypeface; - } - } - - return skTypeface; - } - - private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) - { - typeface = null; - var stretch = (int)key.Stretch; - - if (stretch < 5) - { - for (var i = 0; stretch + i < 9; i++) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface)) - { - return true; - } - } - } - else - { - for (var i = 0; stretch - i > 1; i++) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface)) - { - return true; - } - } - } - - return false; - } - - private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) - { - typeface = null; - var weight = (int)key.Weight; - - //If the target weight given is between 400 and 500 inclusive - if (weight >= 400 && weight <= 500) - { - //Look for available weights between the target and 500, in ascending order. - for (var i = 0; weight + i <= 500; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights greater than 500, in ascending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - } - - //If a weight less than 400 is given, look for available weights less than the target, in descending order. - if (weight < 400) - { - for (var i = 0; weight - i >= 100; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - } - - //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. - if (weight > 500) - { - for (var i = 0; weight + i <= 900; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface)) - { - return true; - } - } - } - - return false; - } - } -} diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs deleted file mode 100644 index d064f49ae4..0000000000 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Concurrent; -using Avalonia.Media; -using Avalonia.Media.Fonts; -using Avalonia.Platform; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal static class SKTypefaceCollectionCache - { - private static readonly ConcurrentDictionary s_cachedCollections; - - static SKTypefaceCollectionCache() - { - s_cachedCollections = new ConcurrentDictionary(); - } - - /// - /// Gets the or add typeface collection. - /// - /// The font family. - /// - public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) - { - return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection); - } - - /// - /// Creates the custom font collection. - /// - /// The font family. - /// - private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily) - { - var typeFaceCollection = new SKTypefaceCollection(); - - if (fontFamily.Key is not { } fontFamilyKey) - { - return typeFaceCollection; - } - - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey); - - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - - foreach (var asset in fontAssets) - { - var assetStream = assetLoader.Open(asset); - - if (assetStream == null) - throw new InvalidOperationException("Asset could not be loaded."); - - var typeface = SKTypeface.FromStream(assetStream); - - if (typeface == null) - throw new InvalidOperationException("Typeface could not be loaded."); - - if (!typeface.FamilyName.Contains(fontFamily.Name)) - { - continue; - } - - var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(), - (FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth); - - typeFaceCollection.AddTypeface(key, typeface); - } - - return typeFaceCollection; - } - } -} diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs index 4663a6561f..b60962a091 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; -using Avalonia.Platform; using SharpDX; using SharpDX.DirectWrite; namespace Avalonia.Direct2D1.Media { - using System; + using System.IO; internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader { @@ -18,19 +17,15 @@ namespace Avalonia.Direct2D1.Media /// /// The factory. /// - public DWriteResourceFontLoader(Factory factory, IEnumerable fontAssets) + public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets) { var factory1 = factory; - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - foreach (var asset in fontAssets) { - var assetStream = assetLoader.Open(asset); - - var dataStream = new DataStream((int)assetStream.Length, true, true); + var dataStream = new DataStream((int)asset.Length, true, true); - assetStream.CopyTo(dataStream); + asset.CopyTo(dataStream); dataStream.Position = 0; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index 792bf2d0be..ad2ede3a91 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -6,6 +6,9 @@ using FontFamily = Avalonia.Media.FontFamily; using FontStyle = SharpDX.DirectWrite.FontStyle; using FontWeight = SharpDX.DirectWrite.FontWeight; using FontStretch = SharpDX.DirectWrite.FontStretch; +using Avalonia.Platform; +using System.Linq; +using System; namespace Avalonia.Direct2D1.Media { @@ -53,9 +56,15 @@ namespace Avalonia.Direct2D1.Media private static FontCollection CreateFontCollection(FontFamilyKey key) { - var assets = FontFamilyLoader.LoadFontAssets(key); + var source = key.BaseUri != null ? new Uri(key.BaseUri, key.Source) : key.Source; - var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets); + var assets = FontFamilyLoader.LoadFontAssets(source); + + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var fontAssets = assets.Select(x => assetLoader.Open(x)).ToArray(); + + var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, fontAssets); return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index b98ed3ffe6..ec2f6385da 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Platform; -using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; using FontStretch = Avalonia.Media.FontStretch; using FontStyle = Avalonia.Media.FontStyle; @@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.Media return "Segoe UI"; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -62,9 +62,56 @@ namespace Avalonia.Direct2D1.Media return false; } - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) { - return new GlyphTypefaceImpl(typeface); + var systemFonts = Direct2D1FontCollectionCache.InstalledFontCollection; + + if (familyName == FontFamily.DefaultFontFamilyName) + { + familyName = "Segoe UI"; + } + + if (systemFonts.FindFamilyName(familyName, out var index)) + { + var font = systemFonts.GetFontFamily(index).GetFirstMatchingFont( + (SharpDX.DirectWrite.FontWeight)weight, + (SharpDX.DirectWrite.FontStretch)stretch, + (SharpDX.DirectWrite.FontStyle)style); + + glyphTypeface = new GlyphTypefaceImpl(font); + + return true; + } + + glyphTypeface = null; + + return false; + } + + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + { + var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream }); + + var fontCollection = new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); + + if (fontCollection.FontFamilyCount > 0) + { + var fontFamily = fontCollection.GetFontFamily(0); + + if (fontFamily.FontCount > 0) + { + var font = fontFamily.GetFont(0); + + glyphTypeface = new GlyphTypefaceImpl(font); + + return true; + } + } + + glyphTypeface = null; + + return false; } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs index e4988322e7..01add0f0cb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs @@ -12,9 +12,9 @@ namespace Avalonia.Direct2D1.Media { private bool _isDisposed; - public GlyphTypefaceImpl(Typeface typeface) + public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font) { - DWFont = Direct2D1FontCollectionCache.GetFont(typeface); + DWFont = font; FontFace = new FontFace(DWFont).QueryInterface(); @@ -48,6 +48,14 @@ namespace Avalonia.Direct2D1.Media StrikethroughThickness = strikethroughThickness, IsFixedPitch = FontFace.IsMonospacedFont }; + + FamilyName = DWFont.FontFamily.FamilyNames.GetString(0); + + Weight = (Avalonia.Media.FontWeight)DWFont.Weight; + + Style = (Avalonia.Media.FontStyle)DWFont.Style; + + Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch; } private Blob GetTable(Face face, Tag tag) @@ -83,6 +91,14 @@ namespace Avalonia.Direct2D1.Media public FontSimulations FontSimulations => FontSimulations.None; + public string FamilyName { get; } + + public Avalonia.Media.FontWeight Weight { get; } + + public Avalonia.Media.FontStyle Style { get; } + + public Avalonia.Media.FontStretch Stretch { get; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index 11ecac0039..89e609eb10 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs @@ -16,9 +16,11 @@ namespace Avalonia.Base.UnitTests.Media var typeface = new Typeface(fontFamily); - var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface); + Assert.True(FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface)); - Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface)); + FontManager.Current.TryGetGlyphTypeface(typeface, out var other); + + Assert.Same(glyphTypeface, other); } } diff --git a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs index afc25ab88e..82dcd8f4fc 100644 --- a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs @@ -46,9 +46,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Single_FontAsset() { var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(source); Assert.Single(fontAssets); } @@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Single_FontAsset_Avares_Without_BaseUri() { var source = new Uri(AssetYourFontAvares); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(source); Assert.Single(fontAssets); } @@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts { var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute); var baseUri = new Uri(AssetLocationAvares); - var key = new FontFamilyKey(source, baseUri); - var fontAssets = FontFamilyLoader.LoadFontAssets(key); + var fontAssets = FontFamilyLoader.LoadFontAssets(new Uri(baseUri, source)); Assert.Single(fontAssets); } @@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts public void Should_Load_Matching_Assets() { var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute); - var key = new FontFamilyKey(source); - var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray(); + var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray(); foreach (var fontAsset in fontAssets) { @@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var fontFamily = new FontFamily("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono"); + var source = new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono", UriKind.RelativeOrAbsolute); - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key).ToArray(); + var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray(); Assert.NotEmpty(fontAssets); diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index c6ecc0a7e5..c50f31a834 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -1,4 +1,5 @@ -using Avalonia.Direct2D1.Media; +using System; +using Avalonia.Direct2D1.Media; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"))); - - var font = glyphTypeface.DWFont; - - Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); - - Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + var glyphTypeface = + new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface; - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal("Arial", glyphTypeface.FamilyName); } } @@ -38,42 +31,29 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold)); + var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface; - var font = glyphTypeface.DWFont; + Assert.Equal("Arial", glyphTypeface.FamilyName); - Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal(FontWeight.Bold, glyphTypeface.Weight); - Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight); - - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Equal(FontStyle.Normal, glyphTypeface.Style); } } [Fact] - public void Should_Create_Typeface_For_Unknown_Font() + public void Should_Throw_InvalidOperationException_For_Unknown_Font() { using (AvaloniaLocator.EnterScope()) { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("Unknown"))); - - var font = glyphTypeface.DWFont; - - var defaultName = fontManager.GetDefaultFontFamilyName(); - - Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0)); - - Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + var fontManager = FontManager.Current; - Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + Assert.Throws(() => + { + var glyphTypeface =new Typeface(new FontFamily("Unknown")).GlyphTypeface; + }); } } @@ -86,12 +66,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media var fontManager = new FontManagerImpl(); - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri)); + var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface; - var font = glyphTypeface.DWFont; - - Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } @@ -102,14 +79,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media { Direct2D1Platform.Initialize(); - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black)); - - var font = glyphTypeface.DWFont; + var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface; - Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 5a6d7f2cdf..e18344580b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; +using System; using System.Globalization; using System.Linq; using Avalonia.Media; using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; +using System.Diagnostics.CodeAnalysis; +using System.IO; namespace Avalonia.Skia.UnitTests.Media { @@ -35,9 +37,9 @@ namespace Avalonia.Skia.UnitTests.Media return _defaultFamilyName; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) { - return _customTypefaces.Select(x => x.FontFamily.Name); + return _customTypefaces.Select(x => x.FontFamily.Name).ToArray(); } private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; @@ -70,48 +72,132 @@ namespace Avalonia.Skia.UnitTests.Media { SKTypeface skTypeface; + Uri source = null; + switch (typeface.FontFamily.Name) { case "Twitter Color Emoji": { - var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily); - skTypeface = typefaceCollection.Get(typeface); + source = _emojiTypeface.FontFamily.Key.Source; break; } case "Noto Sans": { - var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); - skTypeface = typefaceCollection.Get(typeface); + source = _italicTypeface.FontFamily.Key.Source; break; } case "Noto Sans Arabic": { - var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily); - skTypeface = typefaceCollection.Get(typeface); + source = _arabicTypeface.FontFamily.Key.Source; break; } case "Noto Sans Hebrew": { - var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily); - skTypeface = typefaceCollection.Get(typeface); + source = _hebrewTypeface.FontFamily.Key.Source; break; } case FontFamily.DefaultFontFamilyName: case "Noto Mono": { - var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); - skTypeface = typefaceCollection.Get(_defaultTypeface); + source = _defaultTypeface.FontFamily.Key.Source; break; } default: { - skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name, - (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + break; } } + if (source is null) + { + skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name, + (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + } + else + { + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var assetUri = FontFamilyLoader.LoadFontAssets(source).First(); + + var stream = assetLoader.Open(assetUri); + + skTypeface = SKTypeface.FromStream(stream); + } + return new GlyphTypefaceImpl(skTypeface, FontSimulations.None); } + + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) + { + SKTypeface skTypeface; + + Uri source = null; + + switch (familyName) + { + case "Twitter Color Emoji": + { + source = _emojiTypeface.FontFamily.Key.Source; + break; + } + case "Noto Sans": + { + source = _italicTypeface.FontFamily.Key.Source; + break; + } + case "Noto Sans Arabic": + { + source = _arabicTypeface.FontFamily.Key.Source; + break; + } + case "Noto Sans Hebrew": + { + source = _hebrewTypeface.FontFamily.Key.Source; + break; + } + case FontFamily.DefaultFontFamilyName: + case "Noto Mono": + { + source = _defaultTypeface.FontFamily.Key.Source; + break; + } + default: + { + + break; + } + } + + if (source is null) + { + skTypeface = SKTypeface.FromFamilyName(familyName, + (SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style); + } + else + { + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + var assetUri = FontFamilyLoader.LoadFontAssets(source).First(); + + var stream = assetLoader.Open(assetUri); + + skTypeface = SKTypeface.FromStream(stream); + } + + glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + + return true; + } + + public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) + { + var skTypeface = SKTypeface.FromStream(stream); + + glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None); + + return true; + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs new file mode 100644 index 0000000000..8e7ed8b1d2 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs @@ -0,0 +1,58 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Skia.UnitTests.Media +{ + public class EmbeddedFontCollectionTests + { + private const string s_notoMono = + "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"; + + [InlineData(FontWeight.SemiLight, FontStyle.Normal)] + [InlineData(FontWeight.Bold, FontStyle.Italic)] + [InlineData(FontWeight.Heavy, FontStyle.Oblique)] + [Theory] + public void Should_Get_Near_Matching_Typeface(FontWeight fontWeight, FontStyle fontStyle) + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) + { + var fontCollection = new EmbeddedFontCollection(FontManager.Current, new Uri(s_notoMono)); + + Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", fontStyle, fontWeight, FontStretch.Normal, out var glyphTypeface)); + + var actual = glyphTypeface?.FamilyName; + + Assert.Equal("Noto Mono", actual); + } + } + + [Fact] + public void Should_Not_Get_Typeface_For_Invalid_FamilyName() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) + { + var fontCollection = new EmbeddedFontCollection(FontManager.Current, new Uri(s_notoMono)); + + Assert.False(fontCollection.TryGetGlyphTypeface("ABC", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface)); + } + } + + [Fact] + public void Should_Get_Typeface_For_Partial_FamilyName() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) + { + var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T", UriKind.Absolute); + + var fontCollection = new EmbeddedFontCollection(FontManager.Current, source); + + Assert.True(fontCollection.TryGetGlyphTypeface("T", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface)); + + Assert.Equal("Twitter Color Emoji", glyphTypeface.FamilyName); + } + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs index 649e1fbf3d..a9a0bd8faf 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs @@ -14,92 +14,67 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Create_Typeface_From_Fallback() { - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName()))); - - var skTypeface = glyphTypeface.Typeface; - - Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName); + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + var fontManager = FontManager.Current; - Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight); + var glyphTypeface = new Typeface(new FontFamily("A, B, " + fontManager.DefaultFontFamilyName)).GlyphTypeface; - Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant); + Assert.Equal(SKTypeface.Default.FamilyName, glyphTypeface.FamilyName); + } } [Fact] public void Should_Create_Typeface_From_Fallback_Bold() { - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold)); + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + var glyphTypeface = new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface; - var skTypeface = glyphTypeface.Typeface; - - Assert.True(skTypeface.FontWeight >= 600); + Assert.True((int)glyphTypeface.Weight >= 600); + } } [Fact] - public void Should_Create_Typeface_For_Unknown_Font() + public void Should_Throw_InvalidOperationException_For_Invalid_FamilyName() { - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("Unknown"))); - - var skTypeface = glyphTypeface.Typeface; - - Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName); - - Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight); - - Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant); + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + Assert.Throws(() => + { + var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; + }); + } } [Fact] public void Should_Load_Typeface_From_Resource() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) { - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri)); + var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface; - var skTypeface = glyphTypeface.Typeface; - - Assert.Equal("Noto Mono", skTypeface.FamilyName); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } [Fact] public void Should_Load_Nearest_Matching_Font() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) { - var fontManager = new FontManagerImpl(); - - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black)); - - var skTypeface = glyphTypeface.Typeface; + var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface; - Assert.Equal("Noto Mono", skTypeface.FamilyName); + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); } } [Fact] public void Should_Throw_For_Invalid_Custom_Font() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) { - var fontManager = new FontManagerImpl(); - - Assert.Throws(() => - fontManager.CreateGlyphTypeface( - new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown"))); + Assert.Throws(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs deleted file mode 100644 index 64050bd85e..0000000000 --- a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Avalonia.Media; -using Avalonia.UnitTests; -using Xunit; - -namespace Avalonia.Skia.UnitTests.Media -{ - public class SKTypefaceCollectionCacheTests - { - private const string s_notoMono = - "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"; - - [InlineData(s_notoMono, FontWeight.SemiLight, FontStyle.Normal)] - [InlineData(s_notoMono, FontWeight.Bold, FontStyle.Italic)] - [InlineData(s_notoMono, FontWeight.Heavy, FontStyle.Oblique)] - [Theory] - public void Should_Get_Near_Matching_Typeface(string familyName, FontWeight fontWeight, FontStyle fontStyle) - { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) - { - var fontFamily = new FontFamily(familyName); - - var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily); - - var actual = typefaceCollection.Get(new Typeface(fontFamily, fontStyle, fontWeight))?.FamilyName; - - Assert.Equal("Noto Mono", actual); - } - } - - [Fact] - public void Should_Get_Typeface_For_Invalid_FamilyName() - { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) - { - var notoMono = - new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); - - var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); - - var typeface = notoMonoCollection.Get(new Typeface("ABC")); - - Assert.NotNull(typeface); - } - } - - [Fact] - public void Should_Get_Typeface_For_Partial_FamilyName() - { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) - { - var fontFamily = new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T"); - - var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily); - - var typeface = fontCollection.Get(new Typeface(fontFamily)); - - Assert.NotNull(typeface); - - Assert.Equal("Twitter Color Emoji", typeface.FamilyName); - } - } - } -} diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs index 55ac16054d..a819cbd5e3 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs @@ -1,9 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using System.Linq; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Platform; namespace Avalonia.UnitTests @@ -31,9 +30,9 @@ namespace Avalonia.UnitTests return _defaultFamilyName; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) { - return _customTypefaces.Select(x => x.FontFamily!.Name); + return _customTypefaces.Select(x => x.FontFamily!.Name).ToArray(); } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, @@ -58,29 +57,19 @@ namespace Avalonia.UnitTests return false; } - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) { - var fontFamily = typeface.FontFamily; + glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream); - if (fontFamily.IsDefault) - { - fontFamily = _defaultTypeface.FontFamily; - } - - if (fontFamily!.Key == null) - { - return null; - } - - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key); + return true; + } - var asset = fontAssets.First(); - - var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) + { + glyphTypeface = null; - var stream = assetLoader.Open(asset); - - return new HarfBuzzGlyphTypefaceImpl(stream); + return false; } } } diff --git a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs index 5b11345f16..db517ba176 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs @@ -57,6 +57,15 @@ namespace Avalonia.UnitTests public FontSimulations FontSimulations { get; } + public string FamilyName => "$Default"; + + public FontWeight Weight { get; } + + public FontStyle Style { get; } + + public FontStretch Stretch { get; } + + /// public ushort GetGlyph(uint codepoint) { diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index e9b923a367..eda4544877 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using Avalonia.Media; using Avalonia.Platform; @@ -19,12 +20,12 @@ namespace Avalonia.UnitTests return _defaultFamilyName; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates) { return new[] { _defaultFamilyName }; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) { @@ -33,9 +34,18 @@ namespace Avalonia.UnitTests return false; } - public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) + public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface) { - return new MockGlyphTypeface(); + glyphTypeface = new MockGlyphTypeface(); + + return true; + } + + public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface) + { + glyphTypeface = new MockGlyphTypeface(); + + return true; } } } diff --git a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs index bd9d8e5adf..5fcee7f515 100644 --- a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs +++ b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs @@ -17,6 +17,14 @@ namespace Avalonia.UnitTests public FontSimulations FontSimulations => throw new NotImplementedException(); + public string FamilyName => "$Default"; + + public FontWeight Weight { get; } + + public FontStyle Style { get; } + + public FontStretch Stretch { get; } + public ushort GetGlyph(uint codepoint) { return (ushort)codepoint; From 67e6ba4354eba81a49cc8efa320d5854374fb565 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Feb 2023 09:27:29 +0100 Subject: [PATCH 25/71] Add AppBuilder extension --- src/Avalonia.Controls/AppBuilder.cs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 9e1222de6d..7e9fa1cab7 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -6,7 +6,6 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; using Avalonia.Media.Fonts; using Avalonia.Media; -using System.Xml.Linq; namespace Avalonia { @@ -19,7 +18,7 @@ namespace Avalonia private Action? _optionsInitializers; private Func? _appFactory; private IApplicationLifetime? _lifetime; - + /// /// Gets or sets the instance. /// @@ -34,12 +33,12 @@ namespace Avalonia /// Gets the instance being initialized. /// public Application? Instance { get; private set; } - + /// /// Gets the type of the Instance (even if it's not created yet) /// public Type? ApplicationType { get; private set; } - + /// /// Gets or sets a method to call the initialize the windowing subsystem. /// @@ -67,7 +66,7 @@ namespace Avalonia public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; - + /// /// Initializes a new instance of the class. /// @@ -76,7 +75,7 @@ namespace Avalonia builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly)) { } - + /// /// Initializes a new instance of the class. /// @@ -126,8 +125,8 @@ namespace Avalonia AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); return Self; } - - + + public AppBuilder AfterPlatformServicesSetup(Action callback) { AfterPlatformServicesSetupCallback = (Action)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); @@ -135,7 +134,7 @@ namespace Avalonia } public delegate void AppMainDelegate(Application app, string[] args); - + public void Start(AppMainDelegate main, string[] args) { Setup(); @@ -163,7 +162,7 @@ namespace Avalonia Setup(); return Self; } - + /// /// Specifies a windowing subsystem to use. /// @@ -198,7 +197,7 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); }; return Self; } - + /// /// Configures platform-specific options /// @@ -207,7 +206,7 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToFunc(options); }; return Self; } - + /// /// Registers a custom font collection with the font manager. /// From 5b2a6368c99297a15ce5dff0856a291a9b0f3260 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Feb 2023 12:01:16 +0100 Subject: [PATCH 26/71] Fix some tests --- .../Media/Fonts/EmbeddedFontCollection.cs | 5 ----- .../Media/Fonts/FontFamilyLoader.cs | 16 ++++++++++---- src/Avalonia.Themes.Simple/Accents/Base.xaml | 2 +- .../Media/EmbeddedFontCollectionTests.cs | 22 ++++++++++++++----- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index c242ed18db..80f47cee3b 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -23,11 +23,6 @@ namespace Avalonia.Media.Fonts { _key = key; - if(!source.IsAvares() && !source.IsAbsoluteResm()) - { - throw new ArgumentOutOfRangeException(nameof(source), "Specified source uri does not follow the resm: or avares: scheme."); - } - _source = source; } diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs index 39e80415fb..37992c895e 100644 --- a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs @@ -13,10 +13,18 @@ namespace Avalonia.Media.Fonts /// /// /// - public static IEnumerable LoadFontAssets(Uri source) => - IsFontTtfOrOtf(source) ? - GetFontAssetsByExpression(source) : - GetFontAssetsBySource(source); + public static IEnumerable LoadFontAssets(Uri source) + { + if (source.IsAvares() || source.IsAbsoluteResm()) + { + return IsFontTtfOrOtf(source) ? + GetFontAssetsByExpression(source) : + GetFontAssetsBySource(source); + } + + return Enumerable.Empty(); + } + /// /// Searches for font assets at a given location and returns a quantity of found assets diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index 0a06927034..38b122d8b2 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -76,7 +76,7 @@ - avares://Avalonia.Fonts.Inter/Assets#Inter, $Default + fonts://Inter#Inter, $Default #CC119EDA #99119EDA #66119EDA diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs index 8e7ed8b1d2..006abe9278 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs @@ -17,9 +17,13 @@ namespace Avalonia.Skia.UnitTests.Media [Theory] public void Should_Get_Near_Matching_Typeface(FontWeight fontWeight, FontStyle fontStyle) { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { - var fontCollection = new EmbeddedFontCollection(FontManager.Current, new Uri(s_notoMono)); + var source = new Uri(s_notoMono, UriKind.Absolute); + + var fontCollection = new EmbeddedFontCollection(source, source); + + fontCollection.Initialize(new CustomFontManagerImpl()); Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", fontStyle, fontWeight, FontStretch.Normal, out var glyphTypeface)); @@ -32,9 +36,13 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Not_Get_Typeface_For_Invalid_FamilyName() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { - var fontCollection = new EmbeddedFontCollection(FontManager.Current, new Uri(s_notoMono)); + var source = new Uri(s_notoMono, UriKind.Absolute); + + var fontCollection = new EmbeddedFontCollection(source, source); + + fontCollection.Initialize(new CustomFontManagerImpl()); Assert.False(fontCollection.TryGetGlyphTypeface("ABC", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface)); } @@ -43,11 +51,13 @@ namespace Avalonia.Skia.UnitTests.Media [Fact] public void Should_Get_Typeface_For_Partial_FamilyName() { - using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()))) + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T", UriKind.Absolute); - var fontCollection = new EmbeddedFontCollection(FontManager.Current, source); + var fontCollection = new EmbeddedFontCollection(source, source); + + fontCollection.Initialize(new CustomFontManagerImpl()); Assert.True(fontCollection.TryGetGlyphTypeface("T", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface)); From 1fc1c26cb782ff544612e07ab59655aae880fc63 Mon Sep 17 00:00:00 2001 From: pr8x Date: Tue, 28 Feb 2023 12:38:29 +0100 Subject: [PATCH 27/71] Set IsReadOnly instead, use ConvertToInvariantString, Fix TextBox foreground on selected row --- .../Diagnostics/Views/MainView.xaml.cs | 5 ++- .../Views/PropertyValueEditorView.cs | 32 ++++++++++++------- .../Controls/TextBox.xaml | 1 + 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs index 7980aa215b..df5188b29b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs @@ -1,7 +1,6 @@ using Avalonia.Controls; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Threading; @@ -18,7 +17,7 @@ namespace Avalonia.Diagnostics.Views public MainView() { InitializeComponent(); - AddHandler(KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel); + AddHandler(KeyUpEvent, PreviewKeyUp); _console = this.GetControl("console"); _consoleSplitter = this.GetControl("consoleSplitter"); _rootGrid = this.GetControl("rootGrid"); @@ -58,7 +57,7 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } - private void PreviewKeyDown(object? sender, KeyEventArgs e) + private void PreviewKeyUp(object? sender, KeyEventArgs e) { if (e.Key == Key.Escape) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs index d20312a218..6e7729a350 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs @@ -10,7 +10,6 @@ using Avalonia.Data.Converters; using Avalonia.Diagnostics.Controls; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; -using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Markup.Xaml.Converters; using Avalonia.Media; @@ -48,7 +47,7 @@ namespace Avalonia.Diagnostics.Views private static bool ImplementsInterface(Type type) { var interfaceType = typeof(TInterface); - return type == interfaceType || type.GetInterface(interfaceType.FullName!) != null; + return type == interfaceType || interfaceType.IsAssignableFrom(type); } private Control? UpdateControl() @@ -60,7 +59,8 @@ namespace Avalonia.Diagnostics.Views TControl CreateControl(AvaloniaProperty valueProperty, IValueConverter? converter = null, - Action? init = null) + Action? init = null, + AvaloniaProperty? readonlyProperty = null) where TControl : Control, new() { var control = new TControl(); @@ -75,7 +75,14 @@ namespace Avalonia.Diagnostics.Views ConverterParameter = propertyType }).DisposeWith(_cleanup); - control.IsEnabled = !Property.IsReadonly; + if (readonlyProperty != null) + { + control[readonlyProperty] = Property.IsReadonly; + } + else + { + control.IsEnabled = !Property.IsReadonly; + } return control; } @@ -93,7 +100,8 @@ namespace Avalonia.Diagnostics.Views n.Increment = 1; n.NumberFormat = new NumberFormatInfo { NumberDecimalDigits = 0 }; n.ParsingNumberStyle = NumberStyles.Integer; - }); + }, + readonlyProperty: NumericUpDown.IsReadOnlyProperty); if (propertyType == typeof(Color)) { @@ -117,7 +125,8 @@ namespace Avalonia.Diagnostics.Views Spacing = 2, Children = { el, tbl }, Background = Brushes.Transparent, - Cursor = new Cursor(StandardCursorType.Hand) + Cursor = new Cursor(StandardCursorType.Hand), + IsEnabled = !Property.IsReadonly }; var cv = new ColorView(); @@ -221,12 +230,13 @@ namespace Avalonia.Diagnostics.Views t => { t.Watermark = "(null)"; - }); + }, + readonlyProperty: TextBox.IsReadOnlyProperty); - tb.IsEnabled &= propertyType != typeof(object) && - StringConversionHelper.CanConvertFromString(propertyType); + tb.IsReadOnly |= propertyType == typeof(object) || + !StringConversionHelper.CanConvertFromString(propertyType); - if (tb.IsEnabled) + if (!tb.IsReadOnly) { tb.GetObservable(TextBox.TextProperty).Subscribe(t => { @@ -311,7 +321,7 @@ namespace Avalonia.Diagnostics.Views converter.GetType() == typeof(CollectionConverter)) return o.ToString(); - return converter.ConvertToString(o); + return converter.ConvertToInvariantString(o); } public static object? FromString(string str, Type type) diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml index 8428e3aae7..0c7095f2f5 100644 --- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml @@ -92,6 +92,7 @@ + From 5281bbcca097bbe4a8ce7d58264d02efc4df3292 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Feb 2023 16:39:41 +0100 Subject: [PATCH 28/71] Search for data template on tree attachment. When a `HeaderedItemsControl` is used in an `ItemsControl` it needs to search for an `ITreeDataTemplate` in order to populate the `Items` property. This can't be done properly until it's attached to the logical tree. Fixes #10398 --- src/Avalonia.Controls/ItemsControl.cs | 9 +--- .../Primitives/HeaderedItemsControl.cs | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9483f98881..bebf4a38f6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -383,14 +383,7 @@ namespace Avalonia.Controls { hic.Header = item; hic.HeaderTemplate = itemTemplate; - - itemTemplate ??= hic.FindDataTemplate(item) ?? this.FindDataTemplate(item); - - if (itemTemplate is ITreeDataTemplate treeTemplate) - { - if (item is not null && treeTemplate.ItemsSelector(item) is { } itemsBinding) - BindingOperations.Apply(hic, ItemsProperty, itemsBinding, null); - } + hic.PrepareItemContainer(); } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 71ae7a5bf6..55d2ec7506 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -1,6 +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 @@ -10,6 +12,9 @@ namespace Avalonia.Controls.Primitives /// public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { + private IDisposable? _itemsBinding; + private bool _prepareItemContainerOnAttach; + /// /// Defines the property. /// @@ -60,6 +65,17 @@ namespace Avalonia.Controls.Primitives /// IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + + if (_prepareItemContainerOnAttach) + { + PrepareItemContainer(); + _prepareItemContainerOnAttach = false; + } + } + /// bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { @@ -81,6 +97,37 @@ namespace Avalonia.Controls.Primitives return false; } + internal void PrepareItemContainer() + { + _itemsBinding?.Dispose(); + _itemsBinding = null; + + var item = Header; + + if (item is null) + { + _prepareItemContainerOnAttach = false; + return; + } + + var headerTemplate = HeaderTemplate; + + if (headerTemplate is null) + { + if (((ILogical)this).IsAttachedToLogicalTree) + headerTemplate = this.FindDataTemplate(item); + else + _prepareItemContainerOnAttach = true; + } + + if (headerTemplate is ITreeDataTemplate treeTemplate && + treeTemplate.Match(item) && + treeTemplate.ItemsSelector(item) is { } itemsBinding) + { + _itemsBinding = BindingOperations.Apply(this, ItemsProperty, itemsBinding, null); + } + } + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) { if (e.OldValue is ILogical oldChild) From 3791fa8a51ed75a8b170d776d459b061e9352909 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 23 Feb 2023 23:44:33 +0100 Subject: [PATCH 29/71] Don't add ItemsControl items to its panel logical children --- src/Avalonia.Controls/Panel.cs | 50 +++++++++++++++---- .../Presenters/ItemsPresenter.cs | 1 + .../ItemsControlTests.cs | 15 +++++- .../Avalonia.Controls.UnitTests/PanelTests.cs | 15 ++++++ .../TabControlTests.cs | 2 +- .../TreeViewTests.cs | 2 +- 6 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index fa18ee468c..035e775af7 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -1,13 +1,12 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using Avalonia.Controls.Presenters; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Reactive; -using Avalonia.Styling; namespace Avalonia.Controls { @@ -26,6 +25,16 @@ namespace Avalonia.Controls public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly DirectProperty IsItemsHostProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsItemsHost), + o => o.IsItemsHost, + (o, v) => o.IsItemsHost = v, + unsetValue: false); + /// /// Initializes static members of the class. /// @@ -34,6 +43,7 @@ namespace Avalonia.Controls AffectsRender(BackgroundProperty); } + private bool _isItemsHost; private EventHandler? _childIndexChanged; /// @@ -59,6 +69,15 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } + /// + /// Gets whether the hosts the items created by an . + /// + public bool IsItemsHost + { + get => _isItemsHost; + set => SetAndRaise(IsItemsHostProperty, ref _isItemsHost, value); + } + event EventHandler? IChildIndexProvider.ChildIndexChanged { add @@ -129,24 +148,29 @@ namespace Avalonia.Controls /// The event args. protected virtual void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) { - List controls; - switch (e.Action) { case NotifyCollectionChangedAction.Add: - controls = e.NewItems!.OfType().ToList(); - LogicalChildren.InsertRange(e.NewStartingIndex, controls); + if (!IsItemsHost) + { + LogicalChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType().ToList()); + } VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems!.OfType()); break; case NotifyCollectionChangedAction.Move: - LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); - VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex); + if (!IsItemsHost) + { + LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); + } + VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex); break; case NotifyCollectionChangedAction.Remove: - controls = e.OldItems!.OfType().ToList(); - LogicalChildren.RemoveAll(controls); + if (!IsItemsHost) + { + LogicalChildren.RemoveAll(e.OldItems!.OfType().ToList()); + } VisualChildren.RemoveAll(e.OldItems!.OfType()); break; @@ -155,7 +179,10 @@ namespace Avalonia.Controls { var index = i + e.OldStartingIndex; var child = (Control)e.NewItems![i]!; - LogicalChildren[index] = child; + if (!IsItemsHost) + { + LogicalChildren[index] = child; + } VisualChildren[index] = child; } break; @@ -200,6 +227,7 @@ namespace Avalonia.Controls return child is Control control ? Children.IndexOf(control) : -1; } + /// public bool TryGetTotalCount(out int count) { count = Children.Count; diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index e8eaac7d17..0a762c438d 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -167,6 +167,7 @@ namespace Avalonia.Controls.Presenters Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); + Panel.IsItemsHost = true; _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo; LogicalChildren.Add(Panel); VisualChildren.Add(Panel); diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index f074972cef..3aaf62f0bf 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -73,6 +73,19 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(target, target.Presenter.Panel.TemplatedParent); } + [Fact] + public void Panel_Should_Have_ItemsHost_Set_To_True() + { + var target = new ItemsControl(); + + target.Template = GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + target.Presenter!.ApplyTemplate(); + + Assert.True(target.Presenter.Panel!.IsItemsHost); + } + [Fact] public void Container_Should_Have_TemplatedParent_Set_To_Null() { @@ -634,7 +647,7 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.LogicalChildren[0]; Assert.Null(NameScope.GetNameScope((TextBlock)item)); } diff --git a/tests/Avalonia.Controls.UnitTests/PanelTests.cs b/tests/Avalonia.Controls.UnitTests/PanelTests.cs index a31f0dd4c2..82f133d533 100644 --- a/tests/Avalonia.Controls.UnitTests/PanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/PanelTests.cs @@ -141,5 +141,20 @@ namespace Avalonia.Controls.UnitTests var panel = new Panel(); Assert.Throws(() => panel.Children.Add(null!)); } + + [Fact] + public void Adding_Control_To_Items_Host_Panel_Should_Not_Affect_Logical_Children() + { + var child = new Control(); + var realParent = new ContentControl { Content = child }; + var panel = new Panel { IsItemsHost = true }; + + panel.Children.Add(child); + + Assert.Empty(panel.LogicalChildren); + Assert.Same(child.Parent, realParent); + Assert.Same(child.GetLogicalParent(), realParent); + Assert.Same(child.GetVisualParent(), panel); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 4c5397db60..0f72b2101a 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -327,7 +327,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); - var logicalChildren = target.ItemsPresenterPart.Panel.GetLogicalChildren(); + var logicalChildren = target.GetLogicalChildren(); var result = logicalChildren .OfType() diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 1a9fb33b7e..2ca716fa8f 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1178,7 +1178,7 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.LogicalChildren[0]; Assert.Null(NameScope.GetNameScope((TreeViewItem)item)); } From b6701f63fd13f09165cbe0c599ddacfb55b15730 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 28 Feb 2023 17:07:16 +0100 Subject: [PATCH 30/71] Made Panel.IsItemsHost setter internal --- src/Avalonia.Controls/Panel.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 035e775af7..eff6603727 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -25,16 +25,6 @@ namespace Avalonia.Controls public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly DirectProperty IsItemsHostProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsItemsHost), - o => o.IsItemsHost, - (o, v) => o.IsItemsHost = v, - unsetValue: false); - /// /// Initializes static members of the class. /// @@ -43,7 +33,6 @@ namespace Avalonia.Controls AffectsRender(BackgroundProperty); } - private bool _isItemsHost; private EventHandler? _childIndexChanged; /// @@ -72,11 +61,7 @@ namespace Avalonia.Controls /// /// Gets whether the hosts the items created by an . /// - public bool IsItemsHost - { - get => _isItemsHost; - set => SetAndRaise(IsItemsHostProperty, ref _isItemsHost, value); - } + public bool IsItemsHost { get; internal set; } event EventHandler? IChildIndexProvider.ChildIndexChanged { From 7786a00623cd2c231050731b1fa6dfdd130ca28a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Feb 2023 18:36:22 +0100 Subject: [PATCH 31/71] Add Inter font reference to all samples --- samples/BindingDemo/BindingDemo.csproj | 1 + samples/GpuInterop/GpuInterop.csproj | 1 + samples/MobileSandbox/MobileSandbox.csproj | 1 + samples/PlatformSanityChecks/PlatformSanityChecks.csproj | 1 + samples/Previewer/Previewer.csproj | 1 + samples/ReactiveUIDemo/ReactiveUIDemo.csproj | 1 + samples/RenderDemo/RenderDemo.csproj | 1 + samples/Sandbox/Sandbox.csproj | 1 + samples/VirtualizationDemo/VirtualizationDemo.csproj | 1 + 9 files changed, 9 insertions(+) diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index 056d3bf552..f094c0081c 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -5,6 +5,7 @@ + diff --git a/samples/GpuInterop/GpuInterop.csproj b/samples/GpuInterop/GpuInterop.csproj index 88e6d3d283..161821d92a 100644 --- a/samples/GpuInterop/GpuInterop.csproj +++ b/samples/GpuInterop/GpuInterop.csproj @@ -15,6 +15,7 @@ + diff --git a/samples/MobileSandbox/MobileSandbox.csproj b/samples/MobileSandbox/MobileSandbox.csproj index 02e4c43960..0d7c7859f8 100644 --- a/samples/MobileSandbox/MobileSandbox.csproj +++ b/samples/MobileSandbox/MobileSandbox.csproj @@ -28,6 +28,7 @@ + diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 5f61a08f3c..40efe818d8 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 76c1ba7b69..b572c3e4f8 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj index 9650068434..0e054d6c36 100644 --- a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj +++ b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index 3c62af1eaf..b37ecf01b9 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -12,6 +12,7 @@ + diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index eab654acb6..f23e391a2a 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -10,6 +10,7 @@ + diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index b27cfe77e8..81b30c6cbe 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -5,6 +5,7 @@ + From 45ef9235622f2244ab2687cedde000dfbbbd5475 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Feb 2023 23:03:13 +0100 Subject: [PATCH 32/71] Add display names to tasks. --- azure-pipelines-integrationtests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 7221fe4657..68c649fbf9 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -15,6 +15,7 @@ jobs: version: 7.0.101 - script: system_profiler SPDisplaysDataType |grep Resolution + displayName: 'Get Resolution' - script: | sudo xcode-select -s /Applications/Xcode.app/Contents/Developer @@ -27,8 +28,10 @@ jobs: ./samples/IntegrationTestApp/bundle.sh open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-arm64/publish/IntegrationTestApp.app pkill IntegrationTestApp + displayName: 'Build IntegrationTestApp' - task: DotNetCoreCLI@2 + displayName: 'Run Integration Tests' inputs: command: 'test' projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' @@ -36,6 +39,7 @@ jobs: - script: | pkill IntegrationTestApp pkill node + displayName: 'Stop Appium' - job: Windows @@ -60,11 +64,13 @@ jobs: displayName: 'Start WinAppDriver' - task: DotNetCoreCLI@2 + displayName: 'Build IntegrationTestApp' inputs: command: 'build' projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj' - task: DotNetCoreCLI@2 + displayName: 'Run Integration Tests' retryCountOnTaskFailure: 3 inputs: command: 'test' From 8f04fcd850e2d0d9c5d208dd6fa890bd25be501e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Feb 2023 23:05:06 +0100 Subject: [PATCH 33/71] Detailed logs on macOS. --- azure-pipelines-integrationtests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 68c649fbf9..8a526cc32e 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -35,6 +35,7 @@ jobs: inputs: command: 'test' projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' + arguments: '-l "console;verbosity=detailed"' - script: | pkill IntegrationTestApp From 0e0db41f055a5c90d455a2c4b2b4021ead630fe7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Mar 2023 10:31:35 +0100 Subject: [PATCH 34/71] Publish appium logs on failure. --- azure-pipelines-integrationtests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 8a526cc32e..194367c076 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -20,7 +20,7 @@ jobs: - script: | sudo xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node - appium & + appium > appium.out & pkill IntegrationTestApp ./build.sh CompileNative rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") @@ -42,6 +42,9 @@ jobs: pkill node displayName: 'Stop Appium' + - publish: appium.out + displayName: 'Publish appium logs on failure' + condition: failed() - job: Windows pool: From 12d418db336efad25859fbf8566768f9a1e86152 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 1 Mar 2023 12:30:20 +0100 Subject: [PATCH 35/71] fix: Address Review --- .../AvaloniaPropertyAnalyzer.CompileAnalyzer.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index af9b367368..ff70edde8e 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -694,11 +694,14 @@ public partial class AvaloniaPropertyAnalyzer void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) { - if (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Public && method is null) + if (method is null) { - context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + if (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Public) + { + context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + } } - else if (method is not null && method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) + else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) { context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, method.Locations[0], "property accessor", avaloniaPropertyStorage)); } From 9201f237541cca0eae59972f7552b36b7cef1b1c Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 1 Mar 2023 12:46:10 +0100 Subject: [PATCH 36/71] fix: Signal warnig if protected --- .../AvaloniaPropertyAnalyzer.CompileAnalyzer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index ff70edde8e..0c0b487dba 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -696,7 +696,9 @@ public partial class AvaloniaPropertyAnalyzer { if (method is null) { - if (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Public) + if (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Public || + (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Protected + && avaloniaPropertyStorage.ContainingSymbol.DeclaredAccessibility == Accessibility.Public)) { context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); } From 8b5114357f3652d9bedf5310b40aea4a84d1ccb0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Mar 2023 13:52:31 +0100 Subject: [PATCH 37/71] Skip flaky test. --- tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 2eaaf2e0a8..90fdc2511f 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -163,7 +163,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(TestPlatforms.MacOS)] + [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test")] public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() { // Issue #9565 From 2364c5d140822eb585ded033de6e3051f42ba69f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 1 Mar 2023 14:05:39 +0100 Subject: [PATCH 38/71] Try to find a fallback glyph typeface for unknown font families --- src/Avalonia.Base/Media/FontManager.cs | 24 ++++++++----- .../Media/Fonts/EmbeddedFontCollection.cs | 1 - .../Media/Fonts/SystemFontCollection.cs | 35 +++++++++++-------- .../Media/FontManagerImplTests.cs | 11 +++--- .../Media/FontManagerImplTests.cs | 11 +++--- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 27e25cf359..e472e56d97 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -15,9 +15,10 @@ namespace Avalonia.Media /// public sealed class FontManager { + internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts"); + public const string FontCollectionScheme = "fonts"; - private readonly SystemFontCollection _systemFonts; private readonly ConcurrentDictionary _fontCollections = new ConcurrentDictionary(); private readonly IReadOnlyList? _fontFallbacks; @@ -36,7 +37,7 @@ namespace Avalonia.Media throw new InvalidOperationException("Default font family name can't be null or empty."); } - _systemFonts = new SystemFontCollection(this); + AddFontCollection(new SystemFontCollection(this)); } public static FontManager Current @@ -71,7 +72,7 @@ namespace Avalonia.Media /// /// Get all system fonts. /// - public IFontCollection SystemFonts => _systemFonts; + public IFontCollection SystemFonts => _fontCollections[SystemFontsKey]; internal IFontManagerImpl PlatformImpl { get; } @@ -120,6 +121,11 @@ namespace Avalonia.Media { return true; } + + if (!fontFamily.FamilyNames.HasFallbacks) + { + return false; + } } foreach (var familyName in fontFamily.FamilyNames) @@ -130,7 +136,7 @@ namespace Avalonia.Media } } - return false; + return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface); } public void AddFontCollection(IFontCollection fontCollection) @@ -139,13 +145,15 @@ namespace Avalonia.Media if (!fontCollection.Key.IsFontCollection()) { - throw new ArgumentException(nameof(fontCollection), "Font collection Key should follow the fontCollection: scheme."); + throw new ArgumentException(nameof(fontCollection), "Font collection Key should follow the fonts: scheme."); } - if (!_fontCollections.TryAdd(key, fontCollection)) + _fontCollections.AddOrUpdate(key, fontCollection, (_, oldCollection) => { - throw new ArgumentException(nameof(fontCollection), "Font collection is already registered."); - } + oldCollection.Dispose(); + + return fontCollection; + }); fontCollection.Initialize(PlatformImpl); } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 80f47cee3b..7f12825a66 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using Avalonia.Platform; -using Avalonia.Utilities; namespace Avalonia.Media.Fonts { diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index ce0deb21e4..fd332c6ebe 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Avalonia.Platform; @@ -8,8 +9,7 @@ namespace Avalonia.Media.Fonts { internal class SystemFontCollection : IFontCollection { - private readonly Dictionary> _glyphTypefaceCache = - new Dictionary>(); + private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); private readonly FontManager _fontManager; private readonly string[] _familyNames; @@ -20,7 +20,7 @@ namespace Avalonia.Media.Fonts _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames(); } - public Uri Key => new Uri("fontCollection:SystemFonts"); + public Uri Key => FontManager.SystemFontsKey; public FontFamily this[int index] { @@ -44,23 +44,30 @@ namespace Avalonia.Media.Fonts var key = new FontCollectionKey(style, weight, stretch); - if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) { - glyphTypefaces = new Dictionary(); - - _glyphTypefaceCache.Add(familyName, glyphTypefaces); - } - - if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) - { - return true; + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + { + return true; + } + else + { + if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) && + glyphTypefaces.TryAdd(key, glyphTypeface)) + { + return true; + } + } } if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) { - glyphTypefaces.Add(key, glyphTypeface); + glyphTypefaces = new ConcurrentDictionary(); - return true; + if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) + { + return true; + } } return false; diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index c50f31a834..14e48b3b6c 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -42,18 +42,17 @@ namespace Avalonia.Direct2D1.UnitTests.Media } [Fact] - public void Should_Throw_InvalidOperationException_For_Unknown_Font() + public void Should_Create_Typeface_For_Unknown_Font() { using (AvaloniaLocator.EnterScope()) { Direct2D1Platform.Initialize(); - var fontManager = FontManager.Current; + var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; - Assert.Throws(() => - { - var glyphTypeface =new Typeface(new FontFamily("Unknown")).GlyphTypeface; - }); + var defaultName = FontManager.Current.DefaultFontFamilyName; + + Assert.Equal(defaultName, glyphTypeface.FamilyName); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs index a9a0bd8faf..21c46b836d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs @@ -36,14 +36,13 @@ namespace Avalonia.Skia.UnitTests.Media } [Fact] - public void Should_Throw_InvalidOperationException_For_Invalid_FamilyName() + public void Should_Yield_Default_GlyphTypeface_For_Invalid_FamilyName() { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) - { - Assert.Throws(() => - { - var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; - }); + { + var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface; + + Assert.Equal(FontManager.Current.DefaultFontFamilyName, glyphTypeface.FamilyName); } } From a4b3d30affffcc32d4692aebdb84eb97cd7b52d2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 1 Mar 2023 20:42:27 +0600 Subject: [PATCH 39/71] Use proper baseline origin --- src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 4d8759f545..4b09bc9280 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -21,7 +21,7 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IImmutableBrush foreground, IRef glyphRun) - : base(new Rect(glyphRun.Item.Size), transform, foreground) + : base(new Rect(glyphRun.Item.BaselineOrigin, glyphRun.Item.Size), transform, foreground) { GlyphRun = glyphRun.Clone(); } From c41193bccb97938daf55d9c64ca5b30a942fdc34 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 1 Mar 2023 16:18:54 +0100 Subject: [PATCH 40/71] feat: AvaloniaVS defered loading Allow loading the AvaloniaVS extension when referencing the Avalonia nuget package Part of AvaloniaUI/AvaloniaVS#311 --- packages/Avalonia/Avalonia.props | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 6f21971d3d..26908b1081 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -6,4 +6,9 @@ false + + + + + From 48a54d67d3b826547e1938c399624c48a801b17f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 1 Mar 2023 17:11:47 +0100 Subject: [PATCH 41/71] Use test font family for TextLayoutTests --- src/Avalonia.Base/Media/FontManager.cs | 15 ++++++++- .../Media/Fonts/IFontCollection.cs | 16 ++++++++++ .../Media/TextFormatting/TextLayoutTests.cs | 32 +++++++++---------- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index e472e56d97..595a2f3474 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -40,6 +40,9 @@ namespace Avalonia.Media AddFontCollection(new SystemFontCollection(this)); } + /// + /// Get the current font manager instance. + /// public static FontManager Current { get @@ -139,13 +142,19 @@ namespace Avalonia.Media return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface); } + /// + /// Add a font collection to the manager. + /// + /// The font collection. + /// + /// If a font collection's key is already present the collection is replaced. public void AddFontCollection(IFontCollection fontCollection) { var key = fontCollection.Key; if (!fontCollection.Key.IsFontCollection()) { - throw new ArgumentException(nameof(fontCollection), "Font collection Key should follow the fonts: scheme."); + throw new ArgumentException("Font collection Key should follow the fonts: scheme.", nameof(fontCollection)); } _fontCollections.AddOrUpdate(key, fontCollection, (_, oldCollection) => @@ -158,6 +167,10 @@ namespace Avalonia.Media fontCollection.Initialize(PlatformImpl); } + /// + /// Removes the font collection that corresponds to specified key. + /// + /// The font collection's key. public void RemoveFontCollection(Uri key) { if (_fontCollections.TryRemove(key, out var fontCollection)) diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs index 27b3378513..814230bcf3 100644 --- a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -7,10 +7,26 @@ namespace Avalonia.Media.Fonts { public interface IFontCollection : IReadOnlyList, IDisposable { + /// + /// Get the font collection's key. + /// Uri Key { get; } + /// + /// Initializes the font collection. + /// + /// The font manager the collection is registered with. void Initialize(IFontManagerImpl fontManager); + /// + /// Try to get a glyph typeface for given parameters. + /// + /// The family name. + /// The font style. + /// The font weight. + /// The font stretch. + /// The glyph typeface. + /// Returns true if a glyph typface can be found; otherwise, false bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); } diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index 6f47aa58d8..7b128076cd 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -17,11 +17,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media { public class TextLayoutTests : TestBase { - private const string FontName = "Courier New"; private const double FontSize = 12; private const double MediumFontSize = 18; private const double BigFontSize = 32; - private const double FontSizeHeight = 13.594;//real value 13.59375 + private const double FontSizeHeight = 14.0625;//real value 13.59375 private const string stringword = "word"; private const string stringmiddle = "The quick brown fox jumps over the lazy dog"; private const string stringmiddle2lines = "The quick brown fox\njumps over the lazy dog"; @@ -40,7 +39,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media } private static TextLayout Create(string text, - string fontFamily, double fontSize, FontStyle fontStyle, TextAlignment textAlignment, @@ -48,7 +46,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media TextWrapping wrapping, double widthConstraint) { - var typeface = new Typeface(fontFamily, fontStyle, fontWeight); + var typeface = new Typeface(TestFontFamily, fontStyle, fontWeight); var formattedText = new TextLayout(text, typeface, fontSize, null, textAlignment, wrapping, maxWidth: widthConstraint == -1 ? double.PositiveInfinity : widthConstraint); @@ -58,7 +56,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media private static TextLayout Create(string text, double fontSize) { - return Create(text, FontName, fontSize, + return Create(text, fontSize, FontStyle.Normal, TextAlignment.Left, FontWeight.Normal, TextWrapping.NoWrap, -1); @@ -66,7 +64,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media private static TextLayout Create(string text, double fontSize, TextAlignment alignment, double widthConstraint) { - return Create(text, FontName, fontSize, + return Create(text, fontSize, FontStyle.Normal, alignment, FontWeight.Normal, TextWrapping.NoWrap, widthConstraint); @@ -74,7 +72,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media private static TextLayout Create(string text, double fontSize, TextWrapping wrap, double widthConstraint) { - return Create(text, FontName, fontSize, + return Create(text, fontSize, FontStyle.Normal, TextAlignment.Left, FontWeight.Normal, wrap, widthConstraint); @@ -86,11 +84,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media [InlineData("x", FontSize, 7.20, FontSizeHeight)] [InlineData(stringword, FontSize, 28.80, FontSizeHeight)] [InlineData(stringmiddle, FontSize, 309.65, FontSizeHeight)] - [InlineData(stringmiddle, MediumFontSize, 464.48, 20.391)] - [InlineData(stringmiddle, BigFontSize, 825.73, 36.25)] + [InlineData(stringmiddle, MediumFontSize, 464.48, 21.09375)] + [InlineData(stringmiddle, BigFontSize, 825.73, 37.5)] [InlineData(stringmiddle2lines, FontSize, 165.63, 2 * FontSizeHeight)] - [InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 20.391)] - [InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 36.25)] + [InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 21.09375)] + [InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 37.5)] [InlineData(stringlong, FontSize, 2160.35, FontSizeHeight)] [InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)] public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight) @@ -221,12 +219,12 @@ namespace Avalonia.Direct2D1.RenderTests.Media } [Theory] - [InlineData("x", 0, 1, "0,0,7.20,13.59")] - [InlineData(stringword, 0, 4, "0,0,28.80,13.59")] - [InlineData(stringmiddlenewlines, 10, 10, "0,13.59,57.61,13.59")] - [InlineData(stringmiddlenewlines, 10, 20, "0,13.59,57.61,13.59;0,27.19,64.81,13.59")] - [InlineData(stringmiddlenewlines, 10, 15, "0,13.59,57.61,13.59;0,27.19,36.01,13.59")] - [InlineData(stringmiddlenewlines, 15, 15, "36.01,13.59,21.60,13.59;0,27.19,64.81,13.59")] + [InlineData("x", 0, 1, "0,0,7.20,14.0625")] + [InlineData(stringword, 0, 4, "0,0,28.80,14.0625")] + [InlineData(stringmiddlenewlines, 10, 10, "0,14.0625,57.61,14.0625")] + [InlineData(stringmiddlenewlines, 10, 20, "0,14.0625,57.61,14.0625;0,28.125,64.81,14.0625")] + [InlineData(stringmiddlenewlines, 10, 15, "0,14.0625,57.61,14.0625;0,28.125,36.01,14.0625")] + [InlineData(stringmiddlenewlines, 15, 15, "36.01,14.0625,21.60,14.0625;0,28.125,64.81,14.0625")] public void Should_HitTestRange_Correctly(string input, int index, int length, string expectedRects) From f07e5de66133a17cc5b647392e7dd76f1c384e0e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 1 Mar 2023 18:56:07 +0100 Subject: [PATCH 42/71] Make sure ItemsRepeater is loaded for XAML tests --- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 8a1ee72c54..933958b3a0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -14,6 +15,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml { public class StyleTests : XamlTestBase { + static StyleTests() + { + GC.KeepAlive(typeof(ItemsRepeater)); + } + [Fact] public void Color_Can_Be_Added_To_Style_Resources() { From c0276f75b9213e8ac90cda97f18f93b397e7a3c4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 2 Mar 2023 09:58:12 +0000 Subject: [PATCH 43/71] macos screen api returns the RenderScaling, instead of Desktop scaling, which is always 1 on macos. --- native/Avalonia.Native/src/OSX/Screens.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 83ab1bfd01..85f4b7c50a 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -41,7 +41,7 @@ public: ret->WorkingArea.X = [screen visibleFrame].origin.x; ret->WorkingArea.Y = ConvertPointY(ToAvnPoint([screen visibleFrame].origin)).Y - ret->WorkingArea.Height; - ret->Scaling = [screen backingScaleFactor]; + ret->Scaling = 1; ret->IsPrimary = index == 0; From de50cda755d2cd14510db5b6b4f6719e0a3a69aa Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Thu, 2 Mar 2023 13:04:02 +0200 Subject: [PATCH 44/71] Add LabelAutomationPeer --- .../Automation/Peers/LabelAutomationPeer.cs | 27 +++++++++++++++++++ src/Avalonia.Controls/Label.cs | 7 +++++ 2 files changed, 34 insertions(+) create mode 100644 src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs diff --git a/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs new file mode 100644 index 0000000000..4ea950df7e --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs @@ -0,0 +1,27 @@ +using Avalonia.Automation.Peers; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls.Automation.Peers +{ + public class LabelAutomationPeer : ControlAutomationPeer + { + public LabelAutomationPeer(Label owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Text"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Text; + } + + override protected string GetNameCore() + { + return AccessText.RemoveAccessKeyMarker(((Label)Owner).Content as string) ?? string.Empty; + } + } +} diff --git a/src/Avalonia.Controls/Label.cs b/src/Avalonia.Controls/Label.cs index 5c8a6e0a5b..cabbb5adf7 100644 --- a/src/Avalonia.Controls/Label.cs +++ b/src/Avalonia.Controls/Label.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -71,5 +73,10 @@ namespace Avalonia.Controls } base.OnPointerPressed(e); } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new LabelAutomationPeer(this); + } } } From 147db08f90b9f3625e7d024cc60f07945df937fe Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Mar 2023 12:34:34 +0100 Subject: [PATCH 45/71] Introduce GlyphRun.Bounds --- src/Avalonia.Base/Media/GlyphRun.cs | 14 +++++++------- src/Avalonia.Base/Media/GlyphRunDrawing.cs | 2 +- src/Avalonia.Base/Media/TextDecoration.cs | 2 +- .../Media/TextFormatting/ShapedTextRun.cs | 4 ++-- .../Media/TextFormatting/TextEllipsisHelper.cs | 2 +- .../TextLeadingPrefixCharacterEllipsis.cs | 2 +- .../Media/TextFormatting/TextLineImpl.cs | 6 +++--- src/Avalonia.Base/Platform/IGlyphRunImpl.cs | 3 +-- .../Composition/Server/DiagnosticTextRenderer.cs | 8 ++++---- .../Rendering/SceneGraph/GlyphRunNode.cs | 5 ++--- .../HeadlessPlatformRenderInterface.cs | 4 ++-- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- src/Skia/Avalonia.Skia/GlyphRunImpl.cs | 4 ++-- .../Avalonia.Direct2D1/Media/GlyphRunImpl.cs | 4 ++-- tests/Avalonia.Benchmarks/NullGlyphRun.cs | 2 +- tests/Avalonia.UnitTests/MockGlyphRun.cs | 4 ++-- 16 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 2966ceee8d..d795cca894 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -151,9 +151,9 @@ namespace Avalonia.Media } /// - /// Gets or sets the conservative bounding box of the . + /// Gets the conservative bounding box of the . /// - public Size Size => PlatformImpl.Item.Size; + public Rect Bounds => PlatformImpl.Item.Bounds; /// /// @@ -252,7 +252,7 @@ namespace Avalonia.Media if (characterIndex > Metrics.LastCluster) { - return Size.Width; + return Bounds.Width; } var glyphIndex = FindGlyphIndex(characterIndex); @@ -287,7 +287,7 @@ namespace Avalonia.Media if (characterIndex <= Metrics.FirstCluster) { - return Size.Width; + return Bounds.Width; } for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++) @@ -295,7 +295,7 @@ namespace Avalonia.Media distance += _glyphInfos[i].GlyphAdvance; } - return Size.Width - distance; + return Bounds.Width - distance; } } @@ -321,7 +321,7 @@ namespace Avalonia.Media } //After - if (distance >= Size.Width) + if (distance >= Bounds.Width) { isInside = false; @@ -354,7 +354,7 @@ namespace Avalonia.Media } else { - currentX = Size.Width; + currentX = Bounds.Width; for (var index = _glyphInfos.Count - 1; index >= 0; index--) { diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 06d92fd81c..961203e30e 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -32,7 +32,7 @@ public override Rect GetBounds() { - return GlyphRun != null ? new Rect(GlyphRun.Size) : default; + return GlyphRun != null ? GlyphRun.Bounds : default; } } } diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index b74b7df9c5..e89a7d8826 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -223,7 +223,7 @@ namespace Avalonia.Media if (intersections.Count > 0) { var last = baselineOrigin.X; - var finalPos = last + glyphRun.Size.Width; + var finalPos = last + glyphRun.Bounds.Width; var end = last; var points = new List(); diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index c5dd30b620..2f28c3f954 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -38,14 +38,14 @@ namespace Avalonia.Media.TextFormatting public override double Baseline => -TextMetrics.Ascent; - public override Size Size => GlyphRun.Size; + public override Size Size => GlyphRun.Bounds.Size; public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun(); /// public override void Draw(DrawingContext drawingContext, Point origin) { - using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) + using (drawingContext.PushTransform(Matrix.CreateTranslation(origin))) { if (GlyphRun.GlyphInfos.Count == 0) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 6422f23dcd..8b6d576c6e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -19,7 +19,7 @@ namespace Avalonia.Media.TextFormatting var collapsedLength = 0; var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); - if (properties.Width < shapedSymbol.GlyphRun.Size.Width) + if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width) { //Not enough space to fit in the symbol return Array.Empty(); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index a21a5d45e9..41d451b9e3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -60,7 +60,7 @@ namespace Avalonia.Media.TextFormatting var currentWidth = 0.0; var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, FlowDirection.LeftToRight); - if (Width < shapedSymbol.GlyphRun.Size.Width) + if (Width < shapedSymbol.GlyphRun.Bounds.Width) { return Array.Empty(); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index b3321d4d9f..f426a20b2c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -423,7 +423,7 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun != null) { - currentDistance -= currentGlyphRun.Size.Width; + currentDistance -= currentGlyphRun.Bounds.Width; } return currentDistance + distance; @@ -477,7 +477,7 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) { - distance = currentGlyphRun.Size.Width; + distance = currentGlyphRun.Bounds.Width; } return true; @@ -1483,7 +1483,7 @@ namespace Avalonia.Media.TextFormatting trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength; - var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width; + var whitespaceWidth = glyphRun.Bounds.Width - glyphRunMetrics.Width; width -= whitespaceWidth; } diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 46b065b04e..fccea27c43 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -10,11 +10,10 @@ namespace Avalonia.Platform [Unstable] public interface IGlyphRunImpl : IDisposable { - /// /// Gets the conservative bounding box of the glyph run./>. /// - Size Size { get; } + Rect Bounds { get; } /// /// Gets the baseline origin of the glyph run./>. diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index b01fb46aa3..04e40e8744 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.Composition.Server for (var c = FirstChar; c <= LastChar; c++) { - var height = _runs[c - FirstChar].Size.Height; + var height = _runs[c - FirstChar].Bounds.Height; if (height > maxHeight) { maxHeight = height; @@ -51,8 +51,8 @@ namespace Avalonia.Rendering.Composition.Server { var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; var run = _runs[effectiveChar - FirstChar]; - width += run.Size.Width; - height = Math.Max(height, run.Size.Height); + width += run.Bounds.Width; + height = Math.Max(height, run.Bounds.Height); } return new Size(width, height); @@ -69,7 +69,7 @@ namespace Avalonia.Rendering.Composition.Server var run = _runs[effectiveChar - FirstChar]; context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); context.DrawGlyphRun(foreground, run.PlatformImpl); - offset += run.Size.Width; + offset += run.Bounds.Width; } context.Transform = originalTransform; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 4b09bc9280..381c63f430 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -16,12 +16,11 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, IImmutableBrush foreground, IRef glyphRun) - : base(new Rect(glyphRun.Item.BaselineOrigin, glyphRun.Item.Size), transform, foreground) + : base(glyphRun.Item.Bounds, transform, foreground) { GlyphRun = glyphRun.Clone(); } @@ -54,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p); + public override bool HitTest(Point p) => GlyphRun.Item.Bounds.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 31aaebcdc7..f8100d3832 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -118,7 +118,7 @@ namespace Avalonia.Headless public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - return new HeadlessGeometryStub(new Rect(glyphRun.Size)); + return new HeadlessGeometryStub(glyphRun.Bounds); } public IGlyphRunImpl CreateGlyphRun( @@ -132,7 +132,7 @@ namespace Avalonia.Headless class HeadlessGlyphRunStub : IGlyphRunImpl { - public Size Size => new Size(8, 12); + public Rect Bounds => new Rect(new Size(8, 12)); public Point BaselineOrigin => new Point(0, 8); diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index af0231579c..e3e2f664c3 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -516,7 +516,7 @@ namespace Avalonia.Skia return; } - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 079eea7bef..0521e238f3 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Skia { TextBlob = textBlob ?? throw new ArgumentNullException(nameof(textBlob)); - Size = size; + Bounds = new Rect(new Point(baselineOrigin.X, 0), size); BaselineOrigin = baselineOrigin; } @@ -21,7 +21,7 @@ namespace Avalonia.Skia /// public SKTextBlob TextBlob { get; } - public Size Size { get; } + public Rect Bounds { get; } public Point BaselineOrigin { get; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 446db47d92..2e7a4b67f6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -9,12 +9,12 @@ namespace Avalonia.Direct2D1.Media { public GlyphRunImpl(GlyphRun glyphRun, Size size, Point baselineOrigin) { - Size = size; + Bounds = new Rect(new Point(baselineOrigin.X, 0), size); BaselineOrigin = baselineOrigin; GlyphRun = glyphRun; } - public Size Size { get; } + public Rect Bounds{ get; } public Point BaselineOrigin { get; } diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs index c4707c78c8..5b584f302d 100644 --- a/tests/Avalonia.Benchmarks/NullGlyphRun.cs +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -5,7 +5,7 @@ namespace Avalonia.Benchmarks { internal class NullGlyphRun : IGlyphRunImpl { - public Size Size => default; + public Rect Bounds => default; public Point BaselineOrigin => default; diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index 0319803a5e..4561d3b3f2 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -16,10 +16,10 @@ namespace Avalonia.UnitTests width += glyphInfos[i].GlyphAdvance; } - Size = new Size(width, 10); + Bounds = new Rect(new Size(width, 10)); } - public Size Size { get; } + public Rect Bounds { get; } public Point BaselineOrigin => new Point(0, 8); From 9016933085285c42e783e347ff09b968c22e9b46 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Mar 2023 12:40:42 +0100 Subject: [PATCH 46/71] Fix some unit tests --- src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs | 2 +- tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs | 6 +++--- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index f9b5953e3f..87fa963871 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -387,7 +387,7 @@ namespace Avalonia.Direct2D1.Media /// The glyph run. public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Item.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Item.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index c89fe9c09a..acdc242f0d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -110,7 +110,7 @@ namespace Avalonia.Skia.UnitTests.Media if (glyphRun.IsLeftToRight) { var characterHit = - glyphRun.GetCharacterHitFromDistance(glyphRun.Size.Width, out _); + glyphRun.GetCharacterHitFromDistance(glyphRun.Bounds.Width, out _); Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } @@ -157,9 +157,9 @@ namespace Avalonia.Skia.UnitTests.Media private static List BuildRects(GlyphRun glyphRun) { - var height = glyphRun.Size.Height; + var height = glyphRun.Bounds.Height; - var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Size.Width; + var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Bounds.Width; var rects = new List(glyphRun.GlyphInfos!.Count); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 817da33e1c..a24da38fdd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -457,7 +457,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var glyphRun = shapedRun.GlyphRun; - var width = glyphRun.Size.Width; + var width = glyphRun.Bounds.Width; var characterHit = glyphRun.GetCharacterHitFromDistance(width, out _); From 90ae432e634f1dc766b32f022b010ab5f0b1fb1c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Mar 2023 12:49:13 +0100 Subject: [PATCH 47/71] Use ConcurrentDictionary --- .../Media/Fonts/EmbeddedFontCollection.cs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 7f12825a66..f2fb490592 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -9,8 +10,7 @@ namespace Avalonia.Media.Fonts { public class EmbeddedFontCollection : IFontCollection { - private readonly Dictionary> _glyphTypefaceCache = - new Dictionary>(); + private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); private readonly List _fontFamilies = new List(1); @@ -45,11 +45,12 @@ namespace Avalonia.Media.Fonts { if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) { - glyphTypefaces = new Dictionary(); + glyphTypefaces = new ConcurrentDictionary(); - _glyphTypefaceCache.Add(glyphTypeface.FamilyName, glyphTypefaces); - - _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); + if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) + { + _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); + } } var key = new FontCollectionKey( @@ -57,10 +58,7 @@ namespace Avalonia.Media.Fonts glyphTypeface.Weight, glyphTypeface.Stretch); - if (!glyphTypefaces.ContainsKey(key)) - { - glyphTypefaces.Add(key, glyphTypeface); - } + glyphTypefaces.TryAdd(key, glyphTypeface); } } } @@ -119,7 +117,7 @@ namespace Avalonia.Media.Fonts } private static bool TryGetNearestMatch( - Dictionary glyphTypefaces, + ConcurrentDictionary glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { @@ -173,7 +171,7 @@ namespace Avalonia.Media.Fonts } private static bool TryFindStretchFallback( - Dictionary glyphTypefaces, + ConcurrentDictionary glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { @@ -206,7 +204,7 @@ namespace Avalonia.Media.Fonts } private static bool TryFindWeightFallback( - Dictionary glyphTypefaces, + ConcurrentDictionary glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? typeface) { From 39126b9a8ca5de80440fac9985611cec89757cb9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 2 Mar 2023 12:10:35 +0000 Subject: [PATCH 48/71] skip flaky test on macos. --- tests/Avalonia.IntegrationTests.Appium/GestureTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs index 65864cc649..775ee723cb 100644 --- a/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs @@ -74,7 +74,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("DoubleTapped", lastGesture.Text); } - [Fact] + [PlatformFact(TestPlatforms.Windows | TestPlatforms.Linux)] public void DoubleTapped_Is_Raised_2() { var border = _session.FindElementByAccessibilityId("GestureBorder"); From b65933b9d6b2cc88b541ea9ab5f911d85f3bd1bb Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Thu, 2 Mar 2023 14:11:17 +0200 Subject: [PATCH 49/71] Update GetNameCore logic to match WPF logic --- .../Automation/Peers/LabelAutomationPeer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs index 4ea950df7e..86cd0d5b14 100644 --- a/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/LabelAutomationPeer.cs @@ -19,9 +19,16 @@ namespace Avalonia.Controls.Automation.Peers return AutomationControlType.Text; } - override protected string GetNameCore() + override protected string? GetNameCore() { - return AccessText.RemoveAccessKeyMarker(((Label)Owner).Content as string) ?? string.Empty; + var content = ((Label)Owner).Content as string; + + if (string.IsNullOrEmpty(content)) + { + return base.GetNameCore(); + } + + return AccessText.RemoveAccessKeyMarker(content) ?? string.Empty; } } } From 81530c42317282bf08a0201011a82a07c404ede3 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Thu, 16 Feb 2023 17:06:11 +0100 Subject: [PATCH 50/71] Converted a variety of writeable DirectProperty definitions to StyledProperty --- src/Avalonia.Base/AttachedProperty.cs | 7 +- .../GestureRecognizerCollection.cs | 15 +- .../PullGestureRecognizer.cs | 12 +- .../ScrollGestureRecognizer.cs | 52 ++--- .../Media/Imaging/CroppedBitmap.cs | 2 - src/Avalonia.Base/StyledElement.cs | 3 +- src/Avalonia.Base/StyledProperty.cs | 7 +- src/Avalonia.Controls/Calendar/Calendar.cs | 90 ++++----- .../Calendar/CalendarItem.cs | 14 +- .../CalendarDatePicker.Properties.cs | 58 +++--- .../CalendarDatePicker/CalendarDatePicker.cs | 51 +++-- src/Avalonia.Controls/ComboBox.cs | 28 ++- .../DateTimePickers/DatePicker.cs | 183 +++++++++--------- .../DateTimePickers/DatePickerPresenter.cs | 181 +++++++++-------- .../DateTimePickers/TimePicker.cs | 93 +++++---- .../DateTimePickers/TimePickerPresenter.cs | 68 +++---- .../Documents/InlineCollection.cs | 8 +- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 23 +-- src/Avalonia.Controls/Label.cs | 22 +-- src/Avalonia.Controls/NativeMenu.cs | 4 +- src/Avalonia.Controls/NativeMenuItem.cs | 111 +++++------ src/Avalonia.Controls/NativeMenuItemBase.cs | 4 +- .../Notifications/NotificationCard.cs | 9 +- .../NumericUpDown/NumericUpDown.cs | 54 +++--- .../Presenters/ContentPresenter.cs | 11 +- .../Presenters/ItemsPresenter.cs | 2 +- src/Avalonia.Controls/Primitives/Popup.cs | 31 ++- .../Primitives/TemplatedControl.cs | 4 +- .../Primitives/ToggleButton.cs | 31 ++- src/Avalonia.Controls/RadioButton.cs | 59 +++--- src/Avalonia.Controls/TreeViewItem.cs | 4 +- src/Avalonia.Controls/Window.cs | 35 ++-- src/Avalonia.Controls/WindowBase.cs | 5 +- .../Diagnostics/Controls/ThicknessEditor.cs | 79 ++++---- .../Avalonia.Markup/Data/TemplateBinding.cs | 4 +- .../Styling/SelectorTests_Template.cs | 6 +- .../ItemsControlTests.cs | 2 +- .../ContentPresenterTests_InTemplate.cs | 2 +- .../Presenters/ItemsPresenterTests.cs | 2 +- .../Templates/TemplateExtensionsTests.cs | 14 +- 40 files changed, 630 insertions(+), 760 deletions(-) diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs index 31b6cad8ab..4a09f2a80a 100644 --- a/src/Avalonia.Base/AttachedProperty.cs +++ b/src/Avalonia.Base/AttachedProperty.cs @@ -32,9 +32,14 @@ namespace Avalonia /// /// The owner type. /// The property. - public new AttachedProperty AddOwner() where TOwner : AvaloniaObject + public new AttachedProperty AddOwner(StyledPropertyMetadata? metadata = null) where TOwner : AvaloniaObject { AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); + if (metadata != null) + { + OverrideMetadata(metadata); + } + return this; } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs index a202d6b5bc..3b9b2d0de6 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.LogicalTree; -using Avalonia.Styling; +using Avalonia.Reactive; namespace Avalonia.Input.GestureRecognizers { @@ -11,13 +11,13 @@ namespace Avalonia.Input.GestureRecognizers private readonly IInputElement _inputElement; private List? _recognizers; private Dictionary? _pointerGrabs; - - + + public GestureRecognizerCollection(IInputElement inputElement) { _inputElement = inputElement; } - + public void Add(IGestureRecognizer recognizer) { if (_recognizers == null) @@ -31,14 +31,13 @@ namespace Avalonia.Input.GestureRecognizers recognizer.Initialize(_inputElement, this); // Hacks to make bindings work - + if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical) { logical.SetParent(logicalParent); if (recognizer is StyledElement styleableRecognizer && _inputElement is StyledElement styleableParent) - styleableRecognizer.Bind(StyledElement.TemplatedParentProperty, - styleableParent.GetObservable(StyledElement.TemplatedParentProperty)); + styleableParent.GetObservable(StyledElement.TemplatedParentProperty).Subscribe(parent => styleableRecognizer.TemplatedParent = parent); } } @@ -58,7 +57,7 @@ namespace Avalonia.Input.GestureRecognizers return false; foreach (var r in _recognizers) { - if(e.Handled) + if (e.Handled) break; r.PointerPressed(e); } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index 991694cc60..6784677520 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs @@ -13,22 +13,18 @@ namespace Avalonia.Input private Point _initialPosition; private int _gestureId; private IPointer? _tracking; - private PullDirection _pullDirection; private bool _pullInProgress; /// /// Defines the property. /// - public static readonly DirectProperty PullDirectionProperty = - AvaloniaProperty.RegisterDirect( - nameof(PullDirection), - o => o.PullDirection, - (o, v) => o.PullDirection = v); + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection)); public PullDirection PullDirection { - get => _pullDirection; - set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value); + get => GetValue(PullDirectionProperty); + set => SetValue(PullDirectionProperty, value); } public PullGestureRecognizer(PullDirection pullDirection) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 7c1ee13eed..1ad2f292ca 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -17,61 +17,45 @@ namespace Avalonia.Input.GestureRecognizers private IPointer? _tracking; private IInputElement? _target; private IGestureRecognizerActionsDispatcher? _actions; - private bool _canHorizontallyScroll; - private bool _canVerticallyScroll; private int _gestureId; - private int _scrollStartDistance = 30; private Point _pointerPressedPoint; private VelocityTracker? _velocityTracker; // Movement per second private Vector _inertia; private ulong? _lastMoveTimestamp; - private bool _isScrollInertiaEnabled; /// /// Defines the property. /// - public static readonly DirectProperty CanHorizontallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanHorizontallyScroll), - o => o.CanHorizontallyScroll, - (o, v) => o.CanHorizontallyScroll = v); + public static readonly StyledProperty CanHorizontallyScrollProperty = + AvaloniaProperty.Register(nameof(CanHorizontallyScroll)); /// /// Defines the property. /// - public static readonly DirectProperty CanVerticallyScrollProperty = - AvaloniaProperty.RegisterDirect( - nameof(CanVerticallyScroll), - o => o.CanVerticallyScroll, - (o, v) => o.CanVerticallyScroll = v); + public static readonly StyledProperty CanVerticallyScrollProperty = + AvaloniaProperty.Register(nameof(CanVerticallyScroll)); /// /// Defines the property. /// - public static readonly DirectProperty IsScrollInertiaEnabledProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsScrollInertiaEnabled), - o => o.IsScrollInertiaEnabled, - (o, v) => o.IsScrollInertiaEnabled = v); + public static readonly StyledProperty IsScrollInertiaEnabledProperty = + AvaloniaProperty.Register(nameof(IsScrollInertiaEnabled)); /// /// Defines the property. /// - public static readonly DirectProperty ScrollStartDistanceProperty = - AvaloniaProperty.RegisterDirect( - nameof(ScrollStartDistance), - o => o.ScrollStartDistance, - (o, v) => o.ScrollStartDistance = v); + public static readonly StyledProperty ScrollStartDistanceProperty = + AvaloniaProperty.Register(nameof(ScrollStartDistance), 30); /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. /// public bool CanHorizontallyScroll { - get => _canHorizontallyScroll; - set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); + get => GetValue(CanHorizontallyScrollProperty); + set => SetValue(CanHorizontallyScrollProperty, value); } /// @@ -79,8 +63,8 @@ namespace Avalonia.Input.GestureRecognizers /// public bool CanVerticallyScroll { - get => _canVerticallyScroll; - set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); + get => GetValue(CanVerticallyScrollProperty); + set => SetValue(CanVerticallyScrollProperty, value); } /// @@ -88,8 +72,8 @@ namespace Avalonia.Input.GestureRecognizers /// public bool IsScrollInertiaEnabled { - get => _isScrollInertiaEnabled; - set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value); + get => GetValue(IsScrollInertiaEnabledProperty); + set => SetValue(IsScrollInertiaEnabledProperty, value); } /// @@ -97,8 +81,8 @@ namespace Avalonia.Input.GestureRecognizers /// public int ScrollStartDistance { - get => _scrollStartDistance; - set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value); + get => GetValue(ScrollStartDistanceProperty); + set => SetValue(ScrollStartDistanceProperty, value); } @@ -137,8 +121,8 @@ namespace Avalonia.Input.GestureRecognizers // Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance _trackedRootPoint = new Point( - _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? _scrollStartDistance : -_scrollStartDistance), - _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance)); + _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance), + _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? ScrollStartDistance : -ScrollStartDistance)); _actions!.Capture(e.Pointer, this); } diff --git a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs index 525a543b70..8e57f9a902 100644 --- a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs @@ -48,8 +48,6 @@ namespace Avalonia.Media.Imaging public CroppedBitmap() { - Source = null; - SourceRect = default; } public CroppedBitmap(IImage source, PixelRect sourceRect) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index cbdf3c3c1e..b51093b40c 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -67,8 +67,7 @@ namespace Avalonia public static readonly DirectProperty TemplatedParentProperty = AvaloniaProperty.RegisterDirect( nameof(TemplatedParent), - o => o.TemplatedParent, - (o ,v) => o.TemplatedParent = v); + o => o.TemplatedParent); /// /// Defines the property. diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 8695918c18..5052840013 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -56,9 +56,14 @@ namespace Avalonia /// /// The type of the additional owner. /// The property. - public StyledProperty AddOwner() where TOwner : AvaloniaObject + public StyledProperty AddOwner(StyledPropertyMetadata? metadata = null) where TOwner : AvaloniaObject { AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); + if (metadata != null) + { + OverrideMetadata(metadata); + } + return this; } diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 3300292857..10aadfa759 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -232,14 +232,9 @@ namespace Avalonia.Controls internal const int RowsPerYear = 3; internal const int ColumnsPerYear = 4; - private DateTime? _selectedDate; private DateTime _selectedMonth; private DateTime _selectedYear; - private DateTime _displayDate = DateTime.Today; - private DateTime? _displayDateStart; - private DateTime? _displayDateEnd; - private bool _isShiftPressed; private bool _displayDateIsChanging; @@ -396,13 +391,13 @@ namespace Avalonia.Controls } case CalendarMode.Year: { - DisplayDate = SelectedMonth; + SetCurrentValue(DisplayDateProperty, SelectedMonth); SelectedYear = SelectedMonth; break; } case CalendarMode.Decade: { - DisplayDate = SelectedYear; + SetCurrentValue(DisplayDateProperty, SelectedYear); SelectedMonth = SelectedYear; break; } @@ -472,7 +467,7 @@ namespace Avalonia.Controls if (IsValidSelectionMode(e.NewValue!)) { _displayDateIsChanging = true; - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); _displayDateIsChanging = false; SelectedDates.Clear(); } @@ -497,11 +492,8 @@ namespace Avalonia.Controls || mode == CalendarSelectionMode.None; } - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedDate), - o => o.SelectedDate, - (o, v) => o.SelectedDate = v, + public static readonly StyledProperty SelectedDateProperty = + AvaloniaProperty.Register(nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay); /// @@ -529,8 +521,8 @@ namespace Avalonia.Controls /// public DateTime? SelectedDate { - get { return _selectedDate; } - set { SetAndRaise(SelectedDateProperty, ref _selectedDate, value); } + get => GetValue(SelectedDateProperty); + set => SetValue(SelectedDateProperty, value); } private void OnSelectedDateChanged(AvaloniaPropertyChangedEventArgs e) { @@ -726,11 +718,8 @@ namespace Avalonia.Controls } } - public static readonly DirectProperty DisplayDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDate), - o => o.DisplayDate, - (o, v) => o.DisplayDate = v, + public static readonly StyledProperty DisplayDateProperty = + AvaloniaProperty.Register(nameof(DisplayDate), defaultBindingMode: BindingMode.TwoWay); /// @@ -760,8 +749,8 @@ namespace Avalonia.Controls /// public DateTime DisplayDate { - get { return _displayDate; } - set { SetAndRaise(DisplayDateProperty, ref _displayDate, value); } + get => GetValue(DisplayDateProperty); + set => SetValue(DisplayDateProperty, value); } internal DateTime DisplayDateInternal { get; private set; } @@ -796,11 +785,8 @@ namespace Avalonia.Controls DisplayDateChanged?.Invoke(this, e); } - public static readonly DirectProperty DisplayDateStartProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateStart), - o => o.DisplayDateStart, - (o, v) => o.DisplayDateStart = v, + public static readonly StyledProperty DisplayDateStartProperty = + AvaloniaProperty.Register(nameof(DisplayDateStart), defaultBindingMode: BindingMode.TwoWay); /// /// Gets or sets the first date to be displayed. @@ -814,8 +800,8 @@ namespace Avalonia.Controls /// public DateTime? DisplayDateStart { - get { return _displayDateStart; } - set { SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); } + get => GetValue(DisplayDateStartProperty); + set => SetValue(DisplayDateStartProperty, value); } private void OnDisplayDateStartChanged(AvaloniaPropertyChangedEventArgs e) { @@ -831,7 +817,7 @@ namespace Avalonia.Controls if (selectedDateMin.HasValue && DateTime.Compare(selectedDateMin.Value, newValue.Value) < 0) { - DisplayDateStart = selectedDateMin.Value; + SetCurrentValue(DisplayDateStartProperty, selectedDateMin.Value); return; } @@ -839,14 +825,14 @@ namespace Avalonia.Controls // DisplayDateEnd = DisplayDateStart if (DateTime.Compare(newValue.Value, DisplayDateRangeEnd) > 0) { - DisplayDateEnd = DisplayDateStart; + SetCurrentValue(DisplayDateEndProperty, DisplayDateStart); } // If DisplayDate < DisplayDateStart, // DisplayDate = DisplayDateStart if (DateTimeHelper.CompareYearMonth(newValue.Value, DisplayDateInternal) > 0) { - DisplayDate = newValue.Value; + SetCurrentValue(DisplayDateProperty, newValue.Value); } } UpdateMonths(); @@ -905,11 +891,8 @@ namespace Avalonia.Controls get { return DisplayDateStart.GetValueOrDefault(DateTime.MinValue); } } - public static readonly DirectProperty DisplayDateEndProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateEnd), - o => o.DisplayDateEnd, - (o, v) => o.DisplayDateEnd = v, + public static readonly StyledProperty DisplayDateEndProperty = + AvaloniaProperty.Register(nameof(DisplayDateEnd), defaultBindingMode: BindingMode.TwoWay); /// @@ -924,8 +907,8 @@ namespace Avalonia.Controls /// public DateTime? DisplayDateEnd { - get { return _displayDateEnd; } - set { SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); } + get => GetValue(DisplayDateEndProperty); + set => SetValue(DisplayDateEndProperty, value); } private void OnDisplayDateEndChanged(AvaloniaPropertyChangedEventArgs e) @@ -942,7 +925,7 @@ namespace Avalonia.Controls if (selectedDateMax.HasValue && DateTime.Compare(selectedDateMax.Value, newValue.Value) > 0) { - DisplayDateEnd = selectedDateMax.Value; + SetCurrentValue(DisplayDateEndProperty, selectedDateMax.Value); return; } @@ -950,7 +933,7 @@ namespace Avalonia.Controls // DisplayDateEnd = DisplayDateStart if (DateTime.Compare(newValue.Value, DisplayDateRangeStart) < 0) { - DisplayDateEnd = DisplayDateStart; + SetCurrentValue(DisplayDateEndProperty, DisplayDateStart); return; } @@ -958,7 +941,7 @@ namespace Avalonia.Controls // DisplayDate = DisplayDateEnd if (DateTimeHelper.CompareYearMonth(newValue.Value, DisplayDateInternal) < 0) { - DisplayDate = newValue.Value; + SetCurrentValue(DisplayDateProperty, newValue.Value); } } UpdateMonths(); @@ -1284,7 +1267,7 @@ namespace Avalonia.Controls { LastSelectedDate = d.Value; } - DisplayDate = d.Value; + SetCurrentValue(DisplayDateProperty, d.Value); } } else @@ -1332,7 +1315,7 @@ namespace Avalonia.Controls { LastSelectedDate = d.Value; } - DisplayDate = d.Value; + SetCurrentValue(DisplayDateProperty, d.Value); } } else @@ -1719,7 +1702,7 @@ namespace Avalonia.Controls if (ctrl) { SelectedMonth = DisplayDateInternal; - DisplayMode = CalendarMode.Year; + SetCurrentValue(DisplayModeProperty, CalendarMode.Year); } else { @@ -1733,7 +1716,7 @@ namespace Avalonia.Controls if (ctrl) { SelectedYear = SelectedMonth; - DisplayMode = CalendarMode.Decade; + SetCurrentValue(DisplayModeProperty, CalendarMode.Decade); } else { @@ -1770,8 +1753,8 @@ namespace Avalonia.Controls { if (ctrl) { - DisplayDate = SelectedMonth; - DisplayMode = CalendarMode.Month; + SetCurrentValue(DisplayDateProperty, SelectedMonth); + SetCurrentValue(DisplayModeProperty, CalendarMode.Month); } else { @@ -1785,7 +1768,7 @@ namespace Avalonia.Controls if (ctrl) { SelectedMonth = SelectedYear; - DisplayMode = CalendarMode.Year; + SetCurrentValue(DisplayModeProperty, CalendarMode.Year); } else { @@ -1850,14 +1833,14 @@ namespace Avalonia.Controls { case CalendarMode.Year: { - DisplayDate = SelectedMonth; - DisplayMode = CalendarMode.Month; + SetCurrentValue(DisplayDateProperty, SelectedMonth); + SetCurrentValue(DisplayModeProperty, CalendarMode.Month); return true; } case CalendarMode.Decade: { SelectedMonth = SelectedYear; - DisplayMode = CalendarMode.Year; + SetCurrentValue(DisplayModeProperty, CalendarMode.Year); return true; } } @@ -2103,7 +2086,8 @@ namespace Avalonia.Controls /// public Calendar() { - UpdateDisplayDate(this, this.DisplayDate, DateTime.MinValue); + SetCurrentValue(DisplayDateProperty, DateTime.Today); + UpdateDisplayDate(this, DisplayDate, DateTime.MinValue); BlackoutDates = new CalendarBlackoutDatesCollection(this); SelectedDates = new SelectedDatesCollection(this); RemovedItems = new Collection(); diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 3d436b4485..2e3f1f96ce 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -41,7 +41,6 @@ namespace Avalonia.Controls.Primitives private Button? _headerButton; private Button? _nextButton; private Button? _previousButton; - private ITemplate? _dayTitleTemplate; private DateTime _currentMonth; private bool _isMouseLeftButtonDown; @@ -61,17 +60,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(HeaderBackgroundProperty, value); } } - public static readonly DirectProperty?> DayTitleTemplateProperty = - AvaloniaProperty.RegisterDirect?>( + public static readonly StyledProperty?> DayTitleTemplateProperty = + AvaloniaProperty.Register?>( nameof(DayTitleTemplate), - o => o.DayTitleTemplate, - (o,v) => o.DayTitleTemplate = v, defaultBindingMode: BindingMode.OneTime); public ITemplate? DayTitleTemplate { - get { return _dayTitleTemplate; } - set { SetAndRaise(DayTitleTemplateProperty, ref _dayTitleTemplate, value); } + get => GetValue(DayTitleTemplateProperty); + set => SetValue(DayTitleTemplateProperty, value); } /// @@ -176,9 +173,8 @@ namespace Avalonia.Controls.Primitives for (int i = 0; i < Calendar.RowsPerMonth; i++) { - if (_dayTitleTemplate != null) + if (DayTitleTemplate?.Build() is Control cell) { - var cell = _dayTitleTemplate.Build(); cell.DataContext = string.Empty; cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs index 6c2356b411..5ff04b1a99 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs @@ -11,29 +11,22 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty DisplayDateProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDate), - o => o.DisplayDate, - (o, v) => o.DisplayDate = v); + public static readonly StyledProperty DisplayDateProperty = + AvaloniaProperty.Register(nameof(DisplayDate)); /// /// Defines the property. /// - public static readonly DirectProperty DisplayDateStartProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateStart), - o => o.DisplayDateStart, - (o, v) => o.DisplayDateStart = v); + public static readonly StyledProperty DisplayDateStartProperty = + AvaloniaProperty.Register( + nameof(DisplayDateStart)); /// /// Defines the property. /// - public static readonly DirectProperty DisplayDateEndProperty = - AvaloniaProperty.RegisterDirect( - nameof(DisplayDateEnd), - o => o.DisplayDateEnd, - (o, v) => o.DisplayDateEnd = v); + public static readonly StyledProperty DisplayDateEndProperty = + AvaloniaProperty.Register( + nameof(DisplayDateEnd)); /// /// Defines the property. @@ -44,11 +37,9 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsDropDownOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsDropDownOpen), - o => o.IsDropDownOpen, - (o, v) => o.IsDropDownOpen = v); + public static readonly StyledProperty IsDropDownOpenProperty = + AvaloniaProperty.Register( + nameof(IsDropDownOpen)); /// /// Defines the property. @@ -88,11 +79,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect( - nameof(Text), - o => o.Text, - (o, v) => o.Text = v); + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text)); /// /// Defines the property. @@ -141,8 +129,8 @@ namespace Avalonia.Controls /// public DateTime DisplayDate { - get => _displayDate; - set => SetAndRaise(DisplayDateProperty, ref _displayDate, value); + get => GetValue(DisplayDateProperty); + set => SetValue(DisplayDateProperty, value); } /// @@ -151,8 +139,8 @@ namespace Avalonia.Controls /// The first date to display. public DateTime? DisplayDateStart { - get => _displayDateStart; - set => SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); + get => GetValue(DisplayDateStartProperty); + set => SetValue(DisplayDateStartProperty, value); } /// @@ -161,8 +149,8 @@ namespace Avalonia.Controls /// The last date to display. public DateTime? DisplayDateEnd { - get => _displayDateEnd; - set => SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); + get => GetValue(DisplayDateEndProperty); + set => SetValue(DisplayDateEndProperty, value); } /// @@ -188,8 +176,8 @@ namespace Avalonia.Controls /// public bool IsDropDownOpen { - get => _isDropDownOpen; - set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); + get => GetValue(IsDropDownOpenProperty); + set => SetValue(IsDropDownOpenProperty, value); } /// @@ -264,8 +252,8 @@ namespace Avalonia.Controls /// public string? Text { - get => _text; - set => SetAndRaise(TextProperty, ref _text, value); + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); } /// diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index 869bdeabea..3f5d355b71 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -45,12 +45,7 @@ namespace Avalonia.Controls private DateTime? _onOpenSelectedDate; private bool _settingSelectedDate; - private DateTime _displayDate; - private DateTime? _displayDateStart; - private DateTime? _displayDateEnd; - private bool _isDropDownOpen; private DateTime? _selectedDate; - private string? _text; private bool _suspendTextChangeHandler; private bool _isPopupClosing; private bool _ignoreButtonClick; @@ -92,9 +87,9 @@ namespace Avalonia.Controls /// public CalendarDatePicker() { - FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek; + SetCurrentValue(FirstDayOfWeekProperty, DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek); _defaultText = string.Empty; - DisplayDate = DateTime.Today; + SetCurrentValue(DisplayDateProperty, DateTime.Today); } /// @@ -257,7 +252,7 @@ namespace Avalonia.Controls Threading.Dispatcher.UIThread.InvokeAsync(() => { _settingSelectedDate = true; - Text = DateTimeToString(day); + SetCurrentValue(TextProperty, DateTimeToString(day)); _settingSelectedDate = false; OnDateSelected(addedDate, removedDate); }); @@ -268,7 +263,7 @@ namespace Avalonia.Controls // be changed by the Calendar if ((day.Month != DisplayDate.Month || day.Year != DisplayDate.Year) && (_calendar == null || !_calendar.CalendarDatePickerDisplayDateFlag)) { - DisplayDate = day; + SetCurrentValue(DisplayDateProperty, day); } if(_calendar != null) @@ -317,7 +312,7 @@ namespace Avalonia.Controls if (!_settingSelectedDate) { _settingSelectedDate = true; - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); _settingSelectedDate = false; } } @@ -400,7 +395,7 @@ namespace Avalonia.Controls DateTime? newDate = DateTimeHelper.AddDays(selectedDate, e.Delta.Y > 0 ? -1 : 1); if (newDate.HasValue && Calendar.IsValidDateSelection(_calendar, newDate.Value)) { - SelectedDate = newDate; + SetCurrentValue(SelectedDateProperty, newDate); e.Handled = true; } } @@ -478,7 +473,7 @@ namespace Avalonia.Controls { if (SelectedDate.HasValue) { - Text = DateTimeToString(SelectedDate.Value); + SetCurrentValue(TextProperty, DateTimeToString(SelectedDate.Value)); } else if (string.IsNullOrEmpty(_textBox.Text)) { @@ -491,7 +486,7 @@ namespace Avalonia.Controls if (date != null) { string? s = DateTimeToString((DateTime)date); - Text = s; + SetCurrentValue(TextProperty, s); } } } @@ -547,7 +542,7 @@ namespace Avalonia.Controls private void Calendar_DayButtonMouseUp(object? sender, PointerReleasedEventArgs e) { Focus(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } private void Calendar_DisplayDateChanged(object? sender, CalendarDateChangedEventArgs e) @@ -564,13 +559,13 @@ namespace Avalonia.Controls if (e.AddedItems.Count > 0 && SelectedDate.HasValue && DateTime.Compare((DateTime)e.AddedItems[0]!, SelectedDate.Value) != 0) { - SelectedDate = (DateTime?)e.AddedItems[0]; + SetCurrentValue(SelectedDateProperty, (DateTime?)e.AddedItems[0]); } else { if (e.AddedItems.Count == 0) { - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); return; } @@ -578,7 +573,7 @@ namespace Avalonia.Controls { if (e.AddedItems.Count > 0) { - SelectedDate = (DateTime?)e.AddedItems[0]; + SetCurrentValue(SelectedDateProperty, (DateTime?)e.AddedItems[0]); } } } @@ -600,18 +595,18 @@ namespace Avalonia.Controls && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape)) { Focus(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); if (e.Key == Key.Escape) { - SelectedDate = _onOpenSelectedDate; + SetCurrentValue(SelectedDateProperty, _onOpenSelectedDate); } } } private void TextBox_GotFocus(object? sender, RoutedEventArgs e) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } private void TextBox_KeyDown(object? sender, KeyEventArgs e) @@ -627,7 +622,7 @@ namespace Avalonia.Controls if (_textBox != null) { _suspendTextChangeHandler = true; - Text = _textBox.Text; + SetCurrentValue(TextProperty, _textBox.Text); _suspendTextChangeHandler = false; } } @@ -660,7 +655,7 @@ namespace Avalonia.Controls private void PopUp_Closed(object? sender, EventArgs e) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); if(!_isPopupClosing) { @@ -678,12 +673,12 @@ namespace Avalonia.Controls if (IsDropDownOpen) { Focus(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } else { SetSelectedDate(); - IsDropDownOpen = true; + SetCurrentValue(IsDropDownOpenProperty, true); _calendar!.Focus(); } } @@ -821,14 +816,14 @@ namespace Avalonia.Controls if (SelectedDate != d) { - SelectedDate = d; + SetCurrentValue(SelectedDateProperty, d); } } else { if (SelectedDate != null) { - SelectedDate = null; + SetCurrentValue(SelectedDateProperty, null); } } } @@ -838,7 +833,7 @@ namespace Avalonia.Controls if (SelectedDate != d) { - SelectedDate = d; + SetCurrentValue(SelectedDateProperty, d); } } } @@ -884,7 +879,7 @@ namespace Avalonia.Controls if (string.IsNullOrEmpty(Watermark) && !UseFloatingWatermark) { DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat(); - Text = string.Empty; + SetCurrentValue(TextProperty, string.Empty); _defaultText = string.Empty; var watermarkFormat = "<{0}>"; string watermarkText; diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 17a6ad7a09..f6e4b32d6b 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -35,11 +35,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsDropDownOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsDropDownOpen), - o => o.IsDropDownOpen, - (o, v) => o.IsDropDownOpen = v); + public static readonly StyledProperty IsDropDownOpenProperty = + AvaloniaProperty.Register(nameof(IsDropDownOpen)); /// /// Defines the property. @@ -77,7 +74,6 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); - private bool _isDropDownOpen; private Popup? _popup; private object? _selectionBoxItem; private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable(); @@ -107,8 +103,8 @@ namespace Avalonia.Controls /// public bool IsDropDownOpen { - get => _isDropDownOpen; - set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); + get => GetValue(IsDropDownOpenProperty); + set => SetValue(IsDropDownOpenProperty, value); } /// @@ -123,10 +119,10 @@ namespace Avalonia.Controls /// /// Gets or sets the item to display as the control's content. /// - protected object? SelectionBoxItem + public object? SelectionBoxItem { get => _selectionBoxItem; - set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); + protected set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } /// @@ -191,23 +187,23 @@ namespace Avalonia.Controls if ((e.Key == Key.F4 && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == false) || ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt))) { - IsDropDownOpen = !IsDropDownOpen; + SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); e.Handled = true; } else if (IsDropDownOpen && e.Key == Key.Escape) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); e.Handled = true; } else if (!IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) { - IsDropDownOpen = true; + SetCurrentValue(IsDropDownOpenProperty, true); e.Handled = true; } else if (IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) { SelectFocusedItem(); - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); e.Handled = true; } else if (!IsDropDownOpen) @@ -291,7 +287,7 @@ namespace Avalonia.Controls } else { - IsDropDownOpen = !IsDropDownOpen; + SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); e.Handled = true; } } @@ -390,7 +386,7 @@ namespace Avalonia.Controls { if (!isVisible && IsDropDownOpen) { - IsDropDownOpen = false; + SetCurrentValue(IsDropDownOpenProperty, false); } } diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index bb05cd1b1f..118183102a 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -1,7 +1,6 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Layout; @@ -29,65 +28,56 @@ namespace Avalonia.Controls /// /// Define the Property /// - public static readonly DirectProperty DayFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(DayFormat), - x => x.DayFormat, (x, v) => x.DayFormat = v); + public static readonly StyledProperty DayFormatProperty = + AvaloniaProperty.Register(nameof(DayFormat), "%d"); /// /// Defines the Property /// - public static readonly DirectProperty DayVisibleProperty = - AvaloniaProperty.RegisterDirect(nameof(DayVisible), - x => x.DayVisible, (x, v) => x.DayVisible = v); + public static readonly StyledProperty DayVisibleProperty = + AvaloniaProperty.Register(nameof(DayVisible), true); /// /// Defines the Property /// - public static readonly DirectProperty MaxYearProperty = - AvaloniaProperty.RegisterDirect(nameof(MaxYear), - x => x.MaxYear, (x, v) => x.MaxYear = v); + public static readonly StyledProperty MaxYearProperty = + AvaloniaProperty.Register(nameof(MaxYear), DateTimeOffset.MaxValue, coerce: CoerceMaxYear); /// /// Defines the Property /// - public static readonly DirectProperty MinYearProperty = - AvaloniaProperty.RegisterDirect(nameof(MinYear), - x => x.MinYear, (x, v) => x.MinYear = v); + public static readonly StyledProperty MinYearProperty = + AvaloniaProperty.Register(nameof(MinYear), DateTimeOffset.MinValue, coerce: CoerceMinYear); /// /// Defines the Property /// - public static readonly DirectProperty MonthFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(MonthFormat), - x => x.MonthFormat, (x, v) => x.MonthFormat = v); + public static readonly StyledProperty MonthFormatProperty = + AvaloniaProperty.Register(nameof(MonthFormat), "MMMM"); /// /// Defines the Property /// - public static readonly DirectProperty MonthVisibleProperty = - AvaloniaProperty.RegisterDirect(nameof(MonthVisible), - x => x.MonthVisible, (x, v) => x.MonthVisible = v); + public static readonly StyledProperty MonthVisibleProperty = + AvaloniaProperty.Register(nameof(MonthVisible), true); /// /// Defines the Property /// - public static readonly DirectProperty YearFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(YearFormat), - x => x.YearFormat, (x, v) => x.YearFormat = v); + public static readonly StyledProperty YearFormatProperty = + AvaloniaProperty.Register(nameof(YearFormat), "yyyy"); /// /// Defines the Property /// - public static readonly DirectProperty YearVisibleProperty = - AvaloniaProperty.RegisterDirect(nameof(YearVisible), - x => x.YearVisible, (x, v) => x.YearVisible = v); + public static readonly StyledProperty YearVisibleProperty = + AvaloniaProperty.Register(nameof(YearVisible), true); /// /// Defines the Property /// - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect(nameof(SelectedDate), - x => x.SelectedDate, (x, v) => x.SelectedDate = v, + public static readonly StyledProperty SelectedDateProperty = + AvaloniaProperty.Register(nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay); // Template Items @@ -103,28 +93,20 @@ namespace Avalonia.Controls private bool _areControlsAvailable; - private string _dayFormat = "%d"; - private bool _dayVisible = true; - private DateTimeOffset _maxYear; - private DateTimeOffset _minYear; - private string _monthFormat = "MMMM"; - private bool _monthVisible = true; - private string _yearFormat = "yyyy"; - private bool _yearVisible = true; - private DateTimeOffset? _selectedDate; - public DatePicker() { PseudoClasses.Set(":hasnodate", true); var now = DateTimeOffset.Now; - _minYear = new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset); - _maxYear = new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset); + SetCurrentValue(MinYearProperty, new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset)); + SetCurrentValue(MaxYearProperty, new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset)); } + private static void OnGridVisibilityChanged(DatePicker sender, AvaloniaPropertyChangedEventArgs e) => sender.SetGrid(); + public string DayFormat { - get => _dayFormat; - set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + get => GetValue(DayFormatProperty); + set => SetValue(DayFormatProperty, value); } /// @@ -132,12 +114,8 @@ namespace Avalonia.Controls /// public bool DayVisible { - get => _dayVisible; - set - { - SetAndRaise(DayVisibleProperty, ref _dayVisible, value); - SetGrid(); - } + get => GetValue(DayVisibleProperty); + set => SetValue(DayVisibleProperty, value); } /// @@ -145,16 +123,24 @@ namespace Avalonia.Controls /// public DateTimeOffset MaxYear { - get => _maxYear; - set - { - if (value < MinYear) - throw new InvalidOperationException("MaxDate cannot be less than MinDate"); - SetAndRaise(MaxYearProperty, ref _maxYear, value); + get => GetValue(MaxYearProperty); + set => SetValue(MaxYearProperty, value); + } - if (SelectedDate.HasValue && SelectedDate.Value > value) - SelectedDate = value; + private static DateTimeOffset CoerceMaxYear(AvaloniaObject sender, DateTimeOffset value) + { + if (value < sender.GetValue(MinYearProperty)) + { + throw new InvalidOperationException($"{MaxYearProperty.Name} cannot be less than {MinYearProperty.Name}"); } + + return value; + } + + private void OnMaxYearChanged(DateTimeOffset? value) + { + if (SelectedDate.HasValue && SelectedDate.Value > value) + SetCurrentValue(SelectedDateProperty, value); } /// @@ -162,16 +148,24 @@ namespace Avalonia.Controls /// public DateTimeOffset MinYear { - get => _minYear; - set - { - if (value > MaxYear) - throw new InvalidOperationException("MinDate cannot be greater than MaxDate"); - SetAndRaise(MinYearProperty, ref _minYear, value); + get => GetValue(MinYearProperty); + set => SetValue(MinYearProperty, value); + } - if (SelectedDate.HasValue && SelectedDate.Value < value) - SelectedDate = value; + private static DateTimeOffset CoerceMinYear(AvaloniaObject sender, DateTimeOffset value) + { + if (value > sender.GetValue(MaxYearProperty)) + { + throw new InvalidOperationException($"{MinYearProperty.Name} cannot be greater than {MaxYearProperty.Name}"); } + + return value; + } + + private void OnMinYearChanged(DateTimeOffset? value) + { + if (SelectedDate.HasValue && SelectedDate.Value < value) + SetCurrentValue(SelectedDateProperty, value); } /// @@ -179,8 +173,8 @@ namespace Avalonia.Controls /// public string MonthFormat { - get => _monthFormat; - set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + get => GetValue(MonthFormatProperty); + set => SetValue(MonthFormatProperty, value); } /// @@ -188,12 +182,8 @@ namespace Avalonia.Controls /// public bool MonthVisible { - get => _monthVisible; - set - { - SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); - SetGrid(); - } + get => GetValue(MonthVisibleProperty); + set => SetValue(MonthVisibleProperty, value); } /// @@ -201,8 +191,8 @@ namespace Avalonia.Controls /// public string YearFormat { - get => _yearFormat; - set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + get => GetValue(YearFormatProperty); + set => SetValue(YearFormatProperty, value); } /// @@ -210,12 +200,8 @@ namespace Avalonia.Controls /// public bool YearVisible { - get => _yearVisible; - set - { - SetAndRaise(YearVisibleProperty, ref _yearVisible, value); - SetGrid(); - } + get => GetValue(YearVisibleProperty); + set => SetValue(YearVisibleProperty, value); } /// @@ -223,14 +209,8 @@ namespace Avalonia.Controls /// public DateTimeOffset? SelectedDate { - get => _selectedDate; - set - { - var old = _selectedDate; - SetAndRaise(SelectedDateProperty, ref _selectedDate, value); - SetSelectedDateText(); - OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(old, value)); - } + get => GetValue(SelectedDateProperty); + set => SetValue(SelectedDateProperty, value); } /// @@ -287,6 +267,31 @@ namespace Avalonia.Controls } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DayVisibleProperty || change.Property == MonthVisibleProperty || change.Property == YearVisibleProperty) + { + SetGrid(); + } + else if (change.Property == MaxYearProperty) + { + OnMaxYearChanged(change.GetNewValue()); + } + else if (change.Property == MinYearProperty) + { + OnMinYearChanged(change.GetNewValue()); + } + else if (change.Property == SelectedDateProperty) + { + SetSelectedDateText(); + + var (oldValue, newValue) = change.GetOldAndNewValue(); + OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(oldValue, newValue)); + } + } + private void OnDismissPicker(object? sender, EventArgs e) { _popup!.Close(); @@ -296,7 +301,7 @@ namespace Avalonia.Controls private void OnConfirmed(object? sender, EventArgs e) { _popup!.Close(); - SelectedDate = _presenter!.Date; + SetCurrentValue(SelectedDateProperty, _presenter!.Date); } private void SetGrid() diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs index 2057480490..0ae743f30a 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -35,65 +35,72 @@ namespace Avalonia.Controls /// /// Defines the Property /// - public static readonly DirectProperty DateProperty = - AvaloniaProperty.RegisterDirect(nameof(Date), - x => x.Date, (x, v) => x.Date = v); + public static readonly StyledProperty DateProperty = + AvaloniaProperty.Register(nameof(Date), coerce: CoerceDate); + + private static DateTimeOffset CoerceDate(AvaloniaObject sender, DateTimeOffset value) + { + var max = sender.GetValue(MaxYearProperty); + if (value > max) + { + return max; + } + var min = sender.GetValue(MinYearProperty); + if (value < min) + { + return min; + } + + return value; + } /// /// Defines the Property /// - public static readonly DirectProperty DayFormatProperty = - DatePicker.DayFormatProperty.AddOwner(x => - x.DayFormat, (x, v) => x.DayFormat = v); + public static readonly StyledProperty DayFormatProperty = + DatePicker.DayFormatProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty DayVisibleProperty = - DatePicker.DayVisibleProperty.AddOwner(x => - x.DayVisible, (x, v) => x.DayVisible = v); + public static readonly StyledProperty DayVisibleProperty = + DatePicker.DayVisibleProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MaxYearProperty = - DatePicker.MaxYearProperty.AddOwner(x => - x.MaxYear, (x, v) => x.MaxYear = v); + public static readonly StyledProperty MaxYearProperty = + DatePicker.MaxYearProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MinYearProperty = - DatePicker.MinYearProperty.AddOwner(x => - x.MinYear, (x, v) => x.MinYear = v); + public static readonly StyledProperty MinYearProperty = + DatePicker.MinYearProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MonthFormatProperty = - DatePicker.MonthFormatProperty.AddOwner(x => - x.MonthFormat, (x, v) => x.MonthFormat = v); + public static readonly StyledProperty MonthFormatProperty = + DatePicker.MonthFormatProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty MonthVisibleProperty = - DatePicker.MonthVisibleProperty.AddOwner(x => - x.MonthVisible, (x, v) => x.MonthVisible = v); + public static readonly StyledProperty MonthVisibleProperty = + DatePicker.MonthVisibleProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty YearFormatProperty = - DatePicker.YearFormatProperty.AddOwner(x => - x.YearFormat, (x, v) => x.YearFormat = v); + public static readonly StyledProperty YearFormatProperty = + DatePicker.YearFormatProperty.AddOwner(); /// /// Defines the Property /// - public static readonly DirectProperty YearVisibleProperty = - DatePicker.YearVisibleProperty.AddOwner(x => - x.YearVisible, (x, v) => x.YearVisible = v); + public static readonly StyledProperty YearVisibleProperty = + DatePicker.YearVisibleProperty.AddOwner(); // Template Items private Grid? _pickerContainer; @@ -114,15 +121,6 @@ namespace Avalonia.Controls private Button? _dayDownButton; private Button? _yearDownButton; - private DateTimeOffset _date; - private string _dayFormat = "%d"; - private bool _dayVisible = true; - private DateTimeOffset _maxYear; - private DateTimeOffset _minYear; - private string _monthFormat = "MMMM"; - private bool _monthVisible = true; - private string _yearFormat = "yyyy"; - private bool _yearVisible = true; private DateTimeOffset _syncDate; private readonly GregorianCalendar _calendar; @@ -131,11 +129,20 @@ namespace Avalonia.Controls public DatePickerPresenter() { var now = DateTimeOffset.Now; - _minYear = new DateTimeOffset(now.Year - 100, 1, 1, 0, 0, 0, now.Offset); - _maxYear = new DateTimeOffset(now.Year + 100, 12, 31, 0, 0, 0, now.Offset); - _date = now; + SetCurrentValue(MinYearProperty, new DateTimeOffset(now.Year - 100, 1, 1, 0, 0, 0, now.Offset)); + SetCurrentValue(MaxYearProperty, new DateTimeOffset(now.Year + 100, 12, 31, 0, 0, 0, now.Offset)); + SetCurrentValue(DateProperty, now); _calendar = new GregorianCalendar(); - KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); + } + + static DatePickerPresenter() + { + KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); + } + + private static void OnDateRangeChanged(DatePickerPresenter sender, AvaloniaPropertyChangedEventArgs e) + { + sender.CoerceValue(DateProperty); } /// @@ -143,13 +150,14 @@ namespace Avalonia.Controls /// public DateTimeOffset Date { - get => _date; - set - { - SetAndRaise(DateProperty, ref _date, value); - _syncDate = Date; - InitPicker(); - } + get => GetValue(DateProperty); + set => SetValue(DateProperty, value); + } + + private void OnDateChanged(DateTimeOffset newValue) + { + _syncDate = newValue; + InitPicker(); } /// @@ -157,8 +165,8 @@ namespace Avalonia.Controls /// public string DayFormat { - get => _dayFormat; - set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + get => GetValue(DayFormatProperty); + set => SetValue(DayFormatProperty, value); } /// @@ -166,11 +174,8 @@ namespace Avalonia.Controls /// public bool DayVisible { - get => _dayVisible; - set - { - SetAndRaise(DayVisibleProperty, ref _dayVisible, value); - } + get => GetValue(DayVisibleProperty); + set => SetValue(DayVisibleProperty, value); } /// @@ -178,16 +183,8 @@ namespace Avalonia.Controls /// public DateTimeOffset MaxYear { - get => _maxYear; - set - { - if (value < MinYear) - throw new InvalidOperationException("MaxDate cannot be less than MinDate"); - SetAndRaise(MaxYearProperty, ref _maxYear, value); - - if (Date > value) - Date = value; - } + get => GetValue(MaxYearProperty); + set => SetValue(MaxYearProperty, value); } /// @@ -195,16 +192,8 @@ namespace Avalonia.Controls /// public DateTimeOffset MinYear { - get => _minYear; - set - { - if (value > MaxYear) - throw new InvalidOperationException("MinDate cannot be greater than MaxDate"); - SetAndRaise(MinYearProperty, ref _minYear, value); - - if (Date < value) - Date = value; - } + get => GetValue(MinYearProperty); + set => SetValue(MinYearProperty, value); } /// @@ -212,8 +201,8 @@ namespace Avalonia.Controls /// public string MonthFormat { - get => _monthFormat; - set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + get => GetValue(MonthFormatProperty); + set => SetValue(MonthFormatProperty, value); } /// @@ -221,11 +210,8 @@ namespace Avalonia.Controls /// public bool MonthVisible { - get => _monthVisible; - set - { - SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); - } + get => GetValue(MonthVisibleProperty); + set => SetValue(MonthVisibleProperty, value); } /// @@ -233,8 +219,8 @@ namespace Avalonia.Controls /// public string YearFormat { - get => _yearFormat; - set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + get => GetValue(YearFormatProperty); + set => SetValue(YearFormatProperty, value); } /// @@ -242,11 +228,8 @@ namespace Avalonia.Controls /// public bool YearVisible { - get => _yearVisible; - set - { - SetAndRaise(YearVisibleProperty, ref _yearVisible, value); - } + get => GetValue(YearVisibleProperty); + set => SetValue(YearVisibleProperty, value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -317,6 +300,20 @@ namespace Avalonia.Controls InitPicker(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DateProperty) + { + OnDateChanged(change.GetNewValue()); + } + else if (change.Property == MaxYearProperty || change.Property == MinYearProperty) + { + OnDateRangeChanged(this, change); + } + } + protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) @@ -334,7 +331,7 @@ namespace Avalonia.Controls } break; case Key.Enter: - Date = _syncDate; + SetCurrentValue(DateProperty, _syncDate); OnConfirmed(); e.Handled = true; break; @@ -381,13 +378,13 @@ namespace Avalonia.Controls _monthSelector.SelectedValue = dt.Month; _monthSelector.FormatDate = dt.Date; } - + if (YearVisible) { _yearSelector.SelectedValue = dt.Year; _yearSelector.FormatDate = dt.Date; } - + _suppressUpdateSelection = false; SetInitialFocus(); @@ -471,7 +468,7 @@ namespace Avalonia.Controls private void OnAcceptButtonClicked(object? sender, RoutedEventArgs e) { - Date = _syncDate; + SetCurrentValue(DateProperty, _syncDate); OnConfirmed(); } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index a7a6881fe5..2f49a44b8c 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -1,7 +1,6 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; using System; @@ -30,23 +29,20 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty MinuteIncrementProperty = - AvaloniaProperty.RegisterDirect(nameof(MinuteIncrement), - x => x.MinuteIncrement, (x, v) => x.MinuteIncrement = v); + public static readonly StyledProperty MinuteIncrementProperty = + AvaloniaProperty.Register(nameof(MinuteIncrement), 1, coerce: CoerceMinuteIncrement); /// /// Defines the property /// - public static readonly DirectProperty ClockIdentifierProperty = - AvaloniaProperty.RegisterDirect(nameof(ClockIdentifier), - x => x.ClockIdentifier, (x, v) => x.ClockIdentifier = v); + public static readonly StyledProperty ClockIdentifierProperty = + AvaloniaProperty.Register(nameof(ClockIdentifier), "12HourClock", coerce: CoerceClockIdentifier); /// /// Defines the property /// - public static readonly DirectProperty SelectedTimeProperty = - AvaloniaProperty.RegisterDirect(nameof(SelectedTime), - x => x.SelectedTime, (x, v) => x.SelectedTime = v, + public static readonly StyledProperty SelectedTimeProperty = + AvaloniaProperty.Register(nameof(SelectedTime), defaultBindingMode: BindingMode.TwoWay); // Template Items @@ -63,17 +59,13 @@ namespace Avalonia.Controls private Grid? _contentGrid; private Popup? _popup; - private TimeSpan? _selectedTime; - private int _minuteIncrement = 1; - private string _clockIdentifier = "12HourClock"; - public TimePicker() { PseudoClasses.Set(":hasnotime", true); var timePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern; if (timePattern.IndexOf("H") != -1) - _clockIdentifier = "24HourClock"; + SetCurrentValue(ClockIdentifierProperty, "24HourClock"); } /// @@ -81,14 +73,16 @@ namespace Avalonia.Controls /// public int MinuteIncrement { - get => _minuteIncrement; - set - { - if (value < 1 || value > 59) - throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59"); - SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value); - SetSelectedTimeText(); - } + get => GetValue(MinuteIncrementProperty); + set => SetValue(MinuteIncrementProperty, value); + } + + private static int CoerceMinuteIncrement(AvaloniaObject sender, int value) + { + if (value < 1 || value > 59) + throw new ArgumentOutOfRangeException(null, "1 >= MinuteIncrement <= 59"); + + return value; } /// @@ -96,15 +90,17 @@ namespace Avalonia.Controls /// public string ClockIdentifier { - get => _clockIdentifier; - set - { - if (!(string.IsNullOrEmpty(value) || value == "12HourClock" || value == "24HourClock")) - throw new ArgumentException("Invalid ClockIdentifier"); - SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value); - SetGrid(); - SetSelectedTimeText(); - } + + get => GetValue(ClockIdentifierProperty); + set => SetValue(ClockIdentifierProperty, value); + } + + private static string CoerceClockIdentifier(AvaloniaObject sender, string value) + { + if (!(string.IsNullOrEmpty(value) || value == "12HourClock" || value == "24HourClock")) + throw new ArgumentException("Invalid ClockIdentifier", default(string)); + + return value; } /// @@ -112,14 +108,8 @@ namespace Avalonia.Controls /// public TimeSpan? SelectedTime { - get => _selectedTime; - set - { - var old = _selectedTime; - SetAndRaise(SelectedTimeProperty, ref _selectedTime, value); - OnSelectedTimeChanged(old, value); - SetSelectedTimeText(); - } + get => GetValue(SelectedTimeProperty); + set => SetValue(SelectedTimeProperty, value); } /// @@ -173,6 +163,27 @@ namespace Avalonia.Controls } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MinuteIncrementProperty) + { + SetSelectedTimeText(); + } + else if (change.Property == ClockIdentifierProperty) + { + SetGrid(); + SetSelectedTimeText(); + } + else if (change.Property == SelectedTimeProperty) + { + var (oldValue, newValue) = change.GetOldAndNewValue(); + OnSelectedTimeChanged(oldValue, newValue); + SetSelectedTimeText(); + } + } + private void SetGrid() { if (_contentGrid == null) @@ -270,7 +281,7 @@ namespace Avalonia.Controls private void OnConfirmed(object? sender, EventArgs e) { _popup!.Close(); - SelectedTime = _presenter!.Time; + SetCurrentValue(SelectedTimeProperty, _presenter!.Time); } } } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index d6599c9f18..ba06e1b5e6 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -30,28 +30,29 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly DirectProperty MinuteIncrementProperty = - TimePicker.MinuteIncrementProperty.AddOwner(x => x.MinuteIncrement, - (x, v) => x.MinuteIncrement = v); + public static readonly StyledProperty MinuteIncrementProperty = + TimePicker.MinuteIncrementProperty.AddOwner(); /// /// Defines the property /// - public static readonly DirectProperty ClockIdentifierProperty = - TimePicker.ClockIdentifierProperty.AddOwner(x => x.ClockIdentifier, - (x, v) => x.ClockIdentifier = v); + public static readonly StyledProperty ClockIdentifierProperty = + TimePicker.ClockIdentifierProperty.AddOwner(); /// /// Defines the property /// - public static readonly DirectProperty TimeProperty = - AvaloniaProperty.RegisterDirect(nameof(Time), - x => x.Time, (x, v) => x.Time = v); + public static readonly StyledProperty TimeProperty = + AvaloniaProperty.Register(nameof(Time)); public TimePickerPresenter() { - Time = DateTime.Now.TimeOfDay; - KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); + SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay); + } + + static TimePickerPresenter() + { + KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); } // TemplateItems @@ -70,24 +71,13 @@ namespace Avalonia.Controls private Button? _minuteDownButton; private Button? _periodDownButton; - // Backing Fields - private TimeSpan _time; - private int _minuteIncrement = 1; - private string _clockIdentifier = "12HourClock"; - /// /// Gets or sets the minute increment in the selector /// public int MinuteIncrement { - get => _minuteIncrement; - set - { - if (value < 1 || value > 59) - throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59"); - SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value); - InitPicker(); - } + get => GetValue(MinuteIncrementProperty); + set => SetValue(MinuteIncrementProperty, value); } /// @@ -95,14 +85,8 @@ namespace Avalonia.Controls /// public string ClockIdentifier { - get => _clockIdentifier; - set - { - if (string.IsNullOrEmpty(value) || !(value == "12HourClock" || value == "24HourClock")) - throw new ArgumentException("Invalid ClockIdentifier"); - SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value); - InitPicker(); - } + get => GetValue(ClockIdentifierProperty); + set => SetValue(ClockIdentifierProperty, value); } /// @@ -110,12 +94,8 @@ namespace Avalonia.Controls /// public TimeSpan Time { - get => _time; - set - { - SetAndRaise(TimeProperty, ref _time, value); - InitPicker(); - } + get => GetValue(TimeProperty); + set => SetValue(TimeProperty, value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -162,6 +142,16 @@ namespace Avalonia.Controls InitPicker(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MinuteIncrementProperty || change.Property == ClockIdentifierProperty || change.Property == TimeProperty) + { + InitPicker(); + } + } + protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) @@ -197,7 +187,7 @@ namespace Avalonia.Controls hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr; } - Time = new TimeSpan(hr, min, 0); + SetCurrentValue(TimeProperty, new TimeSpan(hr, min, 0)); base.OnConfirmed(); } diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 12a096b105..fe9f5e64a8 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Documents this.ForEachItem( x => - { + { x.InlineHost = InlineHost; LogicalChildren?.Add(x); Invalidate(); @@ -92,10 +92,10 @@ namespace Avalonia.Controls.Documents public override void Add(Inline inline) { if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock._text)) - { + { base.Add(new Run(textBlock._text)); - textBlock._text = null; + textBlock._text = null; } base.Add(inline); @@ -159,7 +159,7 @@ namespace Avalonia.Controls.Documents oldParent.Remove(child); } - if(newParent != null) + if (newParent != null) { newParent.Add(child); } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 9d4abec549..36f8371702 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -35,17 +35,14 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property /// - public static readonly DirectProperty ShowModeProperty = - AvaloniaProperty.RegisterDirect(nameof(ShowMode), - x => x.ShowMode, (x, v) => x.ShowMode = v); + public static readonly StyledProperty ShowModeProperty = + AvaloniaProperty.Register(nameof(ShowMode)); /// /// Defines the property /// - public static readonly DirectProperty OverlayInputPassThroughElementProperty = - Popup.OverlayInputPassThroughElementProperty.AddOwner( - o => o._overlayInputPassThroughElement, - (o, v) => o._overlayInputPassThroughElement = v); + public static readonly StyledProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner(); /// /// Defines the AttachedFlyout property @@ -56,12 +53,10 @@ namespace Avalonia.Controls.Primitives private readonly Lazy _popupLazy; private bool _isOpen; private Control? _target; - private FlyoutShowMode _showMode = FlyoutShowMode.Standard; private Rect? _enlargedPopupRect; private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; private Action? _popupHostChangedHandler; - private IInputElement? _overlayInputPassThroughElement; static FlyoutBase() { @@ -98,8 +93,8 @@ namespace Avalonia.Controls.Primitives /// public FlyoutShowMode ShowMode { - get => _showMode; - set => SetAndRaise(ShowModeProperty, ref _showMode, value); + get => GetValue(ShowModeProperty); + set => SetValue(ShowModeProperty, value); } /// @@ -117,8 +112,8 @@ namespace Avalonia.Controls.Primitives /// public IInputElement? OverlayInputPassThroughElement { - get => _overlayInputPassThroughElement; - set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + get => GetValue(OverlayInputPassThroughElementProperty); + set => SetValue(OverlayInputPassThroughElementProperty, value); } IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; @@ -244,7 +239,7 @@ namespace Avalonia.Controls.Primitives { Popup.PlacementTarget = Target = placementTarget; ((ISetLogicalParent)Popup).SetParent(placementTarget); - Popup.SetValue(StyledElement.TemplatedParentProperty, placementTarget.TemplatedParent); + Popup.TemplatedParent = placementTarget.TemplatedParent; } if (Popup.Child == null) diff --git a/src/Avalonia.Controls/Label.cs b/src/Avalonia.Controls/Label.cs index 5c8a6e0a5b..487d816204 100644 --- a/src/Avalonia.Controls/Label.cs +++ b/src/Avalonia.Controls/Label.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Data; -using Avalonia.Input; +using Avalonia.Input; using Avalonia.Interactivity; namespace Avalonia.Controls @@ -18,13 +11,8 @@ namespace Avalonia.Controls /// /// Defines the Direct property /// - public static readonly DirectProperty TargetProperty = - AvaloniaProperty.RegisterDirect(nameof(Target), lbl => lbl.Target, (lbl, inp) => lbl.Target = inp); - - /// - /// Label focus target storage field - /// - private IInputElement? _target; + public static readonly StyledProperty TargetProperty = + AvaloniaProperty.Register(nameof(Target)); /// /// Label focus Target @@ -32,8 +20,8 @@ namespace Avalonia.Controls [ResolveByName] public IInputElement? Target { - get => _target; - set => SetAndRaise(TargetProperty, ref _target, value); + get => GetValue(TargetProperty); + set => SetValue(TargetProperty, value); } static Label() diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 5ff4148e5a..7b03c607b6 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -79,12 +79,12 @@ namespace Avalonia.Controls } public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); public NativeMenuItem? Parent { get => _parent; - set => SetAndRaise(ParentProperty, ref _parent, value); + internal set => SetAndRaise(ParentProperty, ref _parent, value); } public void Add(NativeMenuItemBase item) => _items.Add(item); diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 32fa574ee6..b7ce928b4b 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -4,36 +4,14 @@ using Avalonia.Input; using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Utilities; -using Avalonia.Reactive; namespace Avalonia.Controls { public class NativeMenuItem : NativeMenuItemBase, INativeMenuItemExporterEventsImplBridge { - private string? _header; - private KeyGesture? _gesture; - private bool _isEnabled = true; private ICommand? _command; - private bool _isChecked = false; - private NativeMenuItemToggleType _toggleType; - private IBitmap? _icon; private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; - private NativeMenu? _menu; - - static NativeMenuItem() - { - MenuProperty.Changed.Subscribe(args => - { - var item = (NativeMenuItem)args.Sender; - var value = args.NewValue.GetValueOrDefault()!; - if (value.Parent != null && value.Parent != item) - throw new InvalidOperationException("NativeMenu already has a parent"); - value.Parent = item; - }); - } - - class CanExecuteChangedSubscriber : IWeakEventSubscriber { private readonly NativeMenuItem _parent; @@ -60,71 +38,66 @@ namespace Avalonia.Controls Header = header; } - public static readonly DirectProperty MenuProperty = - AvaloniaProperty.RegisterDirect(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v); + public static readonly StyledProperty MenuProperty = + AvaloniaProperty.Register(nameof(Menu), coerce: CoerceMenu); [Content] public NativeMenu? Menu { - get => _menu; - set - { - if (value != null && value.Parent != null && value.Parent != this) - throw new InvalidOperationException("NativeMenu already has a parent"); - SetAndRaise(MenuProperty, ref _menu, value); - } + get => GetValue(MenuProperty); + set => SetValue(MenuProperty, value); } - public static readonly DirectProperty IconProperty = - AvaloniaProperty.RegisterDirect(nameof(Icon), o => o.Icon, (o, v) => o.Icon = v); + private static NativeMenu? CoerceMenu(AvaloniaObject sender, NativeMenu? value) + { + if (value != null && value.Parent != null && value.Parent != sender) + throw new InvalidOperationException("NativeMenu already has a parent"); + return value; + } + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); public IBitmap? Icon { - get => _icon; - set => SetAndRaise(IconProperty, ref _icon, value); + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); } - public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, (o, v) => o.Header = v); + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); public string? Header { - get => _header; - set => SetAndRaise(HeaderProperty, ref _header, value); + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); } - public static readonly DirectProperty GestureProperty = - AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o.Gesture, (o, v) => o.Gesture = v); + public static readonly StyledProperty GestureProperty = + AvaloniaProperty.Register(nameof(Gesture)); public KeyGesture? Gesture { - get => _gesture; - set => SetAndRaise(GestureProperty, ref _gesture, value); + get => GetValue(GestureProperty); + set => SetValue(GestureProperty, value); } - public static readonly DirectProperty IsCheckedProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsChecked), - o => o.IsChecked, - (o, v) => o.IsChecked = v); + public static readonly StyledProperty IsCheckedProperty = + AvaloniaProperty.Register(nameof(IsChecked)); public bool IsChecked { - get => _isChecked; - set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); + get => GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); } - public static readonly DirectProperty ToggleTypeProperty = - AvaloniaProperty.RegisterDirect( - nameof(ToggleType), - o => o.ToggleType, - (o, v) => o.ToggleType = v); + public static readonly StyledProperty ToggleTypeProperty = + AvaloniaProperty.Register(nameof(ToggleType)); public NativeMenuItemToggleType ToggleType { - get => _toggleType; - set => SetAndRaise(ToggleTypeProperty, ref _toggleType, value); + get => GetValue(ToggleTypeProperty); + set => SetValue(ToggleTypeProperty, value); } public static readonly DirectProperty CommandProperty = @@ -139,18 +112,18 @@ namespace Avalonia.Controls public static readonly StyledProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); - public static readonly DirectProperty IsEnabledProperty = - AvaloniaProperty.RegisterDirect(nameof(IsEnabled), o => o.IsEnabled, (o, v) => o.IsEnabled = v, true); + public static readonly StyledProperty IsEnabledProperty = + AvaloniaProperty.Register(nameof(IsEnabled), true); public bool IsEnabled { - get => _isEnabled; - set => SetAndRaise(IsEnabledProperty, ref _isEnabled, value); + get => GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); } void CanExecuteChanged() { - IsEnabled = _command?.CanExecute(CommandParameter) ?? true; + SetCurrentValue(IsEnabledProperty, Command?.CanExecute(CommandParameter) ?? true); } public bool HasClickHandlers => Click != null; @@ -196,8 +169,20 @@ namespace Avalonia.Controls Command.Execute(CommandParameter); } } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == MenuProperty && change.NewValue is NativeMenu newMenu) + { + if (newMenu.Parent != null && newMenu.Parent != this) + throw new InvalidOperationException("NativeMenu already has a parent"); + newMenu.Parent = this; + } + } } - + public enum NativeMenuItemToggleType { None, diff --git a/src/Avalonia.Controls/NativeMenuItemBase.cs b/src/Avalonia.Controls/NativeMenuItemBase.cs index 4946d16f01..70cb2b806e 100644 --- a/src/Avalonia.Controls/NativeMenuItemBase.cs +++ b/src/Avalonia.Controls/NativeMenuItemBase.cs @@ -12,12 +12,12 @@ namespace Avalonia.Controls } public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect("Parent", o => o.Parent, (o, v) => o.Parent = v); + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); public NativeMenu? Parent { get => _parent; - set => SetAndRaise(ParentProperty, ref _parent, value); + internal set => SetAndRaise(ParentProperty, ref _parent, value); } } } diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 663bd3358a..705d40380e 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -13,7 +13,6 @@ namespace Avalonia.Controls.Notifications [PseudoClasses(":error", ":information", ":success", ":warning")] public class NotificationCard : ContentControl { - private bool _isClosed; private bool _isClosing; static NotificationCard() @@ -84,15 +83,15 @@ namespace Avalonia.Controls.Notifications /// public bool IsClosed { - get { return _isClosed; } - set { SetAndRaise(IsClosedProperty, ref _isClosed, value); } + get => GetValue(IsClosedProperty); + set => SetValue(IsClosedProperty, value); } /// /// Defines the property. /// - public static readonly DirectProperty IsClosedProperty = - AvaloniaProperty.RegisterDirect(nameof(IsClosed), o => o.IsClosed, (o, v) => o.IsClosed = v); + public static readonly StyledProperty IsClosedProperty = + AvaloniaProperty.Register(nameof(IsClosed)); /// /// Defines the event. diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index ac4f699313..e676ec0759 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -43,16 +43,14 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ClipValueToMinMaxProperty = - AvaloniaProperty.RegisterDirect(nameof(ClipValueToMinMax), - updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); + public static readonly StyledProperty ClipValueToMinMaxProperty = + AvaloniaProperty.Register(nameof(ClipValueToMinMax)); /// /// Defines the property. /// - public static readonly DirectProperty NumberFormatProperty = - AvaloniaProperty.RegisterDirect(nameof(NumberFormat), o => o.NumberFormat, - (o, v) => o.NumberFormat = v, NumberFormatInfo.CurrentInfo); + public static readonly StyledProperty NumberFormatProperty = + AvaloniaProperty.Register(nameof(NumberFormat), NumberFormatInfo.CurrentInfo); /// /// Defines the property. @@ -87,9 +85,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ParsingNumberStyleProperty = - AvaloniaProperty.RegisterDirect(nameof(ParsingNumberStyle), - updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); + public static readonly StyledProperty ParsingNumberStyleProperty = + AvaloniaProperty.Register(nameof(ParsingNumberStyle), NumberStyles.Any); /// /// Defines the property. @@ -101,9 +98,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty TextConverterProperty = - AvaloniaProperty.RegisterDirect(nameof(TextConverter), - updown => updown.TextConverter, (o, v) => o.TextConverter = v, null, BindingMode.OneWay, false); + public static readonly StyledProperty TextConverterProperty = + AvaloniaProperty.Register(nameof(TextConverter), defaultBindingMode: BindingMode.OneWay); /// /// Defines the property. @@ -134,13 +130,9 @@ namespace Avalonia.Controls private decimal? _value; private string? _text; - private IValueConverter? _textConverter; private bool _internalValueSet; - private bool _clipValueToMinMax; private bool _isSyncingTextAndValueProperties; private bool _isTextChangedFromUI; - private NumberStyles _parsingNumberStyle = NumberStyles.Any; - private NumberFormatInfo? _numberFormat; /// /// Gets the Spinner template part. @@ -184,8 +176,8 @@ namespace Avalonia.Controls /// public bool ClipValueToMinMax { - get { return _clipValueToMinMax; } - set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } + get => GetValue(ClipValueToMinMaxProperty); + set => SetValue(ClipValueToMinMaxProperty, value); } /// @@ -193,8 +185,8 @@ namespace Avalonia.Controls /// public NumberFormatInfo? NumberFormat { - get { return _numberFormat; } - set { SetAndRaise(NumberFormatProperty, ref _numberFormat, value); } + get => GetValue(NumberFormatProperty); + set => SetValue(NumberFormatProperty, value); } /// @@ -249,8 +241,8 @@ namespace Avalonia.Controls /// public NumberStyles ParsingNumberStyle { - get { return _parsingNumberStyle; } - set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } + get => GetValue(ParsingNumberStyleProperty); + set => SetValue(ParsingNumberStyleProperty, value); } /// @@ -269,8 +261,8 @@ namespace Avalonia.Controls /// public IValueConverter? TextConverter { - get { return _textConverter; } - set { SetAndRaise(TextConverterProperty, ref _textConverter, value); } + get => GetValue(TextConverterProperty); + set => SetValue(TextConverterProperty, value); } /// @@ -475,7 +467,7 @@ namespace Avalonia.Controls } if (ClipValueToMinMax && Value.HasValue) { - Value = MathUtilities.Clamp(Value.Value, Minimum, Maximum); + SetCurrentValue(ValueProperty, MathUtilities.Clamp(Value.Value, Minimum, Maximum)); } } @@ -492,7 +484,7 @@ namespace Avalonia.Controls } if (ClipValueToMinMax && Value.HasValue) { - Value = MathUtilities.Clamp(Value.Value, Minimum, Maximum); + SetCurrentValue(ValueProperty, MathUtilities.Clamp(Value.Value, Minimum, Maximum)); } } @@ -676,7 +668,7 @@ namespace Avalonia.Controls result = Minimum; } - Value = MathUtilities.Clamp(result, Minimum, Maximum); + SetCurrentValue(ValueProperty, MathUtilities.Clamp(result, Minimum, Maximum)); } /// @@ -695,7 +687,7 @@ namespace Avalonia.Controls result = Maximum; } - Value = MathUtilities.Clamp(result, Minimum, Maximum); + SetCurrentValue(ValueProperty, MathUtilities.Clamp(result, Minimum, Maximum)); } /// @@ -862,7 +854,7 @@ namespace Avalonia.Controls _internalValueSet = true; try { - Value = value; + SetCurrentValue(ValueProperty, value); } finally { @@ -907,7 +899,7 @@ namespace Avalonia.Controls _isTextChangedFromUI = true; if (TextBox != null) { - Text = TextBox.Text; + SetCurrentValue(TextProperty, TextBox.Text); } } finally @@ -1026,7 +1018,7 @@ namespace Avalonia.Controls var newText = ConvertValueToText(); if (!Equals(Text, newText)) { - Text = newText; + SetCurrentValue(TextProperty, newText); } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index be61bb18a1..329a0fa6ab 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -156,16 +156,13 @@ namespace Avalonia.Controls.Presenters /// /// Defines the property /// - public static readonly DirectProperty RecognizesAccessKeyProperty = - AvaloniaProperty.RegisterDirect( - nameof(RecognizesAccessKey), - cp => cp.RecognizesAccessKey, (cp, value) => cp.RecognizesAccessKey = value); + public static readonly StyledProperty RecognizesAccessKeyProperty = + AvaloniaProperty.Register(nameof(RecognizesAccessKey)); private Control? _child; private bool _createdChild; private IRecyclingDataTemplate? _recyclingDataTemplate; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); - private bool _recognizesAccessKey; /// /// Initializes static members of the class. @@ -386,8 +383,8 @@ namespace Avalonia.Controls.Presenters /// public bool RecognizesAccessKey { - get => _recognizesAccessKey; - set => SetAndRaise(RecognizesAccessKeyProperty, ref _recognizesAccessKey, value); + get => GetValue(RecognizesAccessKeyProperty); + set => SetValue(RecognizesAccessKeyProperty, value); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 0a762c438d..a0020a0b6e 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -166,7 +166,7 @@ namespace Avalonia.Controls.Presenters } Panel = ItemsPanel.Build(); - Panel.SetValue(TemplatedParentProperty, TemplatedParent); + Panel.TemplatedParent = TemplatedParent; Panel.IsItemsHost = true; _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo; LogicalChildren.Add(Panel); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 9d443d9289..0c6c434713 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,7 +2,6 @@ using System; using System.ComponentModel; using Avalonia.Reactive; using Avalonia.Automation.Peers; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; @@ -41,11 +40,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsOpen), - o => o.IsOpen, - (o, v) => o.IsOpen = v); + public static readonly StyledProperty IsOpenProperty = + AvaloniaProperty.Register(nameof(IsOpen)); /// /// Defines the property. @@ -90,11 +86,8 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty OverlayDismissEventPassThroughProperty = AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough)); - public static readonly DirectProperty OverlayInputPassThroughElementProperty = - AvaloniaProperty.RegisterDirect( - nameof(OverlayInputPassThroughElement), - o => o.OverlayInputPassThroughElement, - (o, v) => o.OverlayInputPassThroughElement = v); + public static readonly StyledProperty OverlayInputPassThroughElementProperty = + AvaloniaProperty.Register(nameof(OverlayInputPassThroughElement)); /// /// Defines the property. @@ -121,10 +114,8 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(Topmost)); private bool _isOpenRequested; - private bool _isOpen; private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; - private IInputElement? _overlayInputPassThroughElement; private Action? _popupHostChangedHandler; /// @@ -209,8 +200,8 @@ namespace Avalonia.Controls.Primitives /// public bool IsOpen { - get { return _isOpen; } - set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } + get => GetValue(IsOpenProperty); + set => SetValue(IsOpenProperty, value); } /// @@ -301,8 +292,8 @@ namespace Avalonia.Controls.Primitives /// public IInputElement? OverlayInputPassThroughElement { - get => _overlayInputPassThroughElement; - set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + get => GetValue(OverlayInputPassThroughElementProperty); + set => SetValue(OverlayInputPassThroughElementProperty, value); } /// @@ -486,7 +477,7 @@ namespace Avalonia.Controls.Primitives using (BeginIgnoringIsOpen()) { - IsOpen = true; + SetCurrentValue(IsOpenProperty, true); } Opened?.Invoke(this, EventArgs.Empty); @@ -704,7 +695,7 @@ namespace Avalonia.Controls.Primitives { using (BeginIgnoringIsOpen()) { - IsOpen = false; + SetCurrentValue(IsOpenProperty, false); } return; @@ -717,7 +708,7 @@ namespace Avalonia.Controls.Primitives using (BeginIgnoringIsOpen()) { - IsOpen = false; + SetCurrentValue(IsOpenProperty, false); } Closed?.Invoke(this, EventArgs.Empty); diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 8253342782..e1cf25d89f 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -275,7 +275,7 @@ namespace Avalonia.Controls.Primitives { foreach (var child in this.GetTemplateChildren()) { - child.SetValue(TemplatedParentProperty, null); + child.TemplatedParent = null; ((ISetLogicalParent)child).SetParent(null); } @@ -377,7 +377,7 @@ namespace Avalonia.Controls.Primitives /// The templated parent to apply. internal static void ApplyTemplatedParent(StyledElement control, AvaloniaObject? templatedParent) { - control.SetValue(TemplatedParentProperty, templatedParent); + control.TemplatedParent = templatedParent; var children = control.LogicalChildren; var count = children.Count; diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 158c5d875b..dfaf7bbc45 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -15,12 +15,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly DirectProperty IsCheckedProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsChecked), - o => o.IsChecked, - (o, v) => o.IsChecked = v, - unsetValue: false, + public static readonly StyledProperty IsCheckedProperty = + AvaloniaProperty.Register(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay); /// @@ -64,8 +60,6 @@ namespace Avalonia.Controls.Primitives nameof(IsCheckedChanged), RoutingStrategies.Bubble); - private bool? _isChecked = false; - static ToggleButton() { } @@ -119,12 +113,8 @@ namespace Avalonia.Controls.Primitives /// public bool? IsChecked { - get => _isChecked; - set - { - SetAndRaise(IsCheckedProperty, ref _isChecked, value); - UpdatePseudoClasses(IsChecked); - } + get => GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); } /// @@ -147,28 +137,31 @@ namespace Avalonia.Controls.Primitives /// protected virtual void Toggle() { + bool? newValue; if (IsChecked.HasValue) { if (IsChecked.Value) { if (IsThreeState) { - IsChecked = null; + newValue = null; } else { - IsChecked = false; + newValue = false; } } else { - IsChecked = true; + newValue = true; } } else { - IsChecked = false; + newValue = false; } + + SetCurrentValue(IsCheckedProperty, newValue); } /// @@ -224,6 +217,8 @@ namespace Avalonia.Controls.Primitives { var newValue = change.GetNewValue(); + UpdatePseudoClasses(newValue); + #pragma warning disable CS0618 // Type or member is obsolete switch (newValue) { diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index 87772aced7..d4528fdb1c 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -98,31 +98,22 @@ namespace Avalonia.Controls } } - public static readonly DirectProperty GroupNameProperty = - AvaloniaProperty.RegisterDirect( - nameof(GroupName), - o => o.GroupName, - (o, v) => o.GroupName = v); + public static readonly StyledProperty GroupNameProperty = + AvaloniaProperty.Register(nameof(GroupName)); - private string? _groupName; private RadioButtonGroupManager? _groupManager; - public RadioButton() - { - this.GetObservable(IsCheckedProperty).Subscribe(IsCheckedChanged); - } - public string? GroupName { - get { return _groupName; } - set { SetGroupName(value); } + get => GetValue(GroupNameProperty); + set => SetValue(GroupNameProperty, value); } protected override void Toggle() { if (!IsChecked.GetValueOrDefault()) { - IsChecked = true; + SetCurrentValue(IsCheckedProperty, true); } } @@ -154,28 +145,38 @@ namespace Avalonia.Controls return new RadioButtonAutomationPeer(this); } - private void SetGroupName(string? newGroupName) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - var oldGroupName = GroupName; - if (newGroupName != oldGroupName) + base.OnPropertyChanged(change); + + if (change.Property == IsCheckedProperty) { - if (!string.IsNullOrEmpty(oldGroupName)) - { - _groupManager?.Remove(this, oldGroupName); - } - _groupName = newGroupName; - if (!string.IsNullOrEmpty(newGroupName)) + IsCheckedChanged(change.GetNewValue()); + } + else if (change.Property == GroupNameProperty) + { + var (oldValue, newValue) = change.GetOldAndNewValue(); + OnGroupNameChanged(oldValue, newValue); + } + } + + private void OnGroupNameChanged(string? oldGroupName, string? newGroupName) + { + if (!string.IsNullOrEmpty(oldGroupName)) + { + _groupManager?.Remove(this, oldGroupName); + } + if (!string.IsNullOrEmpty(newGroupName)) + { + if (_groupManager == null) { - if (_groupManager == null) - { - _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(this.GetVisualRoot()); - } - _groupManager.Add(this); + _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(this.GetVisualRoot()); } + _groupManager.Add(this); } } - private void IsCheckedChanged(bool? value) + private new void IsCheckedChanged(bool? value) { var groupName = GroupName; if (string.IsNullOrEmpty(groupName)) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index e9abfef673..3ac157f727 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -104,12 +104,12 @@ namespace Avalonia.Controls if (ItemTemplate == null && _treeView?.ItemTemplate != null) { - ItemTemplate = _treeView.ItemTemplate; + SetCurrentValue(ItemTemplateProperty, _treeView.ItemTemplate); } if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null) { - ItemContainerTheme = _treeView.ItemContainerTheme; + SetCurrentValue(ItemContainerThemeProperty, _treeView.ItemContainerTheme); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ba1b599421..f9593f1c1b 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; @@ -11,6 +9,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Styling; namespace Avalonia.Controls @@ -149,11 +148,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty WindowStartupLocationProperty = - AvaloniaProperty.RegisterDirect( - nameof(WindowStartupLocation), - o => o.WindowStartupLocation, - (o, v) => o.WindowStartupLocation = v); + public static readonly StyledProperty WindowStartupLocationProperty = + AvaloniaProperty.Register(nameof(WindowStartupLocation)); public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), true); @@ -171,7 +167,6 @@ namespace Avalonia.Controls RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); private object? _dialogResult; private readonly Size _maxPlatformClientSize; - private WindowStartupLocation _windowStartupLocation; private bool _shown; private bool _showingAsDialog; @@ -305,7 +300,7 @@ namespace Avalonia.Controls { get => GetValue(ExtendClientAreaTitleBarHeightHintProperty); set => SetValue(ExtendClientAreaTitleBarHeightHintProperty, value); - } + } /// /// Gets if the ClientArea is Extended into the Window Decorations. @@ -314,7 +309,7 @@ namespace Avalonia.Controls { get => _isExtendedIntoWindowDecorations; private set => SetAndRaise(IsExtendedIntoWindowDecorationsProperty, ref _isExtendedIntoWindowDecorations, value); - } + } /// /// Gets the WindowDecorationMargin. @@ -324,7 +319,7 @@ namespace Avalonia.Controls { get => _windowDecorationMargin; private set => SetAndRaise(WindowDecorationMarginProperty, ref _windowDecorationMargin, value); - } + } /// /// Gets the window margin that is hidden off the screen area. @@ -397,8 +392,8 @@ namespace Avalonia.Controls /// public WindowStartupLocation WindowStartupLocation { - get { return _windowStartupLocation; } - set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); } + get => GetValue(WindowStartupLocationProperty); + set => SetValue(WindowStartupLocationProperty, value); } /// @@ -488,7 +483,7 @@ namespace Avalonia.Controls CloseInternal(); return false; } - + return true; } @@ -614,7 +609,7 @@ namespace Avalonia.Controls if (_shown != isVisible) { - if(!_shown) + if (!_shown) { Show(); } @@ -657,7 +652,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot re-show a closed window."); } } - + private void EnsureParentStateBeforeShow(Window owner) { if (owner.PlatformImpl == null) @@ -819,7 +814,7 @@ namespace Avalonia.Controls { bool isEnabled = true; - foreach (var (_, isDialog) in _children) + foreach (var (_, isDialog) in _children) { if (isDialog) { @@ -856,7 +851,7 @@ namespace Avalonia.Controls { Window? firstDialogChild = null; - foreach (var (child, isDialog) in _children) + foreach (var (child, isDialog) in _children) { if (isDialog) { @@ -880,7 +875,7 @@ namespace Avalonia.Controls var startupLocation = WindowStartupLocation; if (startupLocation == WindowStartupLocation.CenterOwner && - (owner is null || + (owner is null || (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized)) ) { @@ -902,7 +897,7 @@ namespace Avalonia.Controls if (owner is not null) { - screen = Screens.ScreenFromWindow(owner) + screen = Screens.ScreenFromWindow(owner) ?? Screens.ScreenFromPoint(owner.Position); } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 26e11f0d4a..814a9b5960 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -27,10 +27,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly DirectProperty OwnerProperty = - AvaloniaProperty.RegisterDirect( - nameof(Owner), - o => o.Owner, - (o, v) => o.Owner = v); + AvaloniaProperty.RegisterDirect(nameof(Owner), o => o.Owner); public static readonly StyledProperty TopmostProperty = AvaloniaProperty.Register(nameof(Topmost)); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs index c9189a886d..f765871ee8 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs @@ -6,84 +6,73 @@ namespace Avalonia.Diagnostics.Controls { internal class ThicknessEditor : ContentControl { - public static readonly DirectProperty ThicknessProperty = - AvaloniaProperty.RegisterDirect(nameof(Thickness), o => o.Thickness, - (o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay); + public static readonly StyledProperty ThicknessProperty = + AvaloniaProperty.Register(nameof(Thickness), + defaultBindingMode: BindingMode.TwoWay); - public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, - (o, v) => o.Header = v); + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); - public static readonly DirectProperty IsPresentProperty = - AvaloniaProperty.RegisterDirect(nameof(IsPresent), o => o.IsPresent, - (o, v) => o.IsPresent = v); + public static readonly StyledProperty IsPresentProperty = + AvaloniaProperty.Register(nameof(IsPresent), true); - public static readonly DirectProperty LeftProperty = - AvaloniaProperty.RegisterDirect(nameof(Left), o => o.Left, (o, v) => o.Left = v); + public static readonly StyledProperty LeftProperty = + AvaloniaProperty.Register(nameof(Left)); - public static readonly DirectProperty TopProperty = - AvaloniaProperty.RegisterDirect(nameof(Top), o => o.Top, (o, v) => o.Top = v); + public static readonly StyledProperty TopProperty = + AvaloniaProperty.Register(nameof(Top)); - public static readonly DirectProperty RightProperty = - AvaloniaProperty.RegisterDirect(nameof(Right), o => o.Right, - (o, v) => o.Right = v); + public static readonly StyledProperty RightProperty = + AvaloniaProperty.Register(nameof(Right)); - public static readonly DirectProperty BottomProperty = - AvaloniaProperty.RegisterDirect(nameof(Bottom), o => o.Bottom, - (o, v) => o.Bottom = v); + public static readonly StyledProperty BottomProperty = + AvaloniaProperty.Register(nameof(Bottom)); public static readonly StyledProperty HighlightProperty = AvaloniaProperty.Register(nameof(Highlight)); - private Thickness _thickness; - private string? _header; - private bool _isPresent = true; - private double _left; - private double _top; - private double _right; - private double _bottom; private bool _isUpdatingThickness; public Thickness Thickness { - get => _thickness; - set => SetAndRaise(ThicknessProperty, ref _thickness, value); + get => GetValue(ThicknessProperty); + set => SetValue(ThicknessProperty, value); } public string? Header { - get => _header; - set => SetAndRaise(HeaderProperty, ref _header, value); + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); } public bool IsPresent { - get => _isPresent; - set => SetAndRaise(IsPresentProperty, ref _isPresent, value); + get => GetValue(IsPresentProperty); + set => SetValue(IsPresentProperty, value); } public double Left { - get => _left; - set => SetAndRaise(LeftProperty, ref _left, value); + get => GetValue(LeftProperty); + set => SetValue(LeftProperty, value); } public double Top { - get => _top; - set => SetAndRaise(TopProperty, ref _top, value); + get => GetValue(TopProperty); + set => SetValue(TopProperty, value); } public double Right { - get => _right; - set => SetAndRaise(RightProperty, ref _right, value); + get => GetValue(RightProperty); + set => SetValue(RightProperty, value); } public double Bottom { - get => _bottom; - set => SetAndRaise(BottomProperty, ref _bottom, value); + get => GetValue(BottomProperty); + set => SetValue(BottomProperty, value); } public IBrush Highlight @@ -104,10 +93,10 @@ namespace Avalonia.Diagnostics.Controls var value = change.GetNewValue(); - Left = value.Left; - Top = value.Top; - Right = value.Right; - Bottom = value.Bottom; + SetCurrentValue(LeftProperty, value.Left); + SetCurrentValue(TopProperty, value.Top); + SetCurrentValue(RightProperty, value.Right); + SetCurrentValue(BottomProperty, value.Bottom); } finally { @@ -118,7 +107,7 @@ namespace Avalonia.Diagnostics.Controls (change.Property == LeftProperty || change.Property == TopProperty || change.Property == RightProperty || change.Property == BottomProperty)) { - Thickness = new Thickness(Left, Top, Right, Bottom); + SetCurrentValue(ThicknessProperty, new(Left, Top, Right, Bottom)); } } } diff --git a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs index ba321db144..cfb1b508d9 100644 --- a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs @@ -104,9 +104,7 @@ namespace Avalonia.Data CultureInfo.CurrentCulture); } - // Use LocalValue priority here, as TemplatedParent doesn't make sense on controls - // that aren't template children. - templatedParent.SetValue(Property, value, BindingPriority.LocalValue); + templatedParent.SetCurrentValue(Property, value); } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs index 738469bc6f..7528815510 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Styling .Template() .OfType(); - border.SetValue(StyledElement.TemplatedParentProperty, null); + border.TemplatedParent = null; Assert.Equal(SelectorMatchResult.NeverThisInstance, selector.Match(border).Result); } @@ -124,10 +124,10 @@ namespace Avalonia.Base.UnitTests.Styling { VisualChildren.Add(new Border { - [TemplatedParentProperty] = this, + TemplatedParent = this, Child = new TextBlock { - [TemplatedParentProperty] = this, + TemplatedParent = this, }, }); } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 3aaf62f0bf..7b5aa83b46 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -131,7 +131,7 @@ namespace Avalonia.Controls.UnitTests root.Content = target; var templatedParent = new Button(); - target.SetValue(StyledElement.TemplatedParentProperty, templatedParent); + target.TemplatedParent = templatedParent; target.Template = GetTemplate(); target.Items = new[] { "Foo" }; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index d3737de45b..9042e84fa1 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -26,7 +26,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Null(host.Presenter); - target.SetValue(Control.TemplatedParentProperty, host); + target.TemplatedParent = host; Assert.Same(target, host.Presenter); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 71f803fab7..573ce5834d 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Null(host.Presenter); - target.SetValue(Control.TemplatedParentProperty, host); + target.TemplatedParent = host; Assert.Same(target, host.Presenter); } diff --git a/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs b/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs index 8789d79742..33593a0631 100644 --- a/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Templates/TemplateExtensionsTests.cs @@ -18,17 +18,17 @@ namespace Avalonia.Controls.Templates.UnitTests var border1 = new Border { Name = "border1", - [StyledElement.TemplatedParentProperty] = target, + TemplatedParent = target, }; var inner = new TestTemplatedControl { Name = "inner", - [StyledElement.TemplatedParentProperty] = target, + TemplatedParent = target, }; - var border2 = new Border { Name = "border2", [StyledElement.TemplatedParentProperty] = inner }; - var border3 = new Border { Name = "border3", [StyledElement.TemplatedParentProperty] = inner }; - var border4 = new Border { Name = "border4", [StyledElement.TemplatedParentProperty] = target }; - var border5 = new Border { Name = "border5", [StyledElement.TemplatedParentProperty] = null }; + var border2 = new Border { Name = "border2", TemplatedParent = inner }; + var border3 = new Border { Name = "border3", TemplatedParent = inner }; + var border4 = new Border { Name = "border4", TemplatedParent = target }; + var border5 = new Border { Name = "border5", TemplatedParent = null }; target.AddVisualChild(border1); border1.Child = inner; @@ -42,4 +42,4 @@ namespace Avalonia.Controls.Templates.UnitTests Assert.Equal(new[] { "border1", "inner", "border4" }, result); } } -} \ No newline at end of file +} From f36cf7e3ba9de8a36e89c43541b942cf58f4d593 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Mon, 27 Feb 2023 19:57:41 +0100 Subject: [PATCH 51/71] Converted several "dataValidation" properties to StyledProperty Changed Button.IsPressed to a read-only DirectProperty --- src/Avalonia.Controls/Button.cs | 45 +++++++++---------- .../CalendarDatePicker.Properties.cs | 10 ++--- .../CalendarDatePicker/CalendarDatePicker.cs | 1 - src/Avalonia.Controls/MenuItem.cs | 22 ++++----- src/Avalonia.Controls/NativeMenuItem.cs | 31 +++++-------- .../NumericUpDown/NumericUpDown.cs | 36 +++++++-------- .../SplitButton/SplitButton.cs | 12 ++--- src/Avalonia.Controls/TrayIcon.cs | 14 ++---- .../Xaml/XamlIlTests.cs | 12 ----- 9 files changed, 69 insertions(+), 114 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 1ec6f8dabc..f48d7a7cc1 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,11 +1,9 @@ using System; -using System.Diagnostics; using System.Linq; using System.Windows.Input; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -48,9 +46,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - AvaloniaProperty.RegisterDirect(nameof(Command), - button => button.Command, (button, command) => button.Command = command, enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + AvaloniaProperty.Register(nameof(Command), enableDataValidation: true); /// /// Defines the property. @@ -85,8 +82,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty IsPressedProperty = - AvaloniaProperty.Register(nameof(IsPressed)); + public static readonly DirectProperty IsPressedProperty = + AvaloniaProperty.RegisterDirect(nameof(IsPressed), b => b.IsPressed); /// /// Defines the property @@ -94,10 +91,10 @@ namespace Avalonia.Controls public static readonly StyledProperty FlyoutProperty = AvaloniaProperty.Register(nameof(Flyout)); - private ICommand? _command; private bool _commandCanExecute = true; private KeyGesture? _hotkey; private bool _isFlyoutOpen = false; + private bool _isPressed = false; /// /// Initializes static members of the class. @@ -138,8 +135,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get => _command; - set => SetAndRaise(CommandProperty, ref _command, value); + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -185,8 +182,8 @@ namespace Avalonia.Controls /// public bool IsPressed { - get => GetValue(IsPressedProperty); - private set => SetValue(IsPressedProperty, value); + get => _isPressed; + private set => SetAndRaise(IsPressedProperty, ref _isPressed, value); } /// @@ -248,7 +245,7 @@ namespace Avalonia.Controls { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control { - HotKey = _hotkey; + SetCurrentValue(HotKeyProperty, _hotkey); } base.OnAttachedToLogicalTree(e); @@ -267,7 +264,7 @@ namespace Avalonia.Controls if (HotKey != null) { _hotkey = HotKey; - HotKey = null; + SetCurrentValue(HotKeyProperty, null); } base.OnDetachedFromLogicalTree(e); @@ -291,17 +288,17 @@ namespace Avalonia.Controls break; case Key.Space: - { - if (ClickMode == ClickMode.Press) { - OnClick(); + if (ClickMode == ClickMode.Press) + { + OnClick(); + } + + IsPressed = true; + e.Handled = true; + break; } - IsPressed = true; - e.Handled = true; - break; - } - case Key.Escape when Flyout != null: // If Flyout doesn't have focusable content, close the flyout here CloseFlyout(); @@ -592,7 +589,7 @@ namespace Avalonia.Controls { flyout.Opened -= Flyout_Opened; flyout.Closed -= Flyout_Closed; - } + } } /// @@ -671,7 +668,7 @@ namespace Avalonia.Controls void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); void IClickableControl.RaiseClick() => OnClick(); - + /// /// Event handler for when the button's flyout is opened. /// diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs index 5ff04b1a99..1454b4ab6c 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.Properties.cs @@ -50,11 +50,9 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty SelectedDateProperty = - AvaloniaProperty.RegisterDirect( + public static readonly StyledProperty SelectedDateProperty = + AvaloniaProperty.Register( nameof(SelectedDate), - o => o.SelectedDate, - (o, v) => o.SelectedDate = v, enableDataValidation: true, defaultBindingMode:BindingMode.TwoWay); @@ -211,8 +209,8 @@ namespace Avalonia.Controls /// public DateTime? SelectedDate { - get => _selectedDate; - set => SetAndRaise(SelectedDateProperty, ref _selectedDate, value); + get => GetValue(SelectedDateProperty); + set => SetValue(SelectedDateProperty, value); } /// diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index 3f5d355b71..c091d07632 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -45,7 +45,6 @@ namespace Avalonia.Controls private DateTime? _onOpenSelectedDate; private bool _settingSelectedDate; - private DateTime? _selectedDate; private bool _suspendTextChangeHandler; private bool _isPopupClosing; private bool _ignoreButtonClick; diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 1670e496b4..03e3444d71 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -27,11 +27,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - menuItem => menuItem.Command, - (menuItem, command) => menuItem.Command = command, - enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(new(enableDataValidation: true)); /// /// Defines the property. @@ -113,7 +110,6 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - private ICommand? _command; private bool _commandCanExecute = true; private bool _commandBindingError; private Popup? _popup; @@ -217,8 +213,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get { return _command; } - set { SetAndRaise(CommandProperty, ref _command, value); } + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -337,7 +333,7 @@ namespace Avalonia.Controls /// /// This has the same effect as setting to true. /// - public void Open() => IsSubMenuOpen = true; + public void Open() => SetCurrentValue(IsSubMenuOpenProperty, true); /// /// Closes the submenu. @@ -345,7 +341,7 @@ namespace Avalonia.Controls /// /// This has the same effect as setting to false. /// - public void Close() => IsSubMenuOpen = false; + public void Close() => SetCurrentValue(IsSubMenuOpenProperty, false); /// void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent)); @@ -369,7 +365,7 @@ namespace Avalonia.Controls { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control { - HotKey = _hotkey; + SetCurrentValue(HotKeyProperty, _hotkey); } base.OnAttachedToLogicalTree(e); @@ -397,7 +393,7 @@ namespace Avalonia.Controls if (HotKey != null) { _hotkey = HotKey; - HotKey = null; + SetCurrentValue(HotKeyProperty, null); } base.OnDetachedFromLogicalTree(e); @@ -663,7 +659,7 @@ namespace Avalonia.Controls } RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent)); - IsSelected = true; + SetCurrentValue(IsSelectedProperty, true); PseudoClasses.Add(":open"); } else diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index b7ce928b4b..9b5f756887 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -9,7 +9,6 @@ namespace Avalonia.Controls { public class NativeMenuItem : NativeMenuItemBase, INativeMenuItemExporterEventsImplBridge { - private ICommand? _command; private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; class CanExecuteChangedSubscriber : IWeakEventSubscriber @@ -100,11 +99,8 @@ namespace Avalonia.Controls set => SetValue(ToggleTypeProperty, value); } - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - menuItem => menuItem.Command, - (menuItem, command) => menuItem.Command = command, - enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(new(enableDataValidation: true)); /// /// Defines the property. @@ -130,19 +126,8 @@ namespace Avalonia.Controls public ICommand? Command { - get => _command; - set - { - if (_command != null) - WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber); - - SetAndRaise(CommandProperty, ref _command, value); - - if (_command != null) - WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber); - - CanExecuteChanged(); - } + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -180,6 +165,14 @@ namespace Avalonia.Controls throw new InvalidOperationException("NativeMenu already has a parent"); newMenu.Parent = this; } + else if (change.Property == CommandProperty) + { + if (change.OldValue is ICommand oldCommand) + WeakEvents.CommandCanExecuteChanged.Unsubscribe(oldCommand, _canExecuteChangedSubscriber); + if (change.NewValue is ICommand newCommand) + WeakEvents.CommandCanExecuteChanged.Subscribe(newCommand, _canExecuteChangedSubscriber); + CanExecuteChanged(); + } } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index e676ec0759..885a8af5d1 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -91,8 +91,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty TextProperty = - AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v, + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text), defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// @@ -104,9 +104,9 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, - (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register(nameof(Value), coerce: (s,v) => ((NumericUpDown)s).OnCoerceValue(v), + defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); /// /// Defines the property. @@ -128,8 +128,6 @@ namespace Avalonia.Controls private IDisposable? _textBoxTextChangedSubscription; - private decimal? _value; - private string? _text; private bool _internalValueSet; private bool _isSyncingTextAndValueProperties; private bool _isTextChangedFromUI; @@ -250,8 +248,8 @@ namespace Avalonia.Controls /// public string? Text { - get { return _text; } - set { SetAndRaise(TextProperty, ref _text, value); } + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); } /// @@ -270,12 +268,8 @@ namespace Avalonia.Controls /// public decimal? Value { - get { return _value; } - set - { - value = OnCoerceValue(value); - SetAndRaise(ValueProperty, ref _value, value); - } + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); } /// @@ -500,7 +494,7 @@ namespace Avalonia.Controls SyncTextAndValueProperties(true, Text); } } - + /// /// Called when the property value changed. /// @@ -667,7 +661,7 @@ namespace Avalonia.Controls { result = Minimum; } - + SetCurrentValue(ValueProperty, MathUtilities.Clamp(result, Minimum, Maximum)); } @@ -677,7 +671,7 @@ namespace Avalonia.Controls private void OnDecrement() { decimal result; - + if (Value.HasValue) { result = Value.Value - Increment; @@ -686,7 +680,7 @@ namespace Avalonia.Controls { result = Maximum; } - + SetCurrentValue(ValueProperty, MathUtilities.Clamp(result, Minimum, Maximum)); } @@ -704,7 +698,7 @@ namespace Avalonia.Controls { validDirections = ValidSpinDirections.Increase | ValidSpinDirections.Decrease; } - + if (Value < Maximum) { validDirections = validDirections | ValidSpinDirections.Increase; @@ -1058,7 +1052,7 @@ namespace Avalonia.Controls { return null; } - + if (TextConverter != null) { var valueFromText = TextConverter.Convert(text, typeof(decimal?), null, CultureInfo.CurrentCulture); diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 31a06d875a..e82fb39a66 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -42,10 +42,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - splitButton => splitButton.Command, - (splitButton, command) => splitButton.Command = command); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(); /// /// Defines the property. @@ -59,8 +57,6 @@ namespace Avalonia.Controls public static readonly StyledProperty FlyoutProperty = Button.FlyoutProperty.AddOwner(); - private ICommand? _Command; - private Button? _primaryButton = null; private Button? _secondaryButton = null; @@ -83,8 +79,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get => _Command; - set => SetAndRaise(CommandProperty, ref _Command, value); + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 5713846b35..73bcb84c69 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -13,13 +13,10 @@ namespace Avalonia.Controls public sealed class TrayIcons : AvaloniaList { } - - public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable { private readonly ITrayIconImpl? _impl; - private ICommand? _command; private TrayIcon(ITrayIconImpl? impl) { @@ -85,11 +82,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = - Button.CommandProperty.AddOwner( - trayIcon => trayIcon.Command, - (trayIcon, command) => trayIcon.Command = command, - enableDataValidation: true); + public static readonly StyledProperty CommandProperty = + Button.CommandProperty.AddOwner(new(enableDataValidation: true)); /// /// Defines the property. @@ -136,8 +130,8 @@ namespace Avalonia.Controls /// public ICommand? Command { - get => _command; - set => SetAndRaise(CommandProperty, ref _command, value); + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index be2cae8ec4..09ed78accb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -21,17 +21,6 @@ namespace Avalonia.Markup.Xaml.UnitTests { public class XamlIlTests : XamlTestBase { - [Fact] - public void Binding_Button_IsPressed_ShouldWork() - { - var parsed = (Button)AvaloniaRuntimeXamlLoader.Parse(@" - - + + + + + + + + + + + + + + + + + + + + + + + Normal + Minimized + Maximized + FullScreen + + + + + + + + + + + + + + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 43875dd990..ffcd6fc32c 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -7,6 +7,25 @@ using Avalonia.Threading; namespace IntegrationTestApp { + public class MeasureBorder : Border + { + protected override Size MeasureOverride(Size availableSize) + { + MeasuredWith = availableSize; + + return base.MeasureOverride(availableSize); + } + + public static readonly StyledProperty MeasuredWithProperty = AvaloniaProperty.Register( + nameof(MeasuredWith)); + + public Size MeasuredWith + { + get => GetValue(MeasuredWithProperty); + set => SetValue(MeasuredWithProperty, value); + } + } + public class ShowWindowTest : Window { private readonly DispatcherTimer? _timer; From f2bceb40c92fd8626773499718c9742455eb9a72 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 2 Mar 2023 16:06:31 +0000 Subject: [PATCH 55/71] add an integration test. --- .../WindowTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index a2bfb618d6..fb3283fbe7 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Controls; +using Avalonia.Utilities; using Avalonia.Media.Imaging; using OpenQA.Selenium; using OpenQA.Selenium.Appium; @@ -143,6 +144,24 @@ namespace Avalonia.IntegrationTests.Appium } } + + [Fact] + public void Showing_Window_With_Size_Larger_Than_Screen_Measures_Content_With_Working_Area() + { + using (OpenWindow(new Size(4000, 2200), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) + { + var measuredWithTextBlock = _session.FindElementById("MeasuredWithText"); + var screenRectTextBox = _session.FindElementById("ScreenRect"); + + var measuredWithString = measuredWithTextBlock.Text; + var workingAreaString = screenRectTextBox.Text; + + var workingArea = Rect.Parse(workingAreaString); + var measuredWith = Size.Parse(measuredWithString); + + Assert.Equal(workingArea.Size, measuredWith); + } + } [Theory] [InlineData(ShowWindowMode.NonOwned)] From 87402e5b94774938c3b9bb2a3f1f5702b8a9437f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 2 Mar 2023 07:58:28 -1000 Subject: [PATCH 56/71] Update insets API and implement insets for iOS with IAvaloniaViewController --- .../Platform/AndroidInsetsManager.cs | 41 +++++---- .../Platform/SkiaPlatform/TopLevelImpl.cs | 2 +- .../Platform/IInsetsManager.cs | 43 +++++----- .../Avalonia.Browser/BrowserInsetsManager.cs | 14 ++-- src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs | 4 +- src/iOS/Avalonia.iOS/AvaloniaView.cs | 35 ++++++-- src/iOS/Avalonia.iOS/InsetsManager.cs | 83 +++++++++++++++++++ src/iOS/Avalonia.iOS/ViewController.cs | 74 +++++++++++++++++ 8 files changed, 241 insertions(+), 55 deletions(-) create mode 100644 src/iOS/Avalonia.iOS/InsetsManager.cs create mode 100644 src/iOS/Avalonia.iOS/ViewController.cs diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 7bc3a02736..3850199226 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -59,30 +59,39 @@ namespace Avalonia.Android.Platform DisplayEdgeToEdge = false; } - public Thickness GetSafeAreaPadding() + public Thickness SafeAreaPadding { - var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); - - if (insets != null) + get { - var renderScaling = _topLevel.RenderScaling; + var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); - var inset = insets.GetInsets((DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0 ) | WindowInsetsCompat.Type.Ime()); - var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); - var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + if (insets != null) + { + var renderScaling = _topLevel.RenderScaling; + + var inset = insets.GetInsets( + (DisplayEdgeToEdge ? + WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | + WindowInsetsCompat.Type.DisplayCutout() : + 0) | WindowInsetsCompat.Type.Ime()); + var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); + var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + + return new Thickness(inset.Left / renderScaling, + inset.Top / renderScaling, + inset.Right / renderScaling, + (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ? + imeInset.Bottom - navBarInset.Bottom : + inset.Bottom) / renderScaling); + } - return new Thickness(inset.Left / renderScaling, - inset.Top / renderScaling, - inset.Right / renderScaling, - (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom) / renderScaling); + return default; } - - return default; } public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) { - NotifySafeAreaChanged(GetSafeAreaPadding()); + NotifySafeAreaChanged(SafeAreaPadding); return insets; } @@ -93,7 +102,7 @@ namespace Avalonia.Android.Platform public void OnGlobalLayout() { - NotifySafeAreaChanged(GetSafeAreaPadding()); + NotifySafeAreaChanged(SafeAreaPadding); } public SystemBarTheme? SystemBarTheme diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index f0fc13c86f..b8d80a50ff 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -43,7 +43,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; - private readonly IInsetsManager _insetsManager; + private readonly AndroidInsetsManager _insetsManager; private ViewImpl _view; public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs index 028e434104..6288142805 100644 --- a/src/Avalonia.Controls/Platform/IInsetsManager.cs +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -1,28 +1,18 @@ using System; -using Avalonia.Interactivity; +using Avalonia.Metadata; +#nullable enable namespace Avalonia.Controls.Platform { - [Avalonia.Metadata.Unstable] + [Unstable] + [NotClientImplementable] public interface IInsetsManager { - /// - /// Gets or sets the theme for the system bars, if supported. - /// - SystemBarTheme? SystemBarTheme { get; set; } - /// /// Gets or sets whether the system bars are visible. /// bool? IsSystemBarVisible { get; set; } - /// - /// Occurs when safe area for the current window changes. - /// - - event EventHandler SafeAreaChanged; - - /// /// Gets or sets whether the window draws edge to edge. behind any visibile system bars. /// @@ -31,18 +21,23 @@ namespace Avalonia.Controls.Platform /// /// Gets the current safe area padding. /// - /// - Thickness GetSafeAreaPadding(); - - public class SafeAreaChangedArgs : RoutedEventArgs + Thickness SafeAreaPadding { get; } + + /// + /// Occurs when safe area for the current window changes. + /// + event EventHandler? SafeAreaChanged; + } + + public class SafeAreaChangedArgs : EventArgs + { + public SafeAreaChangedArgs(Thickness safeArePadding) { - public SafeAreaChangedArgs(Thickness safeArePadding) - { - SafeAreaPadding = safeArePadding; - } - - public Thickness SafeAreaPadding { get; } + SafeAreaPadding = safeArePadding; } + + /// + public Thickness SafeAreaPadding { get; } } public enum SystemBarTheme diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs index d751152a53..30f80ba27c 100644 --- a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -11,7 +11,6 @@ namespace Avalonia.Browser { internal class BrowserInsetsManager : IInsetsManager { - public SystemBarTheme? SystemBarTheme { get; set; } public bool? IsSystemBarVisible { get @@ -20,7 +19,7 @@ namespace Avalonia.Browser } set { - DomHelper.SetFullscreen(value != null ? !value.Value : false); + DomHelper.SetFullscreen(!value ?? false); } } @@ -28,16 +27,19 @@ namespace Avalonia.Browser public event EventHandler? SafeAreaChanged; - public Thickness GetSafeAreaPadding() + public Thickness SafeAreaPadding { - var padding = DomHelper.GetSafeAreaPadding(); + get + { + var padding = DomHelper.GetSafeAreaPadding(); - return new Thickness(padding[0], padding[1], padding[2], padding[3]); + return new Thickness(padding[0], padding[1], padding[2], padding[3]); + } } public void NotifySafeAreaPaddingChanged() { - SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(GetSafeAreaPadding())); + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); } } } diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index b605d82541..18ccbad692 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -40,10 +40,12 @@ namespace Avalonia.iOS var view = new AvaloniaView(); lifetime.View = view; - Window.RootViewController = new UIViewController + var controller = new DefaultAvaloniaViewController { View = view }; + Window.RootViewController = controller; + view.InitWithController(controller); }); builder.SetupWithLifetime(lifetime); diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 2d6b93f818..09721ad181 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -16,6 +16,7 @@ using Foundation; using ObjCRuntime; using OpenGLES; using UIKit; +using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager; namespace Avalonia.iOS { @@ -26,6 +27,7 @@ namespace Avalonia.iOS private EmbeddableControlRoot _topLevel; private TouchHandler _touches; private ITextInputMethodClient _client; + private IAvaloniaViewController _controller; public AvaloniaView() { @@ -48,10 +50,13 @@ namespace Avalonia.iOS MultipleTouchEnabled = true; } + /// public override bool CanBecomeFirstResponder => true; + /// public override bool CanResignFirstResponder => true; + /// public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection) { base.TraitCollectionDidChange(previousTraitCollection); @@ -60,6 +65,7 @@ namespace Avalonia.iOS settings?.TraitCollectionDidChange(); } + /// public override void TintColorDidChange() { base.TintColorDidChange(); @@ -68,18 +74,31 @@ namespace Avalonia.iOS settings?.TraitCollectionDidChange(); } + public void InitWithController(TController controller) + where TController : UIViewController, IAvaloniaViewController + { + _controller = controller; + _topLevelImpl._insetsManager.InitWithController(controller); + } + internal class TopLevelImpl : ITopLevelImpl { private readonly AvaloniaView _view; private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; + internal readonly InsetsManager _insetsManager; public AvaloniaView View => _view; public TopLevelImpl(AvaloniaView view) { _view = view; - _nativeControlHost = new NativeControlHostImpl(_view); + _nativeControlHost = new NativeControlHostImpl(view); _storageProvider = new IOSStorageProvider(view); + _insetsManager = new InsetsManager(view); + _insetsManager.DisplayEdgeToEdgeChanged += (sender, b) => + { + view._topLevel.Padding = b ? default : _insetsManager.SafeAreaPadding; + }; } public void Dispose() @@ -141,17 +160,14 @@ namespace Avalonia.iOS public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { // TODO adjust status bar depending on full screen mode. - if (OperatingSystem.IsIOSVersionAtLeast(13)) + if (OperatingSystem.IsIOSVersionAtLeast(13) && _view._controller is not null) { - var uiStatusBarStyle = themeVariant switch + _view._controller.PreferredStatusBarStyle = themeVariant switch { PlatformThemeVariant.Light => UIStatusBarStyle.DarkContent, PlatformThemeVariant.Dark => UIStatusBarStyle.LightContent, - _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null) + _ => UIStatusBarStyle.Default }; - - // Consider using UIViewController.PreferredStatusBarStyle in the future. - UIApplication.SharedApplication.SetStatusBarStyle(uiStatusBarStyle, true); } } @@ -175,6 +191,11 @@ namespace Avalonia.iOS return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs new file mode 100644 index 0000000000..62e560ddf9 --- /dev/null +++ b/src/iOS/Avalonia.iOS/InsetsManager.cs @@ -0,0 +1,83 @@ +using System; +using Avalonia.Controls.Platform; +using UIKit; + +namespace Avalonia.iOS; +#nullable enable + +internal class InsetsManager : IInsetsManager +{ + private readonly AvaloniaView _view; + private IAvaloniaViewController? _controller; + private bool _displayEdgeToEdge; + + public InsetsManager(AvaloniaView view) + { + _view = view; + } + + internal void InitWithController(IAvaloniaViewController controller) + { + _controller = controller; + if (_controller is not null) + { + _controller.SafeAreaPaddingChanged += (_, _) => + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); + DisplayEdgeToEdgeChanged?.Invoke(this, _displayEdgeToEdge); + }; + } + } + + public SystemBarTheme? SystemBarTheme + { + get => _controller?.PreferredStatusBarStyle switch + { + UIStatusBarStyle.LightContent => Controls.Platform.SystemBarTheme.Dark, + UIStatusBarStyle.DarkContent => Controls.Platform.SystemBarTheme.Light, + _ => null + }; + set + { + if (_controller != null) + { + _controller.PreferredStatusBarStyle = value switch + { + Controls.Platform.SystemBarTheme.Light => UIStatusBarStyle.DarkContent, + Controls.Platform.SystemBarTheme.Dark => UIStatusBarStyle.LightContent, + null => UIStatusBarStyle.Default, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) + }; + } + } + } + + public bool? IsSystemBarVisible + { + get => _controller?.PrefersStatusBarHidden == false; + set + { + if (_controller is not null) + { + _controller.PrefersStatusBarHidden = value == false; + } + } + } + public event EventHandler? SafeAreaChanged; + public event EventHandler? DisplayEdgeToEdgeChanged; + + public bool DisplayEdgeToEdge + { + get => _displayEdgeToEdge; + set + { + if (_displayEdgeToEdge != value) + { + _displayEdgeToEdge = value; + DisplayEdgeToEdgeChanged?.Invoke(this, value); + } + } + } + + public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default; +} diff --git a/src/iOS/Avalonia.iOS/ViewController.cs b/src/iOS/Avalonia.iOS/ViewController.cs new file mode 100644 index 0000000000..42a0949a9c --- /dev/null +++ b/src/iOS/Avalonia.iOS/ViewController.cs @@ -0,0 +1,74 @@ +using System; +using Avalonia.Metadata; +using UIKit; + +namespace Avalonia.iOS; + +[Unstable] +public interface IAvaloniaViewController +{ + UIStatusBarStyle PreferredStatusBarStyle { get; set; } + bool PrefersStatusBarHidden { get; set; } + Thickness SafeAreaPadding { get; } + event EventHandler SafeAreaPaddingChanged; +} + +/// +public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewController +{ + private UIStatusBarStyle? _preferredStatusBarStyle; + private bool? _prefersStatusBarHidden; + + /// + public override void ViewDidLayoutSubviews() + { + base.ViewDidLayoutSubviews(); + var size = View?.Frame.Size ?? default; + var frame = View?.SafeAreaLayoutGuide.LayoutFrame ?? default; + var safeArea = new Thickness(frame.Left, frame.Top, size.Width - frame.Right, size.Height - frame.Bottom); + if (SafeAreaPadding != safeArea) + { + SafeAreaPadding = safeArea; + SafeAreaPaddingChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + public override bool PrefersStatusBarHidden() + { + return _prefersStatusBarHidden ??= base.PrefersStatusBarHidden(); + } + + /// + public override UIStatusBarStyle PreferredStatusBarStyle() + { + // don't set _preferredStatusBarStyle value if it's null, so we can keep "default" there instead of actual app style. + return _preferredStatusBarStyle ?? base.PreferredStatusBarStyle(); + } + + UIStatusBarStyle IAvaloniaViewController.PreferredStatusBarStyle + { + get => _preferredStatusBarStyle ?? UIStatusBarStyle.Default; + set + { + _preferredStatusBarStyle = value; + SetNeedsStatusBarAppearanceUpdate(); + } + } + + bool IAvaloniaViewController.PrefersStatusBarHidden + { + get => _prefersStatusBarHidden ?? false; // false is default on ios/ipados + set + { + _prefersStatusBarHidden = value; + SetNeedsStatusBarAppearanceUpdate(); + } + } + + /// + public Thickness SafeAreaPadding { get; private set; } + + /// + public event EventHandler SafeAreaPaddingChanged; +} From a28aa6e04ada88853865d0900805d58bf1cff27d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 2 Mar 2023 07:58:57 -1000 Subject: [PATCH 57/71] Update control catalog to support insets samples --- .../ControlCatalog.iOS.csproj | 4 +- samples/ControlCatalog.iOS/Info.plist | 6 +-- samples/ControlCatalog/App.xaml.cs | 4 +- samples/ControlCatalog/MainView.xaml.cs | 38 ++++++++++++++++++- samples/ControlCatalog/MainWindow.xaml.cs | 1 - .../Pages/WindowCustomizationsPage.xaml | 22 ++++++++--- .../ViewModels/MainWindowViewModel.cs | 30 +++++++++++++-- 7 files changed, 84 insertions(+), 21 deletions(-) diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 74d5b2fd8c..b4dac5399c 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -3,7 +3,7 @@ Exe manual net6.0-ios - 10.0 + 13.0 True iossimulator-x64 @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog.iOS/Info.plist b/samples/ControlCatalog.iOS/Info.plist index 6ffe3ba662..1dd4416c28 100644 --- a/samples/ControlCatalog.iOS/Info.plist +++ b/samples/ControlCatalog.iOS/Info.plist @@ -13,7 +13,7 @@ LSRequiresIPhoneOS MinimumOSVersion - 10.0 + 13.0 UIDeviceFamily 1 @@ -39,9 +39,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIStatusBarHidden - - UIViewControllerBasedStatusBarAppearance - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index d71d51f068..246fe4385f 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -44,11 +44,11 @@ namespace ControlCatalog { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { - desktopLifetime.MainWindow = new MainWindow(); + desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() }; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) { - singleViewLifetime.MainView = new MainView(); + singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() }; } base.OnFrameworkInitializationCompleted(); diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 6f31d22677..f29da3126f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -12,6 +13,7 @@ using Avalonia.VisualTree; using Avalonia.Styling; using ControlCatalog.Models; using ControlCatalog.Pages; +using ControlCatalog.ViewModels; namespace ControlCatalog { @@ -99,13 +101,47 @@ namespace ControlCatalog }; } + internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!; + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); + var decorations = this.Get("Decorations"); if (VisualRoot is Window window) decorations.SelectedIndex = (int)window.SystemDecorations; - + + var insets = TopLevel.GetTopLevel(this)!.InsetsManager; + if (insets != null) + { + // In real life application these events should be unsubscribed to avoid memory leaks. + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + insets.SafeAreaChanged += (sender, args) => + { + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + }; + + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? false; + + ViewModel.PropertyChanged += async (sender, args) => + { + if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge)) + { + insets.DisplayEdgeToEdge = ViewModel.DisplayEdgeToEdge; + } + else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible)) + { + insets.IsSystemBarVisible = ViewModel.IsSystemBarVisible; + } + + // Give the OS some time to apply new values and refresh the view model. + await Task.Delay(100); + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? false; + }; + } + _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); } diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index c589f41442..10ff94d25c 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -17,7 +17,6 @@ namespace ControlCatalog { this.InitializeComponent(); - DataContext = new MainWindowViewModel(); _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu; } diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index d690058b27..bcc1a71243 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -5,11 +5,21 @@ xmlns:viewModels="using:ControlCatalog.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ControlCatalog.Pages.WindowCustomizationsPage" - x:DataType="viewModels:MainWindowViewModel"> - - - - - + x:DataType="viewModels:MainWindowViewModel" + x:CompileBindings="True"> + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 3628a9b8a7..8c6f0a2bd6 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -6,6 +6,7 @@ using Avalonia.Platform; using Avalonia.Reactive; using System; using System.ComponentModel.DataAnnotations; +using Avalonia; using MiniMvvm; namespace ControlCatalog.ViewModels @@ -20,6 +21,9 @@ namespace ControlCatalog.ViewModels private bool _systemTitleBarEnabled; private bool _preferSystemChromeEnabled; private double _titleBarHeight; + private bool _isSystemBarVisible; + private bool _displayEdgeToEdge; + private Thickness _safeAreaPadding; public MainWindowViewModel() { @@ -78,25 +82,25 @@ namespace ControlCatalog.ViewModels { get { return _chromeHints; } set { this.RaiseAndSetIfChanged(ref _chromeHints, value); } - } + } public bool ExtendClientAreaEnabled { get { return _extendClientAreaEnabled; } set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } - } + } public bool SystemTitleBarEnabled { get { return _systemTitleBarEnabled; } set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); } - } + } public bool PreferSystemChromeEnabled { get { return _preferSystemChromeEnabled; } set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } - } + } public double TitleBarHeight { @@ -122,6 +126,24 @@ namespace ControlCatalog.ViewModels set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); } } + public bool IsSystemBarVisible + { + get { return _isSystemBarVisible; } + set { this.RaiseAndSetIfChanged(ref _isSystemBarVisible, value); } + } + + public bool DisplayEdgeToEdge + { + get { return _displayEdgeToEdge; } + set { this.RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); } + } + + public Thickness SafeAreaPadding + { + get { return _safeAreaPadding; } + set { this.RaiseAndSetIfChanged(ref _safeAreaPadding, value); } + } + public MiniCommand AboutCommand { get; } public MiniCommand ExitCommand { get; } From 8cb4cf8344fa0c84ac65defb9761e940827cddd5 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Thu, 2 Mar 2023 20:47:59 +0200 Subject: [PATCH 58/71] Remove unneccessary using directives. --- src/Avalonia.Controls/Label.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Avalonia.Controls/Label.cs b/src/Avalonia.Controls/Label.cs index 6c6518f3ed..94ea66c4c1 100644 --- a/src/Avalonia.Controls/Label.cs +++ b/src/Avalonia.Controls/Label.cs @@ -1,12 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; using Avalonia.Automation.Peers; using Avalonia.Controls.Automation.Peers; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; From c1de1ca90263d92a269cd85d36402a312236a7c2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 2 Mar 2023 21:28:30 +0000 Subject: [PATCH 59/71] skip netfx tests on macos. --- nukebuild/Build.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 3704cee890..46f267ae17 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -165,10 +165,10 @@ partial class Build : NukeBuild foreach (var fw in targetFrameworks) { if (fw.StartsWith("net4") - && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { - Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969"); + Information($"Skipping {projectName} ({fw}) tests on *nix - https://github.com/mono/mono/issues/13969"); continue; } From c55747f328dd56a861d71dc9e2e57f99045500af Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Mar 2023 12:35:45 +0100 Subject: [PATCH 60/71] Introduce ConfigureFonts AppBuilder action Introduce WithInterFont extension method --- samples/ControlCatalog.NetCore/Program.cs | 2 +- src/Avalonia.Controls/AppBuilder.cs | 16 ++++------------ src/Avalonia.Fonts.Inter/AppBuilderExtension.cs | 13 +++++++++++++ .../Avalonia.Fonts.Inter.csproj | 1 + 4 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 src/Avalonia.Fonts.Inter/AppBuilderExtension.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index a86d311f4e..85c159467b 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -125,7 +125,7 @@ namespace ControlCatalog.NetCore EnableIme = true }) .UseSkia() - .WithFonts(new InterFontCollection()) + .WithInterFont() .AfterSetup(builder => { builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 7e9fa1cab7..64bf92b7cd 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -208,23 +208,15 @@ namespace Avalonia } /// - /// Registers a custom font collection with the font manager. + /// Registers an action that is executed with the current font manager. /// - /// The font collection. + /// The action. /// An instance. - /// - public AppBuilder WithFonts(IFontCollection fontCollection) + public AppBuilder ConfigureFonts(Action action) { - if(fontCollection == null) - { - throw new ArgumentNullException(nameof(fontCollection), "Font collection can't be null."); - } - return AfterSetup(appBuilder => { - var fontManager = FontManager.Current; - - fontManager.AddFontCollection(fontCollection); + action?.Invoke(FontManager.Current); }); } diff --git a/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs new file mode 100644 index 0000000000..842629c923 --- /dev/null +++ b/src/Avalonia.Fonts.Inter/AppBuilderExtension.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Fonts.Inter +{ + public static class AppBuilderExtension + { + public static AppBuilder WithInterFont(this AppBuilder appBuilder) + { + return appBuilder.ConfigureFonts(fontManager => + { + fontManager.AddFontCollection(new InterFontCollection()); + }); + } + } +} diff --git a/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj b/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj index c81a13558c..c18c07d347 100644 --- a/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj +++ b/src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj @@ -8,6 +8,7 @@ + From 101ea9cb5fd4e3a68165e634e629dcd1bbba7313 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 3 Mar 2023 12:21:54 +0000 Subject: [PATCH 61/71] only constrain to working area before show. After that cocoa will do it for us. --- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index dba5daf90b..bf98a6d1b6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -273,7 +273,6 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso auto resizeBlock = ResizeScope(View, reason); @autoreleasepool { - auto screenSize = [Window screen].visibleFrame.size; auto maxSize = lastMaxSize; auto minSize = lastMinSize; @@ -293,20 +292,25 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso y = maxSize.height; } - if(x > screenSize.width){ - x = screenSize.width; - } - - if(y > screenSize.height) - { - y = screenSize.height; - } - @try { if(x != lastSize.width || y != lastSize.height) { lastSize = NSSize{x, y}; if (!_shown) { + // Before the window is shown, Cocoa wont give Resized events + // constraining the window to the maximum size available on the monitor. + // we have to emulated this behavior that Avalonia relies on. + auto screenSize = [Window screen].visibleFrame.size; + + if(x > screenSize.width){ + x = screenSize.width; + } + + if(y > screenSize.height) + { + y = screenSize.height; + } + BaseEvents->Resized(AvnSize{x, y}, reason); } else if (Window != nullptr) { [Window setContentSize:lastSize]; From c8b868a558228e44196c7ad46bfd539a1d114d42 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 3 Mar 2023 14:55:21 +0000 Subject: [PATCH 62/71] alway setContentSize even before show, but preconstrain the size. --- native/Avalonia.Native/src/OSX/AvnView.mm | 7 ++---- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 24 ++++++++----------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 4ae6ad5a00..bcfdc23053 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -127,11 +127,8 @@ [self updateRenderTarget]; auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; - - if(_parent->IsShown()) - { - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); - } + + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); } } diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index bf98a6d1b6..b579920c6b 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -4,6 +4,7 @@ // #import +#import #include "common.h" #include "AvnView.h" #include "menu.h" @@ -293,29 +294,24 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso } @try { - if(x != lastSize.width || y != lastSize.height) { - lastSize = NSSize{x, y}; - + if(x != lastSize.width || y != lastSize.height) + { if (!_shown) { - // Before the window is shown, Cocoa wont give Resized events - // constraining the window to the maximum size available on the monitor. - // we have to emulated this behavior that Avalonia relies on. auto screenSize = [Window screen].visibleFrame.size; - if(x > screenSize.width){ + if (x > screenSize.width) { x = screenSize.width; } - if(y > screenSize.height) - { + if (y > screenSize.height) { y = screenSize.height; } - - BaseEvents->Resized(AvnSize{x, y}, reason); - } else if (Window != nullptr) { - [Window setContentSize:lastSize]; - [Window invalidateShadow]; } + + lastSize = NSSize{x, y}; + + [Window setContentSize:lastSize]; + [Window invalidateShadow]; } } @finally { From 34c1b46b30fad19f043a1f46a5e887685fd2aba6 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 3 Mar 2023 15:00:40 +0000 Subject: [PATCH 63/71] Force app theme change when status bar theme changes on android. --- .../Resources/values/styles.xml | 2 +- samples/ControlCatalog/MainView.xaml.cs | 4 ++-- src/Android/Avalonia.Android/AvaloniaMainActivity.cs | 3 +++ .../Avalonia.Android/Platform/AndroidInsetsManager.cs | 11 ++++++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml index 49e079a719..3e1270256d 100644 --- a/samples/ControlCatalog.Android/Resources/values/styles.xml +++ b/samples/ControlCatalog.Android/Resources/values/styles.xml @@ -4,7 +4,7 @@ - diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index f29da3126f..9c439c874f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -122,7 +122,7 @@ namespace ControlCatalog }; ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; - ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? false; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; ViewModel.PropertyChanged += async (sender, args) => { @@ -138,7 +138,7 @@ namespace ControlCatalog // Give the OS some time to apply new values and refresh the view model. await Task.Delay(100); ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; - ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? false; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; }; } diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index 5b22d2c270..eb4b6bf6a0 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -33,6 +33,9 @@ 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 3850199226..35d1b06e6a 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -2,6 +2,7 @@ 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; @@ -124,6 +125,8 @@ namespace Avalonia.Android.Platform { _statusBarTheme = value; + var isDefault = _statusBarTheme == null; + if (!_topLevel.View.IsShown) { return; @@ -143,6 +146,8 @@ 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; } } @@ -150,7 +155,11 @@ namespace Avalonia.Android.Platform { get { - var compat = ViewCompat.GetRootWindowInsets(_topLevel.View); + if(_activity.Window == null) + { + return true; + } + var compat = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); } From 16efee95caef4632a573a860ca7f25e07ce28c8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Mar 2023 16:11:22 +0100 Subject: [PATCH 64/71] Disambiguate ShowWindowTest controls. Under the appium-mac2-driver, `FindElementByAccessibilityId` doesn't actually find elements by their accessibility ID, it also finds them by their accessibility name. Since #10531 added an automation peer for `Label` which sets the accessibility name, the wrong control was being located (e.g. it located the "Position" label instead of the `Name="Position"` text box). Disambiguate these controls by adding a `Current` prefix to make integration tests pass again. --- .../IntegrationTestApp/ShowWindowTest.axaml | 16 +++++++-------- .../ShowWindowTest.axaml.cs | 12 +++++------ .../WindowTests.cs | 20 +++++++++---------- .../WindowTests_MacOS.cs | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 00987429d0..5162eeee92 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -6,27 +6,27 @@ Title="Show Window Test"> - - - + - + - + - + - + Normal Minimized Maximized @@ -34,7 +34,7 @@ - + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 43875dd990..1a267ea20b 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -16,11 +16,11 @@ namespace IntegrationTestApp { InitializeComponent(); DataContext = this; - PositionChanged += (s, e) => this.GetControl("Position").Text = $"{Position}"; + PositionChanged += (s, e) => this.GetControl("CurrentPosition").Text = $"{Position}"; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - _orderTextBox = this.GetControl("Order"); + _orderTextBox = this.GetControl("CurrentOrder"); _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; _timer.Tick += TimerOnTick; _timer.Start(); @@ -36,13 +36,13 @@ namespace IntegrationTestApp { base.OnOpened(e); var scaling = PlatformImpl!.DesktopScaling; - this.GetControl("Position").Text = $"{Position}"; - this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; - this.GetControl("Scaling").Text = $"{scaling}"; + this.GetControl("CurrentPosition").Text = $"{Position}"; + this.GetControl("CurrentScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; + this.GetControl("CurrentScaling").Text = $"{scaling}"; if (Owner is not null) { - var ownerRect = this.GetControl("OwnerRect"); + var ownerRect = this.GetControl("CurrentOwnerRect"); var owner = (Window)Owner; ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index a2bfb618d6..79eea09e01 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -92,7 +92,7 @@ namespace Avalonia.IntegrationTests.Appium { try { - _session.FindElementByAccessibilityId("WindowState").SendClick(); + _session.FindElementByAccessibilityId("CurrentWindowState").SendClick(); _session.FindElementByAccessibilityId("WindowStateNormal").SendClick(); // Wait for animations to run. @@ -112,7 +112,7 @@ namespace Avalonia.IntegrationTests.Appium { using (OpenWindow(new Size(400, 400), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) { - var windowState = _session.FindElementByAccessibilityId("WindowState"); + var windowState = _session.FindElementByAccessibilityId("CurrentWindowState"); Assert.Equal("Normal", windowState.GetComboBoxValue()); @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium public void ShowMode(ShowWindowMode mode) { using var window = OpenWindow(null, mode, WindowStartupLocation.Manual); - var windowState = _session.FindElementByAccessibilityId("WindowState"); + var windowState = _session.FindElementByAccessibilityId("CurrentWindowState"); var original = GetWindowInfo(); Assert.Equal("Normal", windowState.GetComboBoxValue()); @@ -354,7 +354,7 @@ namespace Avalonia.IntegrationTests.Appium { PixelRect? ReadOwnerRect() { - var text = _session.FindElementByAccessibilityId("OwnerRect").Text; + var text = _session.FindElementByAccessibilityId("CurrentOwnerRect").Text; return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null; } @@ -365,13 +365,13 @@ namespace Avalonia.IntegrationTests.Appium try { return new( - Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text), - Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text), - PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text), + Size.Parse(_session.FindElementByAccessibilityId("CurrentClientSize").Text), + Size.Parse(_session.FindElementByAccessibilityId("CurrentFrameSize").Text), + PixelPoint.Parse(_session.FindElementByAccessibilityId("CurrentPosition").Text), ReadOwnerRect(), - PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text), - double.Parse(_session.FindElementByAccessibilityId("Scaling").Text), - Enum.Parse(_session.FindElementByAccessibilityId("WindowState").Text)); + PixelRect.Parse(_session.FindElementByAccessibilityId("CurrentScreenRect").Text), + double.Parse(_session.FindElementByAccessibilityId("CurrentScaling").Text), + Enum.Parse(_session.FindElementByAccessibilityId("CurrentWindowState").Text)); } catch (OpenQA.Selenium.NoSuchElementException) when (retry++ < 3) { diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 90fdc2511f..039d30bbc1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -393,7 +393,7 @@ namespace Avalonia.IntegrationTests.Appium private int GetWindowOrder(string identifier) { var window = GetWindow(identifier); - var order = window.FindElementByXPath("//*[@identifier='Order']"); + var order = window.FindElementByXPath("//*[@identifier='CurrentOrder']"); return int.Parse(order.Text); } From e51aaf4b2dbdda73aa27cc1d7bd0f6a0cc2a9d98 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 3 Mar 2023 16:51:01 +0000 Subject: [PATCH 65/71] only reset _lastWindowState if we are zoomed. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 8e890d3216..840f2c9e88 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -54,7 +54,11 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { WindowBaseImpl::Show(activate, isDialog); GetWindowState(&_actualWindowState); - _lastWindowState = _actualWindowState; + + if(IsZoomed()) { + _lastWindowState = _actualWindowState; + } + return SetWindowState(_lastWindowState); } } From 4203bae16d96660cdf06dd241f40990c9c7ab52d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 3 Mar 2023 17:25:59 +0000 Subject: [PATCH 66/71] fix test. --- tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 6b70edbdf4..aa65f38084 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium using (OpenWindow(new Size(4000, 2200), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) { var measuredWithTextBlock = _session.FindElementById("MeasuredWithText"); - var screenRectTextBox = _session.FindElementById("ScreenRect"); + var screenRectTextBox = _session.FindElementById("CurrentScreenRect"); var measuredWithString = measuredWithTextBlock.Text; var workingAreaString = screenRectTextBox.Text; From 3ec8888b5dcb5b9c7f826e135b1764e32568e47f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 3 Mar 2023 17:35:39 +0000 Subject: [PATCH 67/71] actually fix the test. --- tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index aa65f38084..ce97678a24 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium using (OpenWindow(new Size(4000, 2200), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) { var measuredWithTextBlock = _session.FindElementById("MeasuredWithText"); - var screenRectTextBox = _session.FindElementById("CurrentScreenRect"); + var screenRectTextBox = _session.FindElementById("CurrentClientSize"); var measuredWithString = measuredWithTextBlock.Text; var workingAreaString = screenRectTextBox.Text; From c634f11def09f8346bdbc1a5d11629ed314cf322 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 3 Mar 2023 17:59:59 +0000 Subject: [PATCH 68/71] really really fix the test. --- tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index ce97678a24..948fb3225f 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -156,10 +156,10 @@ namespace Avalonia.IntegrationTests.Appium var measuredWithString = measuredWithTextBlock.Text; var workingAreaString = screenRectTextBox.Text; - var workingArea = Rect.Parse(workingAreaString); + var workingArea = Size.Parse(workingAreaString); var measuredWith = Size.Parse(measuredWithString); - Assert.Equal(workingArea.Size, measuredWith); + Assert.Equal(workingArea, measuredWith); } } From ac9b06ef66c97542ce95b0cde6d3a603b70efa06 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 3 Mar 2023 19:12:56 +0000 Subject: [PATCH 69/71] make integration tests intel compatible --- samples/IntegrationTestApp/bundle.sh | 11 +++++++++-- .../macos-clean-build-test.sh | 8 +++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/samples/IntegrationTestApp/bundle.sh b/samples/IntegrationTestApp/bundle.sh index 505991582e..e52968215b 100755 --- a/samples/IntegrationTestApp/bundle.sh +++ b/samples/IntegrationTestApp/bundle.sh @@ -1,5 +1,12 @@ #!/usr/bin/env bash cd $(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) -dotnet restore -r osx-arm64 -dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-arm64 -p:_AvaloniaUseExternalMSBuild=false \ No newline at end of file + +arch="x64" + +if [[ $(uname -m) == 'arm64' ]]; then +arch="arm64" +fi + +dotnet restore -r osx-$arch +dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-$arch -p:_AvaloniaUseExternalMSBuild=false \ No newline at end of file diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh index dc4619f35c..0d4a1fa57c 100755 --- a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh +++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh @@ -1,5 +1,11 @@ # Cleans, builds, and runs integration tests on macOS. # Can be used by `git bisect run` to automatically find the commit which introduced a problem. +arch="x64" + +if [[ $(uname -m) == 'arm64' ]]; then +arch="arm64" +fi + SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) cd "$SCRIPT_DIR"/../.. || exit git clean -xdf @@ -10,7 +16,7 @@ pkill IntegrationTestApp rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") pkill IntegrationTestApp ./samples/IntegrationTestApp/bundle.sh -open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-arm64/publish/IntegrationTestApp.app +open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-$arch/publish/IntegrationTestApp.app pkill IntegrationTestApp open -b net.avaloniaui.avalonia.integrationtestapp dotnet test tests/Avalonia.IntegrationTests.Appium/ -l "console;verbosity=detailed" From 46b46b2500337460b3e2bb3961ea6717e7ea3656 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 3 Mar 2023 19:33:32 +0000 Subject: [PATCH 70/71] fix intel --- azure-pipelines-integrationtests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 194367c076..dec94a44d5 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -18,6 +18,10 @@ jobs: displayName: 'Get Resolution' - script: | + arch="x64" + if [[ $(uname -m) == 'arm64' ]]; then + arch="arm64" + fi sudo xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node appium > appium.out & @@ -26,7 +30,7 @@ jobs: rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") pkill IntegrationTestApp ./samples/IntegrationTestApp/bundle.sh - open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-arm64/publish/IntegrationTestApp.app + open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-$arch/publish/IntegrationTestApp.app pkill IntegrationTestApp displayName: 'Build IntegrationTestApp' From 4d343293c82b8ecd84715870c3f065252f15caeb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 3 Mar 2023 22:15:50 +0000 Subject: [PATCH 71/71] fix test on windows. --- samples/IntegrationTestApp/ShowWindowTest.axaml | 10 ++++------ tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index ed23797ad7..bd6910dd4d 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -37,13 +37,11 @@ - - - - - - +