From 083c7cb187a0dfe23ae65529f2ba90c7599e13e4 Mon Sep 17 00:00:00 2001 From: Marko Prosen <106885623+maprosen@users.noreply.github.com> Date: Fri, 3 Nov 2023 22:31:35 +0100 Subject: [PATCH 01/60] Fix DateTimePicker scroll down (#13482) --- src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index 5a5c3fdf1d..daa8f1ce13 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -406,7 +406,7 @@ namespace Avalonia.Controls.Primitives /// public void ScrollDown(int numItems = 1) { - var scrollHeight = _extent.Height - Viewport.Height; + var scrollHeight = Math.Max(Extent.Height - ItemHeight, 0); var newY = Math.Min(Offset.Y + (numItems * ItemHeight), scrollHeight); Offset = new Vector(0, newY); } From 52cbe29916fb8ab62837257cbb8e5d63a1e4ed37 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Sun, 5 Nov 2023 02:39:51 +0100 Subject: [PATCH 02/60] feat(DevTools): Pin properties (#13371) * feat(DevTools): Pin properties * fix: Address review * fix: revert using * feat: Show Pin ToggleButton on pointer over. --- src/Avalonia.Diagnostics/Assets/Icons.axaml | 39 ++++++ .../Converters/BoolToImageConverter.cs | 25 ++++ .../ViewModels/AvaloniaPropertyViewModel.cs | 12 +- .../ViewModels/ClrPropertyViewModel.cs | 12 +- .../ViewModels/ControlDetailsViewModel.cs | 126 ++++++++++++------ .../Diagnostics/ViewModels/MainViewModel.cs | 8 +- .../ViewModels/PropertyViewModel.cs | 44 +++--- .../ViewModels/TreePageViewModel.cs | 11 +- .../Diagnostics/Views/ControlDetailsView.xaml | 50 ++++++- 9 files changed, 253 insertions(+), 74 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Assets/Icons.axaml create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToImageConverter.cs diff --git a/src/Avalonia.Diagnostics/Assets/Icons.axaml b/src/Avalonia.Diagnostics/Assets/Icons.axaml new file mode 100644 index 0000000000..f1162855c1 --- /dev/null +++ b/src/Avalonia.Diagnostics/Assets/Icons.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToImageConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToImageConverter.cs new file mode 100644 index 0000000000..6f6c4b977d --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToImageConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Diagnostics.Converters; + +internal class BoolToImageConverter : IValueConverter +{ + public IImage? TrueImage { get; set; } + + public IImage? FalseImage { get; set; } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + value switch + { + true => TrueImage, + false => FalseImage, + _ => null + }; + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + BindingOperations.DoNothing; +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 2412ea5325..d8f69ef6e5 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Data; namespace Avalonia.Diagnostics.ViewModels @@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public override string Group => _group; + public override string Group => IsPinned ? "Pinned" : _group; public override Type? DeclaringType { get; } public override Type PropertyType => _propertyType; @@ -114,5 +115,14 @@ namespace Avalonia.Diagnostics.ViewModels } RaisePropertyChanged(nameof(Type)); } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + if (e.PropertyName == nameof(IsPinned)) + { + RaisePropertyChanged(nameof(Group)); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index b7ee1459f7..60959c7d25 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Reflection; namespace Avalonia.Diagnostics.ViewModels @@ -36,7 +37,7 @@ namespace Avalonia.Diagnostics.ViewModels public PropertyInfo Property { get; } public override object Key => Name; public override string Name { get; } - public override string Group => "CLR Properties"; + public override string Group => IsPinned ? "Pinned" : "CLR Properties"; public override Type AssignedType => _assignedType; public override Type PropertyType => _propertyType; @@ -82,5 +83,14 @@ namespace Avalonia.Diagnostics.ViewModels RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); RaisePropertyChanged(nameof(Type)); } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + if (e.PropertyName == nameof(IsPinned)) + { + RaisePropertyChanged(nameof(Group)); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index fc26d47c78..d57d3088f6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; @@ -10,13 +11,13 @@ using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Styling; -using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { internal class ControlDetailsViewModel : ViewModelBase, IDisposable, IClassesChangedListener { private readonly AvaloniaObject _avaloniaObject; + private readonly ISet _pinnedProperties; private IDictionary? _propertyIndex; private PropertyViewModel? _selectedProperty; private DataGridCollectionView? _propertiesView; @@ -24,19 +25,29 @@ namespace Avalonia.Diagnostics.ViewModels private bool _showInactiveStyles; private string? _styleStatus; private object? _selectedEntity; - private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new(); + private readonly Stack<(string Name, object Entry)> _selectedEntitiesStack = new(); private string? _selectedEntityName; private string? _selectedEntityType; private bool _showImplementedInterfaces; + // new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)) + private readonly static IReadOnlyList GroupDescriptors = new DataGridPathGroupDescription[] + { + new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)) + }; - public ControlDetailsViewModel(TreePageViewModel treePage, AvaloniaObject avaloniaObject) + private readonly static IReadOnlyList SortDescriptions = new DataGridSortDescription[] { - _avaloniaObject = avaloniaObject; + new DataGridComparerSortDescription(PropertyComparer.Instance!, ListSortDirection.Ascending), + }; + public ControlDetailsViewModel(TreePageViewModel treePage, AvaloniaObject avaloniaObject, ISet pinnedProperties) + { + _avaloniaObject = avaloniaObject; + _pinnedProperties = pinnedProperties; TreePage = treePage; - Layout = avaloniaObject is Visual visual - ? new ControlLayoutViewModel(visual) - : default; + Layout = avaloniaObject is Visual visual + ? new ControlLayoutViewModel(visual) + : default; NavigateToProperty(_avaloniaObject, (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString()); @@ -84,7 +95,7 @@ namespace Avalonia.Diagnostics.ViewModels { var setterValue = regularSetter.Value; - var resourceInfo = GetResourceInfo(setterValue); + var resourceInfo = GetResourceInfo(setterValue); SetterViewModel setterVm; @@ -175,13 +186,13 @@ namespace Avalonia.Diagnostics.ViewModels get => _selectedEntityName; set => RaiseAndSetIfChanged(ref _selectedEntityName, value); } - + public string? SelectedEntityType { get => _selectedEntityType; set => RaiseAndSetIfChanged(ref _selectedEntityType, value); } - + public PropertyViewModel? SelectedProperty { get => _selectedProperty; @@ -395,14 +406,23 @@ namespace Avalonia.Diagnostics.ViewModels return !(arg is PropertyViewModel property) || TreePage.PropertiesFilter.Filter(property.Name); } - private class PropertyComparer : IComparer + private class PropertyComparer : IComparer, IComparer { public static PropertyComparer Instance { get; } = new PropertyComparer(); public int Compare(PropertyViewModel? x, PropertyViewModel? y) { - var groupX = GroupIndex(x?.Group); - var groupY = GroupIndex(y?.Group); + if (x is null && y is null) + return 0; + + if (x is null && y is not null) + return -1; + + if (x is not null && y is null) + return 1; + + var groupX = GroupIndex(x!.Group); + var groupY = GroupIndex(y!.Group); if (groupX != groupY) { @@ -410,7 +430,7 @@ namespace Avalonia.Diagnostics.ViewModels } else { - return string.CompareOrdinal(x?.Name, y?.Name); + return string.CompareOrdinal(x.Name, y.Name); } } @@ -418,12 +438,21 @@ namespace Avalonia.Diagnostics.ViewModels { switch (group) { - case "Properties": return 0; - case "Attached Properties": return 1; - case "CLR Properties": return 2; - default: return 3; + case "Pinned": + return -1; + case "Properties": + return 0; + case "Attached Properties": + return 1; + case "CLR Properties": + return 2; + default: + return 3; } } + + public int Compare(object? x, object? y) => + Compare(x as PropertyViewModel, y as PropertyViewModel); } private static IEnumerable GetAllPublicProperties(Type type) @@ -438,8 +467,8 @@ namespace Avalonia.Diagnostics.ViewModels var selectedProperty = SelectedProperty; var selectedEntity = SelectedEntity; var selectedEntityName = SelectedEntityName; - if (selectedEntity == null - || selectedProperty == null + if (selectedEntity == null + || selectedProperty == null || selectedProperty.PropertyType == typeof(string) || selectedProperty.PropertyType.IsValueType) return; @@ -455,19 +484,19 @@ namespace Avalonia.Diagnostics.ViewModels break; case ClrPropertyViewModel clrProperty: - { - property = GetAllPublicProperties(selectedEntity.GetType()) - .FirstOrDefault(pi => clrProperty.Property == pi)? - .GetValue(selectedEntity); + { + property = GetAllPublicProperties(selectedEntity.GetType()) + .FirstOrDefault(pi => clrProperty.Property == pi)? + .GetValue(selectedEntity); - break; - } + break; + } } - if (property == null) + if (property == null) return; - _selectedEntitiesStack.Push((Name:selectedEntityName!, Entry:selectedEntity)); + _selectedEntitiesStack.Push((Name: selectedEntityName!, Entry: selectedEntity)); var propertyName = selectedProperty.Name; @@ -492,7 +521,7 @@ namespace Avalonia.Diagnostics.ViewModels RaisePropertyChanged(nameof(CanNavigateToParentProperty)); } } - + protected void NavigateToProperty(object o, string? entityName) { var oldSelectedEntity = SelectedEntity; @@ -514,17 +543,19 @@ namespace Avalonia.Diagnostics.ViewModels var properties = GetAvaloniaProperties(o) .Concat(GetClrProperties(o, _showImplementedInterfaces)) - .OrderBy(x => x, PropertyComparer.Instance) - .ThenBy(x => x.Name) + .Do(p => + { + p.IsPinned = _pinnedProperties.Contains(p.FullName); + }) .ToArray(); _propertyIndex = properties .GroupBy(x => x.Key) .ToDictionary(x => x.Key, x => x.ToArray()); - var view = new DataGridCollectionView(properties); - view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); + view.GroupDescriptions.AddRange(GroupDescriptors); + view.SortDescriptions.AddRange(SortDescriptions); view.Filter = FilterProperty; PropertiesView = view; @@ -539,7 +570,7 @@ namespace Avalonia.Diagnostics.ViewModels break; } } - + internal void SelectProperty(AvaloniaProperty property) { SelectedProperty = null; @@ -547,10 +578,10 @@ namespace Avalonia.Diagnostics.ViewModels if (SelectedEntity != _avaloniaObject) { NavigateToProperty( - _avaloniaObject, - (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString()); + _avaloniaObject, + (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString()); } - + if (PropertiesView is null) { return; @@ -561,7 +592,7 @@ namespace Avalonia.Diagnostics.ViewModels if (o is AvaloniaPropertyViewModel propertyVm && propertyVm.Property == property) { SelectedProperty = propertyVm; - + break; } } @@ -573,5 +604,24 @@ namespace Avalonia.Diagnostics.ViewModels SelectedProperty = null; NavigateToProperty(_avaloniaObject, (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString()); } + + public void TogglePinnedProperty(object parameter) + { + if (parameter is PropertyViewModel model) + { + var fullname = model.FullName; + if (_pinnedProperties.Contains(fullname)) + { + _pinnedProperties.Remove(fullname); + model.IsPinned = false; + } + else + { + _pinnedProperties.Add(fullname); + model.IsPinned = true; + } + PropertiesView?.Refresh(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index f993d2c957..4462967f03 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -8,6 +8,7 @@ using Avalonia.Metadata; using Avalonia.Threading; using Avalonia.Reactive; using Avalonia.Rendering; +using System.Collections.Generic; namespace Avalonia.Diagnostics.ViewModels { @@ -27,14 +28,15 @@ namespace Avalonia.Diagnostics.ViewModels private string? _pointerOverElementName; private IInputRoot? _pointerOverRoot; private IScreenshotHandler? _screenshotHandler; - private bool _showPropertyType; + private bool _showPropertyType; private bool _showImplementedInterfaces; + private readonly HashSet _pinnedProperties = new(); public MainViewModel(AvaloniaObject root) { _root = root; - _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); - _visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root)); + _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root), _pinnedProperties); + _visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root), _pinnedProperties); _events = new EventsPageViewModel(this); UpdateFocusedControl(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index aa2682e376..f81b34c1bb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -1,27 +1,29 @@ using System; -using System.ComponentModel; -using System.Globalization; -using System.Reflection; -namespace Avalonia.Diagnostics.ViewModels +namespace Avalonia.Diagnostics.ViewModels; + +internal abstract class PropertyViewModel : ViewModelBase { - internal abstract class PropertyViewModel : ViewModelBase - { - 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 object? Value { get; set; } - public abstract string Priority { get; } - public abstract bool? IsAttached { get; } - public abstract void Update(); - public abstract Type PropertyType { get; } + private bool _isPinned; + + 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 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()}}}"; + + public abstract bool IsReadonly { get; } - public string Type => PropertyType == AssignedType ? - PropertyType.GetTypeName() : - $"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}"; + public bool IsPinned { get => _isPinned; set => RaiseAndSetIfChanged(ref _isPinned, value); } - public abstract bool IsReadonly { get; } - } + public string FullName => $"{GetType().Name.Replace("PropertyViewModel","")}:{DeclaringType?.FullName}.{Name}"; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 67dbfed92b..f6fe121f28 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -2,10 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.LogicalTree; -using Avalonia.Metadata; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels @@ -14,12 +10,13 @@ namespace Avalonia.Diagnostics.ViewModels { private TreeNode? _selectedNode; private ControlDetailsViewModel? _details; + private readonly ISet _pinnedProperties; - public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes) + public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes, ISet pinnedProperties) { MainView = mainView; Nodes = nodes; - + _pinnedProperties = pinnedProperties; PropertiesFilter = new FilterViewModel(); PropertiesFilter.RefreshFilter += (s, e) => Details?.PropertiesView?.Refresh(); @@ -45,7 +42,7 @@ namespace Avalonia.Diagnostics.ViewModels if (RaiseAndSetIfChanged(ref _selectedNode, value)) { Details = value != null ? - new ControlDetailsViewModel(this, value.Visual) : + new ControlDetailsViewModel(this, value.Visual, _pinnedProperties) : null; Details?.UpdatePropertiesView(MainView.ShowImplementedInterfaces); Details?.UpdateStyleFilters(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 63b002d110..f62c0f2a72 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -10,10 +10,23 @@ x:DataType="vm:ControlDetailsViewModel"> - - + + + + + + + + + + + @@ -59,7 +72,38 @@ CanUserResizeColumns="true" DoubleTapped="PropertiesGrid_OnDoubleTapped"> - + + + + + + + + + + + + + + + + From b62b00d7e134e0a32042ec08a3c61cadf6ec9208 Mon Sep 17 00:00:00 2001 From: Herman K Date: Sun, 5 Nov 2023 23:26:53 +0200 Subject: [PATCH 03/60] Fixed passing current_folder to DBus for save file dialog (#13491) Co-authored-by: Herman Kirshin --- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 096093347b..8f4942813a 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -88,7 +88,7 @@ namespace Avalonia.FreeDesktop if (options.SuggestedFileName is { } currentName) chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName))); if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) - chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath)))); + chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath + "\0")))); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); From e40d5d624cb6e3eb54743b4046d69ebe69e56b5a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 6 Nov 2023 15:15:35 +0300 Subject: [PATCH 04/60] Fix ImageBrush crash when source bitmap gets disposed (#13506) --- src/Avalonia.Base/Media/Imaging/Bitmap.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 4ddf0eb322..fc6dfb1f19 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -284,6 +284,16 @@ namespace Avalonia.Media.Imaging return AvaloniaLocator.Current.GetRequiredService(); } - IRef IImageBrushSource.Bitmap => PlatformImpl; + IRef? IImageBrushSource.Bitmap + { + get + { + // TODO12: We should probably make PlatformImpl to be nullable or make it possible to check + // and fix IRef in general (right now Item is not nullable while it internally is) + if (PlatformImpl.Item == null!) + return null; + return PlatformImpl; + } + } } } From 020cf9ff213a0c3fa576b13525488b2b65a35b40 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 6 Nov 2023 15:23:42 -0800 Subject: [PATCH 05/60] Make Avalonia.Browser work on .NET 8 (#13312) * Update SkiaSharp/HarfBuzzSharp and use proper native bits, make it configurable * Run UiThreadRender jobs on each animation request in WASM * Update ControlCatalog to match our current templates * Add TaskContinuationOptions.ExecuteSynchronously in compositing engine where it's missed * Log invalid rendering configuration * Use setInterval isntead of Timer on WASM * Minor fixes * Implement BrowserDispatcherImpl and avoid possible memory leak --- .../{ => AppBundle}/Logo.svg | 0 .../ControlCatalog.Browser/AppBundle/app.css | 74 ++++++++++++ .../{ => AppBundle}/embed.js | 0 .../{ => AppBundle}/favicon.ico | Bin .../AppBundle/index.html | 28 +++++ .../{ => AppBundle}/main.js | 5 +- .../ControlCatalog.Browser.csproj | 28 +---- .../EmbedSample.Browser.cs | 3 +- samples/ControlCatalog.Browser/Program.cs | 6 +- samples/ControlCatalog.Browser/Roots.xml | 7 -- samples/ControlCatalog.Browser/app.css | 56 --------- samples/ControlCatalog.Browser/index.html | 31 ----- src/Avalonia.Base/Logging/LogArea.cs | 5 + .../Media/MediaContext.Compositor.cs | 7 +- .../Composition/CompositingRenderer.cs | 2 +- .../Rendering/Composition/Compositor.cs | 3 +- .../Avalonia.Browser/Avalonia.Browser.props | 4 +- .../Avalonia.Browser/Avalonia.Browser.targets | 54 ++++----- src/Browser/Avalonia.Browser/AvaloniaView.cs | 9 +- .../Avalonia.Browser/BrowserDispatcherImpl.cs | 64 ++++++++++ .../Avalonia.Browser/Interop/CanvasHelper.cs | 12 ++ .../Avalonia.Browser/WindowingPlatform.cs | 112 ++++++------------ 22 files changed, 273 insertions(+), 237 deletions(-) rename samples/ControlCatalog.Browser/{ => AppBundle}/Logo.svg (100%) create mode 100644 samples/ControlCatalog.Browser/AppBundle/app.css rename samples/ControlCatalog.Browser/{ => AppBundle}/embed.js (100%) rename samples/ControlCatalog.Browser/{ => AppBundle}/favicon.ico (100%) create mode 100644 samples/ControlCatalog.Browser/AppBundle/index.html rename samples/ControlCatalog.Browser/{ => AppBundle}/main.js (68%) delete mode 100644 samples/ControlCatalog.Browser/Roots.xml delete mode 100644 samples/ControlCatalog.Browser/app.css delete mode 100644 samples/ControlCatalog.Browser/index.html create mode 100644 src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs diff --git a/samples/ControlCatalog.Browser/Logo.svg b/samples/ControlCatalog.Browser/AppBundle/Logo.svg similarity index 100% rename from samples/ControlCatalog.Browser/Logo.svg rename to samples/ControlCatalog.Browser/AppBundle/Logo.svg diff --git a/samples/ControlCatalog.Browser/AppBundle/app.css b/samples/ControlCatalog.Browser/AppBundle/app.css new file mode 100644 index 0000000000..e14dfe4487 --- /dev/null +++ b/samples/ControlCatalog.Browser/AppBundle/app.css @@ -0,0 +1,74 @@ +: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); +} + +/* HTML styles for the splash screen */ + +.highlight { + color: white; + font-size: 2.5rem; + display: block; +} + +.purple { + color: #8b44ac; +} + +.icon { + opacity: 0.05; + height: 35%; + width: 35%; + position: absolute; + background-repeat: no-repeat; + right: 0px; + bottom: 0px; + margin-right: 3%; + margin-bottom: 5%; + z-index: 5000; + background-position: right bottom; + pointer-events: none; +} + +#avalonia-splash a { + color: whitesmoke; + text-decoration: none; +} + +.center { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +#avalonia-splash { + position: relative; + height: 100%; + width: 100%; + color: whitesmoke; + background: #1b2a4e; + font-family: 'Nunito', sans-serif; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + justify-content: center; + align-items: center; +} + +.splash-close { + animation: fadeout 0.25s linear forwards; +} + +@keyframes fadeout { + 0% { + opacity: 100%; + } + + 100% { + opacity: 0; + visibility: collapse; + } +} diff --git a/samples/ControlCatalog.Browser/embed.js b/samples/ControlCatalog.Browser/AppBundle/embed.js similarity index 100% rename from samples/ControlCatalog.Browser/embed.js rename to samples/ControlCatalog.Browser/AppBundle/embed.js diff --git a/samples/ControlCatalog.Browser/favicon.ico b/samples/ControlCatalog.Browser/AppBundle/favicon.ico similarity index 100% rename from samples/ControlCatalog.Browser/favicon.ico rename to samples/ControlCatalog.Browser/AppBundle/favicon.ico diff --git a/samples/ControlCatalog.Browser/AppBundle/index.html b/samples/ControlCatalog.Browser/AppBundle/index.html new file mode 100644 index 0000000000..b35acaed5c --- /dev/null +++ b/samples/ControlCatalog.Browser/AppBundle/index.html @@ -0,0 +1,28 @@ + + + + + + + AvaloniaUI - ControlCatalog + + + + + + +
+
+
+

+ Powered by + Avalonia UI +

+
+ Avalonia Logo +
+
+ + + + diff --git a/samples/ControlCatalog.Browser/main.js b/samples/ControlCatalog.Browser/AppBundle/main.js similarity index 68% rename from samples/ControlCatalog.Browser/main.js rename to samples/ControlCatalog.Browser/AppBundle/main.js index 9d90db8bd2..9eae9fd740 100644 --- a/samples/ControlCatalog.Browser/main.js +++ b/samples/ControlCatalog.Browser/AppBundle/main.js @@ -1,7 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { dotnet } from './dotnet.js' +import { dotnet } from './dotnet.js' // NET 7 +//import { dotnet } from './_framework/dotnet.js' // NET 8+ const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -13,4 +14,4 @@ const dotnetRuntime = await dotnet const config = dotnetRuntime.getConfig(); -await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); +await dotnetRuntime.runMainAndExit(config.mainAssemblyName, [globalThis.location.href]); diff --git a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj index c4278459f3..6a406714c4 100644 --- a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj +++ b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj @@ -1,29 +1,15 @@  + + net7.0 browser-wasm - main.js + AppBundle\main.js Exe true true - true - -sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0 - - - - true - true - full - true - true - -O2 - -O2 - - - - @@ -31,14 +17,8 @@ - - - - - - + - diff --git a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs b/samples/ControlCatalog.Browser/EmbedSample.Browser.cs index c367230ddf..1bd226d578 100644 --- a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs +++ b/samples/ControlCatalog.Browser/EmbedSample.Browser.cs @@ -4,6 +4,7 @@ using Avalonia.Platform; using Avalonia.Browser; using ControlCatalog.Pages; +using System.Threading.Tasks; namespace ControlCatalog.Browser; @@ -25,7 +26,7 @@ public class EmbedSampleWeb : INativeDemoControl _ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ => { EmbedInterop.AddAppButton(defaultHandle.Object); - }); + }, TaskScheduler.FromCurrentSynchronizationContext()); return defaultHandle; } diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index e1a4500173..919df5103c 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -1,8 +1,9 @@ +using System.Diagnostics; using System.Runtime.Versioning; using System.Threading.Tasks; using Avalonia; using Avalonia.Browser; -using Avalonia.Controls; +using Avalonia.Logging; using ControlCatalog; using ControlCatalog.Browser; @@ -12,7 +13,10 @@ internal partial class Program { public static async Task Main(string[] args) { + Trace.Listeners.Add(new ConsoleTraceListener()); + await BuildAvaloniaApp() + .LogToTrace(LogEventLevel.Warning) .AfterSetup(_ => { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); diff --git a/samples/ControlCatalog.Browser/Roots.xml b/samples/ControlCatalog.Browser/Roots.xml deleted file mode 100644 index b07fd86fa2..0000000000 --- a/samples/ControlCatalog.Browser/Roots.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/samples/ControlCatalog.Browser/app.css b/samples/ControlCatalog.Browser/app.css deleted file mode 100644 index 0e6ab12461..0000000000 --- a/samples/ControlCatalog.Browser/app.css +++ /dev/null @@ -1,56 +0,0 @@ -: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 -} - -#avalonia-splash { - position: relative; - height: 100%; - width: 100%; - color: whitesmoke; - background: #171C2C; - font-family: 'Nunito', sans-serif; - background-position: center; - background-size: cover; - background-repeat: no-repeat; -} - -#avalonia-splash a{ - color: whitesmoke; - text-decoration: none; -} - -.center { - display: flex; - justify-content: center; - height: 250px; -} - -.splash-close { - animation: slide 0.5s linear 1s forwards; -} - -@keyframes slide { - 0% { - top: 0%; - } - - 50% { - opacity: 80%; - } - - 100% { - top: 100%; - overflow: hidden; - opacity: 0; - display: none; - visibility: collapse; - } -} diff --git a/samples/ControlCatalog.Browser/index.html b/samples/ControlCatalog.Browser/index.html deleted file mode 100644 index 226ae70695..0000000000 --- a/samples/ControlCatalog.Browser/index.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - AvaloniaUI - ControlCatalog - - - - - - - - - -
-
-
-

Powered by

- - Avalonia Logo - Avalonia - -
-
-
- - - - diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index f15e87da1b..18bd21b664 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -74,5 +74,10 @@ namespace Avalonia.Logging /// The log event comes from macOS Platform /// public const string macOSPlatform = nameof(macOSPlatform); + + /// + /// The log event comes from Browser Platform + /// + public static string BrowserPlatform => nameof(BrowserPlatform); } } diff --git a/src/Avalonia.Base/Media/MediaContext.Compositor.cs b/src/Avalonia.Base/Media/MediaContext.Compositor.cs index feb6fee8d6..41585ddc51 100644 --- a/src/Avalonia.Base/Media/MediaContext.Compositor.cs +++ b/src/Avalonia.Base/Media/MediaContext.Compositor.cs @@ -1,4 +1,6 @@ using System.Linq; +using System.Threading.Tasks; + using Avalonia.Platform; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Transport; @@ -20,7 +22,8 @@ partial class MediaContext _requestedCommits.Remove(compositor); _pendingCompositionBatches[compositor] = commit; commit.Processed.ContinueWith(_ => - _dispatcher.Post(() => CompositionBatchFinished(compositor, commit), DispatcherPriority.Send)); + _dispatcher.Post(() => CompositionBatchFinished(compositor, commit), DispatcherPriority.Send), + TaskContinuationOptions.ExecuteSynchronously); return commit; } @@ -93,7 +96,7 @@ partial class MediaContext // Unit tests are assuming that they can call any API without setting up platforms if (AvaloniaLocator.Current.GetService() == null) return; - + if (compositor is { UseUiThreadForSynchronousCommits: false, diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index ca00b94eaf..52892b379b 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -183,7 +183,7 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester { _queuedSceneInvalidation = false; SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); - }, DispatcherPriority.Input)); + }, DispatcherPriority.Input), TaskContinuationOptions.ExecuteSynchronously); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index ca4dfe5ed3..24817d7865 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -108,7 +108,8 @@ namespace Avalonia.Rendering.Composition var pending = _pendingBatch; if (pending != null) pending.Processed.ContinueWith( - _ => Dispatcher.Post(_triggerCommitRequested, DispatcherPriority.Send)); + _ => Dispatcher.Post(_triggerCommitRequested, DispatcherPriority.Send), + TaskContinuationOptions.ExecuteSynchronously); else _triggerCommitRequested(); } diff --git a/src/Browser/Avalonia.Browser/Avalonia.Browser.props b/src/Browser/Avalonia.Browser/Avalonia.Browser.props index 668dd20789..1cff1c8be5 100644 --- a/src/Browser/Avalonia.Browser/Avalonia.Browser.props +++ b/src/Browser/Avalonia.Browser/Avalonia.Browser.props @@ -1,5 +1,7 @@ - 16384000 + True + True + True diff --git a/src/Browser/Avalonia.Browser/Avalonia.Browser.targets b/src/Browser/Avalonia.Browser/Avalonia.Browser.targets index 22363b33d8..7c396743e7 100644 --- a/src/Browser/Avalonia.Browser/Avalonia.Browser.targets +++ b/src/Browser/Avalonia.Browser/Avalonia.Browser.targets @@ -1,37 +1,33 @@ - - - - - - - True $(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js" $(EmccExtraLDFlags) -sERROR_ON_UNDEFINED_SYMBOLS=0 - true - - true - full - true - -Oz - -Oz - false - false - 0 - false - true - false - false - false - false - false - false - true - true - en - false + + + + + + + true + + + + + + + + + + + + + + + + + + diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index dde4746394..86323afbbd 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -12,6 +12,7 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; +using Avalonia.Logging; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Composition; @@ -138,11 +139,8 @@ namespace Avalonia.Browser } else { - //var rasterInitialized = _interop.InitRaster(); - //Console.WriteLine("raster initialized: {0}", rasterInitialized); - - //_topLevelImpl.SetSurface(ColorType, - // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + Logger.TryGet(LogEventLevel.Error, LogArea.BrowserPlatform)? + .Log(this, "[Avalonia]: Unable to initialize Canvas surface."); } CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); @@ -442,6 +440,7 @@ namespace Avalonia.Browser return; } + Dispatcher.UIThread.RunJobs(DispatcherPriority.UiThreadRender); ManualTriggerRenderTimer.Instance.RaiseTick(); } diff --git a/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs b/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs new file mode 100644 index 0000000000..6ee6c719a7 --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs @@ -0,0 +1,64 @@ +using System; +using System.Diagnostics; +using System.Threading; + +using Avalonia.Browser.Interop; +using Avalonia.Threading; + +namespace Avalonia.Browser; + +internal class BrowserDispatcherImpl : IDispatcherImpl +{ + private readonly Thread _thread; + private readonly Stopwatch _clock; + private bool _signaled; + private int? _timerId; + + private readonly Action _timerCallback; + private readonly Action _signalCallback; + + public BrowserDispatcherImpl() + { + _thread = Thread.CurrentThread; + _clock = Stopwatch.StartNew(); + + _timerCallback = () => Timer?.Invoke(); + _signalCallback = () => + { + _signaled = false; + Signaled?.Invoke(); + }; + } + + public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _thread; + + public long Now => _clock.ElapsedMilliseconds; + + public event Action? Signaled; + public event Action? Timer; + + public void Signal() + { + if (_signaled) + return; + + // NOTE: by HTML5 spec minimal timeout is 4ms, but Chrome seems to work well with 1ms as well. + var interval = 1; + CanvasHelper.SetTimeout(_signalCallback, interval); + } + + public void UpdateTimer(long? dueTimeInMs) + { + if (_timerId is { } timerId) + { + _timerId = null; + CanvasHelper.ClearInterval(timerId); + } + + if (dueTimeInMs.HasValue) + { + var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds); + _timerId = CanvasHelper.SetInterval(_timerCallback, (int)interval); + } + } +} diff --git a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs index 27a2b1dcb7..9e182eaa09 100644 --- a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs @@ -39,4 +39,16 @@ internal static partial class CanvasHelper JSObject canvas, string canvasId, [JSMarshalAs] Action renderFrameCallback); + + [JSImport("globalThis.setTimeout")] + public static partial int SetTimeout([JSMarshalAs] Action callback, int intervalMs); + + [JSImport("globalThis.clearTimeout")] + public static partial int ClearTimeout(int id); + + [JSImport("globalThis.setInterval")] + public static partial int SetInterval([JSMarshalAs] Action callback, int intervalMs); + + [JSImport("globalThis.clearInterval")] + public static partial int ClearInterval(int id); } diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index a23cd01910..2db0e2aec3 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -1,5 +1,4 @@ using System; -using System.Threading; using Avalonia.Browser.Interop; using Avalonia.Browser.Skia; using Avalonia.Input; @@ -8,88 +7,49 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; -namespace Avalonia.Browser -{ - internal class BrowserWindowingPlatform : IWindowingPlatform, IPlatformThreadingInterface - { - private bool _signaled; - private static KeyboardDevice? s_keyboard; - - public IWindowImpl CreateWindow() => throw new NotSupportedException("Browser doesn't support windowing platform. In order to display a single-view content, set ISingleViewApplicationLifetime.MainView."); - - IWindowImpl IWindowingPlatform.CreateEmbeddableWindow() - { - throw new NotImplementedException("Browser doesn't support embeddable windowing platform."); - } - - public ITrayIconImpl? CreateTrayIcon() - { - return null; - } - - public static KeyboardDevice Keyboard => s_keyboard ?? - throw new InvalidOperationException("BrowserWindowingPlatform not registered."); - - public static void Register() - { - var instance = new BrowserWindowingPlatform(); - - s_keyboard = new KeyboardDevice(); - AvaloniaLocator.CurrentMutable - .Bind().ToSingleton() - .Bind().ToSingleton() - .Bind().ToConstant(s_keyboard) - .Bind().ToSingleton() - .Bind().ToConstant(instance) - .Bind().ToConstant(ManualTriggerRenderTimer.Instance) - .Bind().ToConstant(instance) - .Bind().ToConstant(new BrowserSkiaGraphics()) - .Bind().ToSingleton() - .Bind().ToSingleton(); +namespace Avalonia.Browser; - if (AvaloniaLocator.Current.GetService() is { } options - && options.RegisterAvaloniaServiceWorker) - { - var swPath = AvaloniaModule.ResolveServiceWorkerPath(); - AvaloniaModule.RegisterServiceWorker(swPath, options.AvaloniaServiceWorkerScope); - } - } - - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) - { - return new Timer(_ => - { - Dispatcher.UIThread.RunJobs(priority); - tick(); - }, null, interval, interval); - } +internal class BrowserWindowingPlatform : IWindowingPlatform +{ + private static KeyboardDevice? s_keyboard; - public void Signal(DispatcherPriority priority) - { - if (_signaled) - return; + public IWindowImpl CreateWindow() => throw new NotSupportedException("Browser doesn't support windowing platform. In order to display a single-view content, set ISingleViewApplicationLifetime.MainView."); - _signaled = true; - var interval = TimeSpan.FromMilliseconds(1); + IWindowImpl IWindowingPlatform.CreateEmbeddableWindow() + { + throw new NotImplementedException("Browser doesn't support embeddable windowing platform."); + } - IDisposable? disp = null; - disp = new Timer(_ => - { - _signaled = false; - disp?.Dispose(); + public ITrayIconImpl? CreateTrayIcon() + { + return null; + } - Signaled?.Invoke(null); - }, null, interval, interval); - } + public static KeyboardDevice Keyboard => s_keyboard ?? + throw new InvalidOperationException("BrowserWindowingPlatform not registered."); - public bool CurrentThreadIsLoopThread + public static void Register() + { + var instance = new BrowserWindowingPlatform(); + + s_keyboard = new KeyboardDevice(); + AvaloniaLocator.CurrentMutable + .Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToConstant(s_keyboard) + .Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToConstant(ManualTriggerRenderTimer.Instance) + .Bind().ToConstant(instance) + .Bind().ToConstant(new BrowserSkiaGraphics()) + .Bind().ToSingleton() + .Bind().ToSingleton(); + + if (AvaloniaLocator.Current.GetService() is { } options + && options.RegisterAvaloniaServiceWorker) { - get - { - return true; // Browser is single threaded. - } + var swPath = AvaloniaModule.ResolveServiceWorkerPath(); + AvaloniaModule.RegisterServiceWorker(swPath, options.AvaloniaServiceWorkerScope); } - - public event Action? Signaled; } } From c40b627403c39fbc9a077262beb9abc4d9baab2b Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 7 Nov 2023 00:43:50 +0100 Subject: [PATCH 06/60] Fix ref assembly generator making nested types public (#13513) --- api/Avalonia.nupkg.xml | 66 +++++++++++++++++++++++++++++++ nukebuild/RefAssemblyGenerator.cs | 28 +++++-------- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 2230532c05..9b8e28783f 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1003,6 +1003,72 @@ baseline/designer/Avalonia.Designer.HostApp.dll target/designer/Avalonia.Designer.HostApp.dll + + CP0001 + T:Avalonia.AvaloniaLocator.RegistrationHelper`1 + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.AvaloniaLocator.ResolverDisposable + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Input.FocusManager.<GetFocusScopeAncestors>d__18 + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Layout.LayoutManager.<>c + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Layout.LayoutManager.ArrangeResult + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Layout.LayoutManager.EffectiveViewportChangedListener + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Rendering.DefaultRenderTimer.<>c__DisplayClass13_0 + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Rendering.UiThreadRenderTimer.<>c__DisplayClass3_0 + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0001 + T:Avalonia.Controls.Primitives.PopupPositioning.ManagedPopupPositioner.<>c__DisplayClass5_0 + baseline/netstandard2.0/Avalonia.Controls.dll + target/netstandard2.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.PopupPositioning.ManagedPopupPositionerPopupImplHelper.<>c + baseline/netstandard2.0/Avalonia.Controls.dll + target/netstandard2.0/Avalonia.Controls.dll + + + CP0001 + T:Avalonia.Controls.Primitives.PopupPositioning.ManagedPopupPositionerPopupImplHelper.MoveResizeDelegate + baseline/netstandard2.0/Avalonia.Controls.dll + target/netstandard2.0/Avalonia.Controls.dll + CP0006 P:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.ImportCompleted diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index e93070e2f0..f103f16919 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -70,9 +70,6 @@ public class RefAssemblyGenerator static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor) { - foreach (var nested in type.NestedTypes) - ProcessType(nested, obsoleteCtor); - var hideMembers = (type.IsInterface && type.Name.EndsWith("Impl")) || HasPrivateApi(type.CustomAttributes); @@ -97,7 +94,7 @@ public class RefAssemblyGenerator foreach (var m in type.Methods) { - if (hideMembers || HasPrivateApi(m.CustomAttributes)) + if (!m.IsPrivate && (hideMembers || HasPrivateApi(m.CustomAttributes))) { HideMethod(m); } @@ -108,30 +105,27 @@ public class RefAssemblyGenerator { if (HasPrivateApi(p.CustomAttributes)) { - if (p.SetMethod != null) - HideMethod(p.SetMethod); - if (p.GetMethod != null) - HideMethod(p.GetMethod); + if (p.SetMethod is { IsPrivate: false } setMethod) + HideMethod(setMethod); + if (p.GetMethod is { IsPrivate: false } getMethod) + HideMethod(getMethod); } } foreach (var f in type.Fields) { - if (hideMembers || HasPrivateApi(f.CustomAttributes)) + if (!f.IsPrivate && (hideMembers || HasPrivateApi(f.CustomAttributes))) { - var dflags = FieldAttributes.Public | FieldAttributes.Family | FieldAttributes.FamORAssem | - FieldAttributes.FamANDAssem | FieldAttributes.Assembly; - f.Attributes = ((f.Attributes | dflags) ^ dflags) | FieldAttributes.Assembly; + f.IsAssembly = true; } } foreach (var cl in type.NestedTypes) { ProcessType(cl, obsoleteCtor); - if (hideMembers) + if (hideMembers && cl.IsNestedPublic) { - var dflags = TypeAttributes.Public; - cl.Attributes = ((cl.Attributes | dflags) ^ dflags) | TypeAttributes.NotPublic; + cl.IsNestedAssembly = true; } } @@ -143,9 +137,7 @@ public class RefAssemblyGenerator static void HideMethod(MethodDefinition m) { - var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem | - MethodAttributes.FamANDAssem | MethodAttributes.Assembly; - m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; + m.IsAssembly = true; } static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute? unstableAttribute) From 90b09b12dff42ea8190579dfc8ce76f7b31f27f5 Mon Sep 17 00:00:00 2001 From: Glen Stone Date: Mon, 6 Nov 2023 18:45:33 -0500 Subject: [PATCH 07/60] DataGrid inertial scroll support (#13502) (#13511) --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 15 +++++++++++++++ src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 8 ++++++-- src/Avalonia.Controls.DataGrid/Themes/Simple.xaml | 6 ++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 311fb3de40..d7d50f1d6e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -412,6 +412,21 @@ namespace Avalonia.Controls } } + /// + /// Defines the property. + /// + public static readonly AttachedProperty IsScrollInertiaEnabledProperty = + ScrollViewer.IsScrollInertiaEnabledProperty.AddOwner(); + + /// + /// Gets or sets whether scroll gestures should include inertia in their behavior and value. + /// + public bool IsScrollInertiaEnabled + { + get => GetValue(IsScrollInertiaEnabledProperty); + set => SetValue(IsScrollInertiaEnabledProperty, value); + } + private bool _isValid = true; public static readonly DirectProperty IsValidProperty = diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 2d40721bbf..4f4ae74dd9 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -532,9 +532,13 @@ + Grid.ColumnSpan="3" + Grid.Column="0" + ScrollViewer.IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"> - + + Grid.ColumnSpan="2" + ScrollViewer.IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"> + CanVerticallyScroll="True" + IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), ElementName=PART_RowsPresenter}" /> Date: Tue, 7 Nov 2023 00:56:54 +0100 Subject: [PATCH 08/60] Don't clear pointer capture unless we had pointer capture (#13489) --- src/Avalonia.Base/Input/Pointer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs index 8e305a66ad..43692da296 100644 --- a/src/Avalonia.Base/Input/Pointer.cs +++ b/src/Avalonia.Base/Input/Pointer.cs @@ -88,7 +88,10 @@ namespace Avalonia.Input public void Dispose() { - Capture(null); + if (Captured != null) + { + Capture(null); + } } /// From 99737a8395df0caaa854d87664546f803378e646 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Tue, 7 Nov 2023 00:57:45 +0100 Subject: [PATCH 09/60] fix: Navigation when CanExecute is fasle (#13507) * test: Add test navigation with CanExecute is False * fix: Navigation when CanExecute is false --- .../Input/Navigation/TabNavigation.cs | 4 +- .../Input/KeyboardDeviceTests.cs | 15 +------- .../Input/KeyboardNavigationTests_Tab.cs | 38 +++++++++++++++++++ .../Utilities/DelegateCommand.cs | 19 ++++++++++ 4 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/Utilities/DelegateCommand.cs diff --git a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs index 9697e32926..3004a70bdc 100644 --- a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs @@ -649,12 +649,12 @@ namespace Avalonia.Input.Navigation private static bool IsTabStop(IInputElement e) { if (e is InputElement ie) - return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEnabled; + return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEffectivelyEnabled; return false; } private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); private static bool IsVisible(IInputElement e) => (e as Visual)?.IsVisible ?? true; - private static bool IsVisibleAndEnabled(IInputElement e) => IsVisible(e) && e.IsEnabled; + private static bool IsVisibleAndEnabled(IInputElement e) => IsVisible(e) && e.IsEffectivelyEnabled; } } diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs index 28cb9c9282..c486a66da0 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs @@ -1,6 +1,4 @@ -using System; -using System.Windows.Input; -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.UnitTests; @@ -112,7 +110,7 @@ namespace Avalonia.Base.UnitTests.Input button.KeyBindings.Add(new KeyBinding { Gesture = new KeyGesture(Key.O, KeyModifiers.Control), - Command = new DelegateCommand(() => + Command = new Utilities.DelegateCommand(() => { button.KeyBindings.Clear(); ++raised; @@ -134,15 +132,6 @@ namespace Avalonia.Base.UnitTests.Input Assert.Equal(1, raised); } - private class DelegateCommand : ICommand - { - private readonly Action _action; - public DelegateCommand(Action action) => _action = action; - public event EventHandler CanExecuteChanged { add { } remove { } } - public bool CanExecute(object parameter) => true; - public void Execute(object parameter) => _action(); - } - [Fact] public void Control_Focus_Should_Be_Set_Before_FocusedElement_Raises_PropertyChanged() { diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs index 0b3d1a275b..5d8ffd1e13 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Input; @@ -1273,5 +1274,42 @@ namespace Avalonia.Base.UnitTests.Input Assert.True(button.IsFocused); } + + [Fact] + public void Next_Skip_Button_When_Command_CanExecute_Is_False() + { + Button current; + Button expected; + bool executed = false; + + var top = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = + { + new StackPanel + { + Children = + { + (current = new Button { Name = "Button1" }), + new Button + { + Name = "Button2", + Command = new Utilities.DelegateCommand(()=>executed = true, + _ => false), + }, + (expected = new Button { Name = "Button3" }), + } + } + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next) as Button; + + Assert.Equal(expected.Name, result?.Name); + Assert.False(executed); + } + + } } diff --git a/tests/Avalonia.Base.UnitTests/Utilities/DelegateCommand.cs b/tests/Avalonia.Base.UnitTests/Utilities/DelegateCommand.cs new file mode 100644 index 0000000000..0f9c3a0545 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Utilities/DelegateCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Windows.Input; + +namespace Avalonia.Base.UnitTests.Utilities; + +internal class DelegateCommand : ICommand +{ + private readonly Action _action; + private readonly Func _canExecute; + public DelegateCommand(Action action, Func canExecute = default) + { + _action = action; + _canExecute = canExecute ?? new(_ => true); + } + + public event EventHandler CanExecuteChanged { add { } remove { } } + public bool CanExecute(object parameter) => _canExecute(parameter); + public void Execute(object parameter) => _action(); +} From acd626be78ed55d94702126233b5f818b762a7c7 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Tue, 7 Nov 2023 01:09:44 +0100 Subject: [PATCH 10/60] fix(X11): CS0472 The result of the expression is always 'false' (#13510) --- src/Avalonia.X11/Screens/X11Screens.Scaling.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/Screens/X11Screens.Scaling.cs b/src/Avalonia.X11/Screens/X11Screens.Scaling.cs index c57789fcbe..92693db527 100644 --- a/src/Avalonia.X11/Screens/X11Screens.Scaling.cs +++ b/src/Avalonia.X11/Screens/X11Screens.Scaling.cs @@ -197,7 +197,7 @@ internal partial class X11Screens } - if (globalFactorString == null && screenFactorsString == null && usePhysicalDpi == null) + if (globalFactorString == null && screenFactorsString == null) return null; return (userConfig, globalFactor ?? 1, usePhysicalDpi); @@ -246,4 +246,4 @@ internal partial class X11Screens return provider; } -} \ No newline at end of file +} From cbf86c4b896b9ef1313c2f813cd29293c0ab4dd1 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Tue, 7 Nov 2023 02:32:22 +0100 Subject: [PATCH 11/60] feat(DevTools): Focus follower (#12813) * feat(DevTools): Focus follower * fix: remove commented code * fix: Address Review * fix: unused field * fix: missing dispose * fix: do not Avalonia.Diagnostics Popup * feat: using FocusAdornerProperty * code clean up * fix: null annotation * Revert using FocusAdorner --- .../Controls/ControlHighlightAdorner.cs | 47 ++++++ .../Converters/BrushSelectorConveter.cs | 40 +++++ .../Diagnostics/DevToolsOptions.cs | 9 +- .../Diagnostics/ViewModels/MainViewModel.cs | 57 +++++-- .../Diagnostics/Views/MainView.xaml | 158 +++++++++++++++++- 5 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Controls/ControlHighlightAdorner.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Converters/BrushSelectorConveter.cs diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ControlHighlightAdorner.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ControlHighlightAdorner.cs new file mode 100644 index 0000000000..56c933cfbd --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ControlHighlightAdorner.cs @@ -0,0 +1,47 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Reactive; + +namespace Avalonia.Diagnostics.Controls; + +internal class ControlHighlightAdorner : Control +{ + + readonly IPen _pen; + + private ControlHighlightAdorner(IPen pen) + { + _pen = pen; + this.Clip = null; + } + + public static IDisposable? Add(InputElement owner, IBrush highlightBrush) + { + + if (AdornerLayer.GetAdornerLayer(owner) is { } layer) + { + var pen = new Pen(highlightBrush, 2).ToImmutable(); + var adorner = new ControlHighlightAdorner(pen) + { + [AdornerLayer.AdornedElementProperty] = owner + }; + layer.Children.Add(adorner); + + return Disposable.Create((layer, adorner), state => + { + state.layer.Children.Remove(state.adorner); + }); + } + return default; + } + + public override void Render(DrawingContext context) + { + base.Render(context); + context.DrawRectangle(_pen, Bounds.Deflate(2)); + } + +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BrushSelectorConveter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BrushSelectorConveter.cs new file mode 100644 index 0000000000..2216b0e9fc --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/BrushSelectorConveter.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Diagnostics.Converters; + +internal class BrushSelectorConveter : AvaloniaObject, IValueConverter +{ + public static readonly DirectProperty BrushProperty = + AvaloniaProperty.RegisterDirect(nameof(Brush) + , o => o.Brush + , (o, v) => o.Brush = v); + + public IBrush? Brush { get; set; } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (ReferenceEquals(value, parameter)) + { + return Brush; + } + else if (value is ISolidColorBrush a + && parameter is ISolidColorBrush b + && a.Color == b.Color + && a.Transform == b.Transform + && b.Opacity == a.Opacity + ) + { + return Brush; + } + return null; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return BindingOperations.DoNothing; + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index 3cfb0246eb..909c2baa9c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -1,5 +1,5 @@ -using System; -using Avalonia.Input; +using Avalonia.Input; +using Avalonia.Media; using Avalonia.Styling; namespace Avalonia.Diagnostics @@ -47,5 +47,10 @@ namespace Avalonia.Diagnostics /// Gets or sets whether DevTools theme. /// public ThemeVariant? ThemeVariant { get; set; } + + /// + /// Get or set Focus Highlighter + /// + public IBrush? FocusHighlighterBrush { get; set; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 4462967f03..06e4e06f8a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -9,6 +9,8 @@ using Avalonia.Threading; using Avalonia.Reactive; using Avalonia.Rendering; using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Controls.Primitives; namespace Avalonia.Diagnostics.ViewModels { @@ -31,7 +33,9 @@ namespace Avalonia.Diagnostics.ViewModels private bool _showPropertyType; private bool _showImplementedInterfaces; private readonly HashSet _pinnedProperties = new(); - + private IBrush? _FocusHighlighter; + private IDisposable? _currentFocusHighlightAdorner = default; + public MainViewModel(AvaloniaObject root) { _root = root; @@ -210,10 +214,10 @@ namespace Avalonia.Diagnostics.ViewModels private set { RaiseAndSetIfChanged(ref _focusedControl, value); } } - public IInputRoot? PointerOverRoot - { + public IInputRoot? PointerOverRoot + { get => _pointerOverRoot; - private set => RaiseAndSetIfChanged( ref _pointerOverRoot , value); + private set => RaiseAndSetIfChanged(ref _pointerOverRoot, value); } public IInputElement? PointerOverElement @@ -264,7 +268,7 @@ namespace Avalonia.Diagnostics.ViewModels _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); - + _currentFocusHighlightAdorner?.Dispose(); if (TryGetRenderer() is { } renderer) { renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.None; @@ -273,7 +277,20 @@ namespace Avalonia.Diagnostics.ViewModels private void UpdateFocusedControl() { - FocusedControl = KeyboardDevice.Instance?.FocusedElement?.GetType().Name; + var element = KeyboardDevice.Instance?.FocusedElement; + FocusedControl = element?.GetType().Name; + _currentFocusHighlightAdorner?.Dispose(); + if (FocusHighlighter is IBrush brush + && element is InputElement input + && TopLevel.GetTopLevel(input) is { } topLevel + && (topLevel is not Views.MainWindow)) + { + if (topLevel is PopupRoot pr && pr.ParentTopLevel is Views.MainWindow) + { + return; + } + _currentFocusHighlightAdorner = Controls.ControlHighlightAdorner.Add(input, brush); + } } private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -299,7 +316,7 @@ namespace Avalonia.Diagnostics.ViewModels } public int? StartupScreenIndex { get; private set; } = default; - + [DependsOn(nameof(TreePageViewModel.SelectedNode))] [DependsOn(nameof(Content))] public bool CanShot(object? parameter) @@ -333,12 +350,13 @@ namespace Avalonia.Diagnostics.ViewModels _screenshotHandler = options.ScreenshotHandler; StartupScreenIndex = options.StartupScreenIndex; ShowImplementedInterfaces = options.ShowImplementedInterfaces; + FocusHighlighter = options.FocusHighlighterBrush; } - public bool ShowImplementedInterfaces - { - get => _showImplementedInterfaces; - private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces , value); + public bool ShowImplementedInterfaces + { + get => _showImplementedInterfaces; + private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces, value); } public void ToggleShowImplementedInterfaces(object parameter) @@ -351,14 +369,25 @@ namespace Avalonia.Diagnostics.ViewModels } public bool ShowDetailsPropertyType - { - get => _showPropertyType; - private set => RaiseAndSetIfChanged(ref _showPropertyType , value); + { + get => _showPropertyType; + private set => RaiseAndSetIfChanged(ref _showPropertyType, value); } public void ToggleShowDetailsPropertyType(object parameter) { ShowDetailsPropertyType = !ShowDetailsPropertyType; } + + public IBrush? FocusHighlighter + { + get => _FocusHighlighter; + private set => RaiseAndSetIfChanged(ref _FocusHighlighter, value); + } + + public void SelectFocusHighlighter(object parameter) + { + FocusHighlighter = parameter as IBrush; + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index eac807a5bc..f810d8450c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -2,8 +2,24 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="using:Avalonia.Diagnostics.Views" xmlns:viewModels="using:Avalonia.Diagnostics.ViewModels" + xmlns:convertes="using:Avalonia.Diagnostics.Converters" x:Class="Avalonia.Diagnostics.Views.MainView" x:DataType="viewModels:MainViewModel"> + + 16 + + + + + @@ -61,8 +77,7 @@ IsChecked="{Binding ShowDetailsPropertyType}" IsEnabled="False"/> - - + @@ -101,6 +116,145 @@ IsEnabled="False" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8fc0be82cdfa9e2b3e3f7d4eb5edf6619adbdb7a Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Tue, 7 Nov 2023 03:17:07 +0100 Subject: [PATCH 12/60] Converter for DataValidationErrors (#11282) * Introduce ErrorConverter and DisplayErrors attached properties - the converter can be used to change the way a message is print - we use DisplayErrors to get the converted error messages * Adjust FluentTheme * [WIP] Add a sample Page for DataValidationErrors * use a private attached property to store recent errors this approach gets rid of the need to DisplayErrors property * Update samples with some additional details * Reuse rich SetError logic in DataGrid as well * Unify some code with OnErrorsOrConverterChanged * Restore old behavior with null default value * Add SetErrorConverter test --------- Co-authored-by: Max Katz --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/DataValidationPage.axaml | 42 ++++++++ .../Pages/DataValidationPage.axaml.cs | 14 +++ .../ViewModels/DataValidationViewModel.cs | 45 +++++++++ src/Avalonia.Controls.DataGrid/DataGrid.cs | 9 +- src/Avalonia.Controls/DataValidationErrors.cs | 99 +++++++++++++++---- .../TextBoxTests_DataValidation.cs | 24 +++++ 7 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 samples/ControlCatalog/Pages/DataValidationPage.axaml create mode 100644 samples/ControlCatalog/Pages/DataValidationPage.axaml.cs create mode 100644 samples/ControlCatalog/ViewModels/DataValidationViewModel.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 2120b03b20..e19b563fb7 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -74,6 +74,9 @@ ScrollViewer.VerticalScrollBarVisibility="Disabled"> + + + diff --git a/samples/ControlCatalog/Pages/DataValidationPage.axaml b/samples/ControlCatalog/Pages/DataValidationPage.axaml new file mode 100644 index 0000000000..d46562addd --- /dev/null +++ b/samples/ControlCatalog/Pages/DataValidationPage.axaml @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DataValidationPage.axaml.cs b/samples/ControlCatalog/Pages/DataValidationPage.axaml.cs new file mode 100644 index 0000000000..e38f85fec4 --- /dev/null +++ b/samples/ControlCatalog/Pages/DataValidationPage.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages; + +public partial class DataValidationPage : UserControl +{ + public DataValidationPage() + { + InitializeComponent(); + } +} + diff --git a/samples/ControlCatalog/ViewModels/DataValidationViewModel.cs b/samples/ControlCatalog/ViewModels/DataValidationViewModel.cs new file mode 100644 index 0000000000..3b2c37699c --- /dev/null +++ b/samples/ControlCatalog/ViewModels/DataValidationViewModel.cs @@ -0,0 +1,45 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using MiniMvvm; + +namespace ControlCatalog.ViewModels; + +public class DataValidationViewModel : ViewModelBase +{ + private string? _DataAnnotationsSample; + + [Required] + [EmailAddress] + [MinLength(5)] + public string? DataAnnotationsSample + { + get => _DataAnnotationsSample; + set => RaiseAndSetIfChanged(ref _DataAnnotationsSample, value); + } + + public Func Converter { get; } = new Func(o => + { + return $"Error: {o}"; + }); + + + private string? _ExceptionInsideSetterSample; + + public string? ExceptionInsideSetterSample + { + get => _ExceptionInsideSetterSample; + set + { + if (value is null || value.Length < 5) + throw new ArgumentOutOfRangeException(nameof(value), "Give me 5 or more letter please :-)"); + + RaiseAndSetIfChanged(ref _ExceptionInsideSetterSample, value); + } + } + + public Func ExceptionConverter { get; } = new Func(o => + { + return o is Exception ex ? $"Huh, there was an Exception: {ex.Message}" : "Something went really wrong!"; + }); +} diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index d7d50f1d6e..88270ee5cc 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -4160,13 +4160,8 @@ namespace Avalonia.Controls if (editingElement != null) { - var errorList = - binding.ValidationErrors - .SelectMany(ValidationUtil.UnpackException) - .Select(ValidationUtil.UnpackDataValidationException) - .ToList(); - - DataValidationErrors.SetErrors(editingElement, errorList); + DataValidationErrors.SetError(editingElement, + new AggregateException(binding.ValidationErrors)); } } } diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index 4f84a303ea..243032e725 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Reactive; @@ -18,6 +18,8 @@ namespace Avalonia.Controls [PseudoClasses(":error")] public class DataValidationErrors : ContentControl { + private static bool s_overridingErrors; + /// /// Defines the DataValidationErrors.Errors attached property. /// @@ -29,10 +31,24 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty HasErrorsProperty = AvaloniaProperty.RegisterAttached("HasErrors"); + + /// + /// Defines the DataValidationErrors.ErrorConverter attached property. + /// + public static readonly AttachedProperty?> ErrorConverterProperty = + AvaloniaProperty.RegisterAttached?>("ErrorConverter"); + /// + /// Defines the DataValidationErrors.ErrorTemplate property. + /// public static readonly StyledProperty ErrorTemplateProperty = AvaloniaProperty.Register(nameof(ErrorTemplate)); + /// + /// Stores the original, not converted errors passed by the control + /// + private static readonly AttachedProperty?> OriginalErrorsProperty = + AvaloniaProperty.RegisterAttached?>("OriginalErrors"); private Control? _owner; @@ -56,6 +72,12 @@ namespace Avalonia.Controls ErrorsProperty.Changed.Subscribe(ErrorsChanged); HasErrorsProperty.Changed.Subscribe(HasErrorsChanged); TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.OnTemplatedParentChange(e)); + ErrorConverterProperty.Changed.Subscribe(OnErrorConverterChanged); + } + + private static void OnErrorConverterChanged(AvaloniaPropertyChangedEventArgs e) + { + OnErrorsOrConverterChanged((Control)e.Sender); } private void OnTemplatedParentChange(AvaloniaPropertyChangedEventArgs e) @@ -74,15 +96,17 @@ namespace Avalonia.Controls private static void ErrorsChanged(AvaloniaPropertyChangedEventArgs e) { + if (s_overridingErrors) return; + var control = (Control)e.Sender; var errors = (IEnumerable?)e.NewValue; - var hasErrors = false; - if (errors != null && errors.Any()) - hasErrors = true; + // Update original errors + control.SetValue(OriginalErrorsProperty, errors); - control.SetValue(HasErrorsProperty, hasErrors); + OnErrorsOrConverterChanged(control); } + private static void HasErrorsChanged(AvaloniaPropertyChangedEventArgs e) { var control = (Control)e.Sender; @@ -100,8 +124,35 @@ namespace Avalonia.Controls } public static void SetError(Control control, Exception? error) { - SetErrors(control, UnpackException(error)); + SetErrors(control, UnpackException(error)? + .Select(UnpackDataValidationException) + .Where(e => e is not null) + .ToArray()!); + } + + private static void OnErrorsOrConverterChanged(Control control) + { + var converter = GetErrorConverter(control); + var originalErrors = control.GetValue(OriginalErrorsProperty); + var newErrors = (converter is null ? + originalErrors : + originalErrors?.Select(converter) + .Where(e => e is not null))? + .ToArray(); + + s_overridingErrors = true; + try + { + control.SetCurrentValue(ErrorsProperty, newErrors!); + } + finally + { + s_overridingErrors = false; + } + + control.SetValue(HasErrorsProperty, newErrors?.Any() == true); } + public static void ClearErrors(Control control) { SetErrors(control, null); @@ -111,30 +162,36 @@ namespace Avalonia.Controls return control.GetValue(HasErrorsProperty); } - private static IEnumerable? UnpackException(Exception? exception) + public static Func? GetErrorConverter(Control control) + { + return control.GetValue(ErrorConverterProperty); + } + + public static void SetErrorConverter(Control control, Func? converter) + { + control.SetValue(ErrorConverterProperty, converter); + } + + private static IEnumerable? UnpackException(Exception? exception) { if (exception != null) { - var aggregate = exception as AggregateException; - var exceptions = aggregate == null ? - new[] { GetExceptionData(exception) } : - aggregate.InnerExceptions.Select(GetExceptionData).ToArray(); - var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList(); - - if (filtered.Count > 0) - { - return filtered; - } + var exceptions = exception is AggregateException aggregate ? + aggregate.InnerExceptions : + (IEnumerable)new[] { exception }; + + return exceptions.Where(x => !(x is BindingChainException)).ToArray(); } return null; } - private static object GetExceptionData(Exception exception) + private static object? UnpackDataValidationException(Exception exception) { - if (exception is DataValidationException dataValidationException && - dataValidationException.ErrorData is object data) - return data; + if (exception is DataValidationException dataValidationException) + { + return dataValidationException.ErrorData; + } return exception; } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 9a9c4d352e..295fc192d7 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -64,6 +64,30 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Setter_Exceptions_Should_Be_Converter_If_Error_Converter_Set() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + DataContext = new ExceptionTest(), + [!TextBox.TextProperty] = new Binding(nameof(ExceptionTest.LessThan10), BindingMode.TwoWay), + Template = CreateTemplate() + }; + DataValidationErrors.SetErrorConverter(target, err => "Error: " + err); + + target.ApplyTemplate(); + + target.Text = "20"; + + IEnumerable errors = DataValidationErrors.GetErrors(target); + Assert.Single(errors); + var error = Assert.IsType(errors.Single()); + Assert.StartsWith("Error: ", error); + } + } + [Fact] public void Setter_Exceptions_Should_Set_DataValidationErrors_HasErrors() { From a6e936d74accdcf7ead6b776f374defb501e4434 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 7 Nov 2023 09:02:22 +0000 Subject: [PATCH 13/60] Implement Next action in android IME (#13222) * implement Next action in android IME * Handle UIReturnKeyType.Next on iOS * Remove NavigationMethod.Directional (do we need focus adorner?) --------- Co-authored-by: Max Katz Co-authored-by: Julien Lebosquain --- samples/MobileSandbox/MainView.xaml | 2 +- .../Platform/SkiaPlatform/TopLevelImpl.cs | 6 ++++++ src/Avalonia.Base/Input/FocusManager.cs | 11 +++++++++++ src/iOS/Avalonia.iOS/TextInputResponder.cs | 4 ++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/samples/MobileSandbox/MainView.xaml b/samples/MobileSandbox/MainView.xaml index 5d35ec3fec..ccdfb090d2 100644 --- a/samples/MobileSandbox/MainView.xaml +++ b/samples/MobileSandbox/MainView.xaml @@ -8,7 +8,7 @@ - +