From da38b72a35149bc83fe4fb5acb1d828320baccfe Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 15:17:35 +0200 Subject: [PATCH 01/86] fixes(DevTools): Renamed Type as AssignedType --- .../Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs | 8 ++++---- .../Diagnostics/ViewModels/ClrPropertyViewModel.cs | 6 +++--- .../Diagnostics/ViewModels/PropertyViewModel.cs | 2 +- .../Diagnostics/Views/ControlDetailsView.xaml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index e4c4ca6115..6bd26e39d6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -3,7 +3,7 @@ namespace Avalonia.Diagnostics.ViewModels internal class AvaloniaPropertyViewModel : PropertyViewModel { private readonly AvaloniaObject _target; - private string _type; + private string _assignedType; private object? _value; private string _priority; private string _group; @@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels public override string Priority => _priority; - public override string Type => _type; + public override string AssignedType => _assignedType; public override string Value { @@ -56,7 +56,7 @@ namespace Avalonia.Diagnostics.ViewModels if (Property.IsDirect) { RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); + RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); _group = "Properties"; @@ -66,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels var val = _target.GetDiagnostic(Property); RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); + RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); if (val != null) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index 65626aeea5..b87d2b0b53 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -5,7 +5,7 @@ namespace Avalonia.Diagnostics.ViewModels internal class ClrPropertyViewModel : PropertyViewModel { private readonly object _target; - private string _type; + private string _assignedType; private object? _value; #nullable disable @@ -33,7 +33,7 @@ namespace Avalonia.Diagnostics.ViewModels public override string Name { get; } public override string Group => "CLR Properties"; - public override string Type => _type; + public override string AssignedType => _assignedType; public override string Value { @@ -60,7 +60,7 @@ namespace Avalonia.Diagnostics.ViewModels { var val = Property.GetValue(_target); RaiseAndSetIfChanged(ref _value, val, nameof(Value)); - RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type)); + RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index fdbd8c1aa3..080eab072c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -14,7 +14,7 @@ namespace Avalonia.Diagnostics.ViewModels public abstract object Key { get; } public abstract string Name { get; } public abstract string Group { get; } - public abstract string Type { get; } + public abstract string AssignedType { get; } public abstract string Value { get; set; } public abstract string Priority { get; } public abstract bool? IsAttached { get; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 4b37438993..22ad3374ed 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -33,7 +33,7 @@ - + From 4c3666ba214a9926d6468eb912ec4ea5e424b1d7 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 16:18:36 +0200 Subject: [PATCH 02/86] feat(DevTools): Add PropertyType on ControlDettails --- .../ViewModels/AvaloniaPropertyViewModel.cs | 4 +++ .../ViewModels/ClrPropertyViewModel.cs | 4 +++ .../ViewModels/PropertyViewModel.cs | 26 ++++++++++++++++--- .../Diagnostics/Views/ControlDetailsView.xaml | 1 + 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 6bd26e39d6..68e12b545e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -7,6 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels private object? _value; private string _priority; private string _group; + private readonly string _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -20,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels $"[{property.OwnerType.Name}.{property.Name}]" : property.Name; + _propertyType = GetTypeName(property.PropertyType); Update(); } @@ -50,6 +52,8 @@ namespace Avalonia.Diagnostics.ViewModels public override string Group => _group; + public override string PropertyType => _propertyType; + // [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 b87d2b0b53..cbbf3e2fbb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -7,6 +7,7 @@ namespace Avalonia.Diagnostics.ViewModels private readonly object _target; private string _assignedType; private object? _value; + private readonly string _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -25,6 +26,8 @@ namespace Avalonia.Diagnostics.ViewModels Name = property.DeclaringType.Name + '.' + property.Name; } + _propertyType = GetTypeName(property.PropertyType); + Update(); } @@ -34,6 +37,7 @@ namespace Avalonia.Diagnostics.ViewModels public override string Group => "CLR Properties"; public override string AssignedType => _assignedType; + public override string PropertyType => _propertyType; public override string Value { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index 080eab072c..067693de38 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Globalization; using System.Reflection; +using System.Linq; namespace Avalonia.Diagnostics.ViewModels { @@ -17,8 +18,27 @@ namespace Avalonia.Diagnostics.ViewModels public abstract string AssignedType { get; } public abstract string Value { get; set; } public abstract string Priority { get; } - public abstract bool? IsAttached { get; } - public abstract void Update(); + public abstract bool? IsAttached { get; } + public abstract void Update(); + public abstract string PropertyType { get; } + + + protected static string GetTypeName(Type type) + { + var name = type.Name; + if (Nullable.GetUnderlyingType(type) is Type nullable) + { + name = nullable.Name + "?"; + } + else if (type.IsGenericType) + { + var definition = type.GetGenericTypeDefinition(); + var arguments = type.GetGenericArguments(); + name = definition.Name.Substring(0, definition.Name.IndexOf('`')); + name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>"; + } + return name; + } protected static string ConvertToString(object? value) { @@ -30,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels var converter = TypeDescriptor.GetConverter(value); //CollectionConverter does not deliver any important information. It just displays "(Collection)". - if (!converter.CanConvertTo(typeof(string)) || + if (!converter.CanConvertTo(typeof(string)) || converter.GetType() == typeof(CollectionConverter)) { return value.ToString() ?? "(null)"; diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 22ad3374ed..b92ea85227 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -34,6 +34,7 @@ + From 67f83633ce3e7615dd97707e38799c2b769451e5 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 16:26:19 +0200 Subject: [PATCH 03/86] feat(DevTools): Allowed to decode Nullable and Generic Type for AssignedType --- .../Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs | 4 ++-- .../Diagnostics/ViewModels/ClrPropertyViewModel.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 68e12b545e..29e049a5d8 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -60,7 +60,7 @@ namespace Avalonia.Diagnostics.ViewModels if (Property.IsDirect) { RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); + RaiseAndSetIfChanged(ref _assignedType, GetTypeName(_value?.GetType() ?? Property.PropertyType), nameof(AssignedType)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); _group = "Properties"; @@ -70,7 +70,7 @@ namespace Avalonia.Diagnostics.ViewModels var val = _target.GetDiagnostic(Property); RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); + RaiseAndSetIfChanged(ref _assignedType, GetTypeName(_value?.GetType() ?? Property.PropertyType), nameof(AssignedType)); if (val != null) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index cbbf3e2fbb..fe48179150 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -64,7 +64,7 @@ namespace Avalonia.Diagnostics.ViewModels { var val = Property.GetValue(_target); RaiseAndSetIfChanged(ref _value, val, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(AssignedType)); + RaiseAndSetIfChanged(ref _assignedType, GetTypeName(_value?.GetType() ?? Property.PropertyType), nameof(AssignedType)); } } } From 3efcd9d259c43d756d5c1ee1ad548d15cac171f2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 20 Jul 2021 16:35:18 +0200 Subject: [PATCH 04/86] feat(DevTools): Allow caching of GetTypeName --- .../ViewModels/PropertyViewModel.cs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index 067693de38..1de1c34ea0 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Globalization; using System.Reflection; using System.Linq; +using System.Runtime.CompilerServices; namespace Avalonia.Diagnostics.ViewModels { @@ -11,6 +12,8 @@ namespace Avalonia.Diagnostics.ViewModels private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; private static readonly Type[] StringParameter = { typeof(string) }; private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) }; + private static readonly ConditionalWeakTable s_getTypeNameCache = + new ConditionalWeakTable(); public abstract object Key { get; } public abstract string Name { get; } @@ -25,17 +28,21 @@ namespace Avalonia.Diagnostics.ViewModels protected static string GetTypeName(Type type) { - var name = type.Name; - if (Nullable.GetUnderlyingType(type) is Type nullable) + if (!s_getTypeNameCache.TryGetValue(type, out var name)) { - name = nullable.Name + "?"; - } - else if (type.IsGenericType) - { - var definition = type.GetGenericTypeDefinition(); - var arguments = type.GetGenericArguments(); - name = definition.Name.Substring(0, definition.Name.IndexOf('`')); - name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>"; + name = type.Name; + if (Nullable.GetUnderlyingType(type) is Type nullable) + { + name = nullable.Name + "?"; + } + else if (type.IsGenericType) + { + var definition = type.GetGenericTypeDefinition(); + var arguments = type.GetGenericArguments(); + name = definition.Name.Substring(0, definition.Name.IndexOf('`')); + name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>"; + } + s_getTypeNameCache.Add(type, name); } return name; } From d0e810305192ab8d058095f759743bae06103628 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 21 Jul 2021 09:25:10 +0200 Subject: [PATCH 05/86] feat(DevTools): Allow toggle PropertyType column visibility --- .../Diagnostics/ViewModels/MainViewModel.cs | 16 ++++++++++++++-- .../Diagnostics/Views/ControlDetailsView.xaml | 5 ++++- .../Diagnostics/Views/MainView.xaml | 10 ++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 3f367165ac..90f127acd2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -22,6 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; + private bool _showPropertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -46,7 +47,7 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeMarginPadding; set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); } - + public bool ShouldVisualizeDirtyRects { get => _shouldVisualizeDirtyRects; @@ -151,7 +152,7 @@ namespace Avalonia.Diagnostics.ViewModels get { return _pointerOverElement; } private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } } - + private void UpdateConsoleContext(ConsoleContext context) { context.root = _root; @@ -220,5 +221,16 @@ namespace Avalonia.Diagnostics.ViewModels { StartupScreenIndex = options.StartupScreenIndex; } + + public bool ShowDettailsPropertyType + { + get => _showPropertyType; + private set => RaiseAndSetIfChanged(ref _showPropertyType , value); + } + + public void ToggleShowDettailsPropertyType(object paramter) + { + ShowDettailsPropertyType = !ShowDettailsPropertyType; + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index b92ea85227..5dfc75106a 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -34,7 +34,10 @@ - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 8c4db33f91..6d01eb27a1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -15,6 +15,16 @@ IsEnabled="False"/> + + + + + + + + From 739d9de5b7c70d2d35d28063e19663745919ffb4 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 1 Sep 2021 12:32:40 +0200 Subject: [PATCH 06/86] feat(DevTools): Shows the property type and the type assigned to the property in a single or split column. --- .../Diagnostics/ViewModels/PropertyViewModel.cs | 3 +++ .../Diagnostics/Views/ControlDetailsView.xaml | 9 ++++++++- src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs index 1de1c34ea0..d3cdff6e71 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -24,6 +24,9 @@ namespace Avalonia.Diagnostics.ViewModels public abstract bool? IsAttached { get; } public abstract void Update(); public abstract string PropertyType { get; } + public string Type => PropertyType == AssignedType + ? PropertyType + : $"{PropertyType} {{{AssignedType}}}"; protected static string GetTypeName(Type type) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 5dfc75106a..e6965884b1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -33,7 +33,14 @@ - + + - + Date: Wed, 1 Sep 2021 17:03:26 +0200 Subject: [PATCH 07/86] feat: Added Post to IDispatcher --- src/Avalonia.Base/Threading/IDispatcher.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs index 8f46f99283..538fa028b9 100644 --- a/src/Avalonia.Base/Threading/IDispatcher.cs +++ b/src/Avalonia.Base/Threading/IDispatcher.cs @@ -27,6 +27,15 @@ namespace Avalonia.Threading /// A task that can be used to track the method's execution. void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal); + /// + /// Invokes a method on the dispatcher thread. + /// + /// type of argument + /// The method to call. + /// The argument of method to call. + /// The priority with which to invoke the method. + void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal); + /// /// Posts an action that will be invoked on the dispatcher thread. /// @@ -59,4 +68,4 @@ namespace Avalonia.Threading /// A task that represents a proxy for the task returned by . Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal); } -} \ No newline at end of file +} From 75bd449a6901f2a405daeeebc79c9d92e4d68b8a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 1 Sep 2021 17:18:44 +0200 Subject: [PATCH 08/86] feat: Implement Post method of IDispatcher. --- src/Avalonia.Base/ApiCompatBaseline.txt | 3 + src/Avalonia.Base/Threading/Dispatcher.cs | 11 ++- src/Avalonia.Base/Threading/JobRunner.cs | 84 ++++++++++++++++--- .../Avalonia.UnitTests/ImmediateDispatcher.cs | 6 ++ 4 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 src/Avalonia.Base/ApiCompatBaseline.txt diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt new file mode 100644 index 0000000000..4701a83175 --- /dev/null +++ b/src/Avalonia.Base/ApiCompatBaseline.txt @@ -0,0 +1,3 @@ +Compat issues with assembly Avalonia.Base: +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Threading.IDispatcher.Post(System.Action, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract. +Total Issues: 1 diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index fe2cec11f0..217f1b9c3f 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -81,7 +81,7 @@ namespace Avalonia.Threading Contract.Requires(action != null); return _jobRunner.InvokeAsync(action, priority); } - + /// public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) { @@ -110,6 +110,13 @@ namespace Avalonia.Threading _jobRunner.Post(action, priority); } + /// + public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) + { + Contract.Requires(action != null); + _jobRunner.Post(action, arg, priority); + } + /// /// This is needed for platform backends that don't have internal priority system (e. g. win32) /// To ensure that there are no jobs with higher priority @@ -142,4 +149,4 @@ namespace Avalonia.Threading } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs index 58f623fb3f..7aeb1972ea 100644 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ b/src/Avalonia.Base/Threading/JobRunner.cs @@ -13,7 +13,7 @@ namespace Avalonia.Threading { private IPlatformThreadingInterface _platform; - private readonly Queue[] _queues = Enumerable.Range(0, (int) DispatcherPriority.MaxValue + 1) + private readonly Queue[] _queues = Enumerable.Range(0, (int)DispatcherPriority.MaxValue + 1) .Select(_ => new Queue()).ToArray(); public JobRunner(IPlatformThreadingInterface platform) @@ -59,7 +59,7 @@ namespace Avalonia.Threading /// A task that can be used to track the method's execution. public Task InvokeAsync(Func function, DispatcherPriority priority) { - var job = new Job(function, priority); + var job = new JobWithResult(function, priority); AddJob(job); return job.Task; } @@ -75,6 +75,17 @@ namespace Avalonia.Threading AddJob(new Job(action, priority, true)); } + /// + /// Post action that will be invoked on main thread + /// + /// The method to call. + /// The parameter of method to call. + /// The priority with which to invoke the method. + internal void Post(Action action, T parameter, DispatcherPriority priority) + { + AddJob(new Job(action, parameter, priority, true)); + } + /// /// Allows unit tests to change the platform threading interface. /// @@ -86,7 +97,7 @@ namespace Avalonia.Threading private void AddJob(IJob job) { bool needWake; - var queue = _queues[(int) job.Priority]; + var queue = _queues[(int)job.Priority]; lock (queue) { needWake = queue.Count == 0; @@ -98,7 +109,7 @@ namespace Avalonia.Threading private IJob GetNextJob(DispatcherPriority minimumPriority) { - for (int c = (int) DispatcherPriority.MaxValue; c >= (int) minimumPriority; c--) + for (int c = (int)DispatcherPriority.MaxValue; c >= (int)minimumPriority; c--) { var q = _queues[c]; lock (q) @@ -109,14 +120,14 @@ namespace Avalonia.Threading } return null; } - + private interface IJob { /// /// Gets the job priority. /// DispatcherPriority Priority { get; } - + /// /// Runs the job. /// @@ -157,7 +168,7 @@ namespace Avalonia.Threading /// The task. /// public Task Task => _taskCompletionSource?.Task; - + /// void IJob.Run() { @@ -177,11 +188,60 @@ namespace Avalonia.Threading } } } - + /// - /// A job to run. + /// A tipizzed job to run. /// - private sealed class Job : IJob + private sealed class Job : IJob + { + private readonly Action _action; + private readonly T _parameter; + private readonly TaskCompletionSource _taskCompletionSource; + + /// + /// Initializes a new instance of the class. + /// + /// The method to call. + /// The parameter of method to call. + /// The job priority. + /// Do not wrap exception in TaskCompletionSource + + public Job(Action action, T parameter, DispatcherPriority priority, bool throwOnUiThread) + { + _action = action; + _parameter = parameter; + Priority = priority; + _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); + } + + /// + public DispatcherPriority Priority { get; } + + /// + void IJob.Run() + { + if (_taskCompletionSource == null) + { + _action(_parameter); + return; + } + try + { + _action(_parameter); + _taskCompletionSource.SetResult(null); + } + catch (Exception e) + { + _taskCompletionSource.SetException(e); + } + } + } + + + /// + /// A job to run thath return value. + /// + private sealed class JobWithResult : IJob { private readonly Func _function; private readonly TaskCompletionSource _taskCompletionSource; @@ -191,7 +251,7 @@ namespace Avalonia.Threading /// /// The method to call. /// The job priority. - public Job(Func function, DispatcherPriority priority) + public JobWithResult(Func function, DispatcherPriority priority) { _function = function; Priority = priority; @@ -200,7 +260,7 @@ namespace Avalonia.Threading /// public DispatcherPriority Priority { get; } - + /// /// The task. /// diff --git a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs index fac4ee64e7..5f0d41590f 100644 --- a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs +++ b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs @@ -21,6 +21,12 @@ namespace Avalonia.UnitTests action(); } + /// + public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) + { + action(arg); + } + /// public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) { From aa5df186f049065a7d2167a54f1d3725a8b23738 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 2 Sep 2021 09:40:54 +0200 Subject: [PATCH 09/86] fixes: XML Code Comment --- src/Avalonia.Base/Threading/JobRunner.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs index 7aeb1972ea..357fbd9b59 100644 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ b/src/Avalonia.Base/Threading/JobRunner.cs @@ -190,8 +190,9 @@ namespace Avalonia.Threading } /// - /// A tipizzed job to run. + /// A typed job to run. /// + /// Type of job parameter private sealed class Job : IJob { private readonly Action _action; @@ -237,10 +238,10 @@ namespace Avalonia.Threading } } - /// /// A job to run thath return value. /// + /// Type of job result private sealed class JobWithResult : IJob { private readonly Func _function; From 7c414bb3b10fc6199f280fff8be02854724504f8 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 20 Oct 2021 11:14:33 +0200 Subject: [PATCH 10/86] feat(DevTools): Allow to Show/Hide implemented interfaces on Control Details --- .../Diagnostics/DevToolsOptions.cs | 5 ++ .../ViewModels/ControlDetailsViewModel.cs | 46 +++++++++++-------- .../Diagnostics/ViewModels/MainViewModel.cs | 21 ++++++++- .../ViewModels/TreePageViewModel.cs | 6 +++ .../Diagnostics/Views/MainView.xaml | 10 ++++ 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index 5336dca65b..6b5e7b1ec1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -27,5 +27,10 @@ namespace Avalonia.Diagnostics /// Get or set the startup screen index where the DevTools window will be displayed. /// public int? StartupScreenIndex { get; set; } + + /// + /// Gets or sets a value indicating whether DevTools should be displayed implemented interfaces on Control details. The default value is true. + /// + public bool ShowImplementedInterfaces { get; set; } = true; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 3790951b0c..45e578dc10 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -17,11 +17,12 @@ namespace Avalonia.Diagnostics.ViewModels internal class ControlDetailsViewModel : ViewModelBase, IDisposable { private readonly IVisual _control; - private readonly IDictionary> _propertyIndex; + private IDictionary>? _propertyIndex; private PropertyViewModel? _selectedProperty; private bool _snapshotStyles; private bool _showInactiveStyles; private string? _styleStatus; + private DataGridCollectionView _propertiesView; public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { @@ -29,19 +30,6 @@ namespace Avalonia.Diagnostics.ViewModels TreePage = treePage; - var properties = GetAvaloniaProperties(control) - .Concat(GetClrProperties(control)) - .OrderBy(x => x, PropertyComparer.Instance) - .ThenBy(x => x.Name) - .ToList(); - - _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList()); - - var view = new DataGridCollectionView(properties); - view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); - view.Filter = FilterProperty; - PropertiesView = view; - Layout = new ControlLayoutViewModel(control); if (control is INotifyPropertyChanged inpc) @@ -133,7 +121,7 @@ namespace Avalonia.Diagnostics.ViewModels public TreePageViewModel TreePage { get; } - public DataGridCollectionView PropertiesView { get; } + public DataGridCollectionView PropertiesView { get => _propertiesView; private set => RaiseAndSetIfChanged( ref _propertiesView , value); } public ObservableCollection AppliedStyles { get; } @@ -227,18 +215,21 @@ namespace Avalonia.Diagnostics.ViewModels } } - private IEnumerable GetClrProperties(object o) + private IEnumerable GetClrProperties(object o, bool showImplementedInterfaces) { foreach (var p in GetClrProperties(o, o.GetType())) { yield return p; } - foreach (var i in o.GetType().GetInterfaces()) + if (showImplementedInterfaces) { - foreach (var p in GetClrProperties(o, i)) + foreach (var i in o.GetType().GetInterfaces()) { - yield return p; + foreach (var p in GetClrProperties(o, i)) + { + yield return p; + } } } } @@ -378,5 +369,22 @@ namespace Avalonia.Diagnostics.ViewModels } } } + + public void UpdatePropertiesView(bool showImplementedInterfaces) + { + + var properties = GetAvaloniaProperties(_control) + .Concat(GetClrProperties(_control, showImplementedInterfaces)) + .OrderBy(x => x, PropertyComparer.Instance) + .ThenBy(x => x.Name) + .ToList(); + + _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList()); + + var view = new DataGridCollectionView(properties); + view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); + view.Filter = FilterProperty; + PropertiesView = view; + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index d0a4ad38c5..bb94ba16bb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -22,6 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; private bool _freezePopups; + private bool _showImplementedInterfaces; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -52,7 +53,7 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeMarginPadding; set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); } - + public bool ShouldVisualizeDirtyRects { get => _shouldVisualizeDirtyRects; @@ -157,7 +158,7 @@ namespace Avalonia.Diagnostics.ViewModels get { return _pointerOverElement; } private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } } - + private void UpdateConsoleContext(ConsoleContext context) { context.root = _root; @@ -225,6 +226,22 @@ namespace Avalonia.Diagnostics.ViewModels public void SetOptions(DevToolsOptions options) { StartupScreenIndex = options.StartupScreenIndex; + ShowImplementedInterfaces = options.ShowImplementedInterfaces; + } + + public bool ShowImplementedInterfaces + { + get => _showImplementedInterfaces; + private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces , value); + } + + public void ToggleShowImplementedInterfaces(object parametr) + { + ShowImplementedInterfaces = !ShowImplementedInterfaces; + if (Content is TreePageViewModel viewModel) + { + viewModel.UpdatePropertiesView(); + } } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 4b18cf414a..f43896f810 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -39,6 +39,7 @@ namespace Avalonia.Diagnostics.ViewModels Details = value != null ? new ControlDetailsViewModel(this, value.Visual) : null; + Details?.UpdatePropertiesView(MainView.ShowImplementedInterfaces); Details?.UpdateStyleFilters(); } } @@ -134,5 +135,10 @@ namespace Avalonia.Diagnostics.ViewModels return null; } + + internal void UpdatePropertiesView() + { + Details?.UpdatePropertiesView(MainView?.ShowImplementedInterfaces ?? true); + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 6f2ac96a66..f45a65dbf3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -38,6 +38,16 @@ IsEnabled="False" /> + + + + + + + + From ae48a9a13b22df8bd2cecfff923e9a5374f31cdd Mon Sep 17 00:00:00 2001 From: Foaltin Dorin Date: Fri, 12 Nov 2021 15:37:59 +0200 Subject: [PATCH 11/86] Fix DataGrid selection broken after 3 right clicks --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 10c7c16488..95ee73be4e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5751,6 +5751,7 @@ namespace Avalonia.Controls return true; } // Unselect everything except the row that was clicked on + _noSelectionChangeCount++; try { UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); From 4b3b37c961945978b1abdd07dc34f752c310d9de Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 13 Dec 2021 15:16:13 +0100 Subject: [PATCH 12/86] fixes(DevTools): The Type column does not update when the property value changes. --- .../Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs | 2 ++ .../Diagnostics/ViewModels/ClrPropertyViewModel.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index 46f8614273..a09e17ea68 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -45,6 +45,7 @@ namespace Avalonia.Diagnostics.ViewModels { var convertedValue = ConvertFromString(value, Property.PropertyType); _target.SetValue(Property, convertedValue); + Update(); } catch { } } @@ -84,6 +85,7 @@ namespace Avalonia.Diagnostics.ViewModels RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group)); } } + RaisePropertyChanged(nameof(Type)); } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index b6bdb00170..e87be9fa36 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -48,6 +48,7 @@ namespace Avalonia.Diagnostics.ViewModels { var convertedValue = ConvertFromString(value, Property.PropertyType); Property.SetValue(_target, convertedValue); + Update(); } catch { } } @@ -67,6 +68,7 @@ namespace Avalonia.Diagnostics.ViewModels var val = Property.GetValue(_target); RaiseAndSetIfChanged(ref _value, val, nameof(Value)); RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + RaisePropertyChanged(nameof(Type)); } } } From 69c1a37372374fa38f80b97996f24a63d88a1024 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sun, 19 Dec 2021 11:18:44 +0200 Subject: [PATCH 13/86] some very initial implementation --- samples/ControlCatalog/App.xaml | 1 + samples/ControlCatalog/App.xaml.cs | 1 - samples/ControlCatalog/MainView.xaml.cs | 10 +-- src/Avalonia.Themes.Fluent/FluentTheme.cs | 77 +++++++++++++++++++++-- 4 files changed, 74 insertions(+), 15 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 227b31bf20..d6ffa04944 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -5,6 +5,7 @@ x:CompileBindings="True" x:Class="ControlCatalog.App"> + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 36b6fc2dcd..085de2ae7a 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -96,7 +96,6 @@ namespace ControlCatalog public override void Initialize() { - Styles.Insert(0, FluentLight); AvaloniaXamlLoader.Load(this); } diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index abedda3c85..3898e9da85 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -11,6 +11,7 @@ using Avalonia.Media.Immutable; using Avalonia.Platform; using ControlCatalog.Pages; using ControlCatalog.Models; +using Avalonia.Themes.Fluent; namespace ControlCatalog { @@ -43,14 +44,7 @@ namespace ControlCatalog { if (themes.SelectedItem is CatalogTheme theme) { - Application.Current.Styles[0] = theme switch - { - CatalogTheme.FluentLight => App.FluentLight, - CatalogTheme.FluentDark => App.FluentDark, - CatalogTheme.DefaultLight => App.DefaultLight, - CatalogTheme.DefaultDark => App.DefaultDark, - _ => Application.Current.Styles[0] - }; + (Application.Current.Styles[0] as FluentTheme).Mode = FluentThemeMode.Dark; } }; diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 43b71567fa..812bd22839 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; #nullable enable @@ -22,6 +23,7 @@ namespace Avalonia.Themes.Fluent private readonly Uri _baseUri; private IStyle[]? _loaded; private bool _isLoading; + private FluentThemeMode _mode; /// /// Initializes a new instance of the class. @@ -44,7 +46,20 @@ namespace Avalonia.Themes.Fluent /// /// Gets or sets the mode of the fluent theme (light, dark). /// - public FluentThemeMode Mode { get; set; } + public FluentThemeMode Mode + { + get => _mode; + set + { + if (_mode != value) + { + _mode = value; + (Loaded as Styles)[3] = FluentDark[0]; + (Loaded as Styles)[4] = FluentDark[1]; + } + + } + } public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner; @@ -58,8 +73,25 @@ namespace Avalonia.Themes.Fluent if (_loaded == null) { _isLoading = true; - var loaded = (IStyle)AvaloniaXamlLoader.Load(GetUri(), _baseUri); - _loaded = new[] { loaded }; + Styles? resultStyle = new Styles(); + + resultStyle.AddRange(SharedStyles); + + if (Mode == FluentThemeMode.Light) + { + for (int i = 0; i < FluentLight.Count; i++) + { + resultStyle.Add(FluentLight[i]); + } + } + else if (Mode == FluentThemeMode.Dark) + { + for (int i = 0; i < FluentDark.Count; i++) + { + resultStyle.Add(FluentDark[i]); + } + } + _loaded = new[] { resultStyle }; _isLoading = false; } @@ -105,10 +137,43 @@ namespace Avalonia.Themes.Fluent void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner); void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); - private Uri GetUri() => Mode switch + private static Styles SharedStyles = new Styles + { + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Controls/FluentControls.xaml") + } + }; + + private static Styles FluentLight = new Styles { - FluentThemeMode.Dark => new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml", UriKind.Absolute), - _ => new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml", UriKind.Absolute), + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml") + } + }; + private static Styles FluentDark = new Styles + { + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml") + } }; } } From 032b78cd0698215e084f5352159c77706239429a Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sun, 19 Dec 2021 11:34:16 +0200 Subject: [PATCH 14/86] wip --- src/Avalonia.Themes.Fluent/FluentTheme.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 812bd22839..73bf5b7226 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -21,7 +21,7 @@ namespace Avalonia.Themes.Fluent public class FluentTheme : IStyle, IResourceProvider { private readonly Uri _baseUri; - private IStyle[]? _loaded; + private IStyle? _loaded; private bool _isLoading; private FluentThemeMode _mode; @@ -91,17 +91,17 @@ namespace Avalonia.Themes.Fluent resultStyle.Add(FluentDark[i]); } } - _loaded = new[] { resultStyle }; + _loaded = resultStyle; _isLoading = false; } - return _loaded?[0]!; + return _loaded; } } bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; - IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); + IReadOnlyList IStyle.Children => _loaded?.Children ?? Array.Empty(); public event EventHandler OwnerChanged { From 9c8b68ff5d0f5fd6cf5a4158b2aa4c093e6d3047 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sun, 19 Dec 2021 12:09:08 +0200 Subject: [PATCH 15/86] more refactorings --- src/Avalonia.Themes.Fluent/FluentTheme.cs | 97 ++++++++++++----------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 73bf5b7226..08d9dc317d 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -21,8 +21,11 @@ namespace Avalonia.Themes.Fluent public class FluentTheme : IStyle, IResourceProvider { private readonly Uri _baseUri; - private IStyle? _loaded; + private Styles _fluentDark = new(); + private Styles _fluentLight = new(); + private Styles _sharedStyles = new(); private bool _isLoading; + private IStyle? _loaded; private FluentThemeMode _mode; /// @@ -32,6 +35,7 @@ namespace Avalonia.Themes.Fluent public FluentTheme(Uri baseUri) { _baseUri = baseUri; + InitStyles(baseUri); } /// @@ -41,6 +45,7 @@ namespace Avalonia.Themes.Fluent public FluentTheme(IServiceProvider serviceProvider) { _baseUri = ((IUriContext)serviceProvider.GetService(typeof(IUriContext))).BaseUri; + InitStyles(_baseUri); } /// @@ -54,8 +59,8 @@ namespace Avalonia.Themes.Fluent if (_mode != value) { _mode = value; - (Loaded as Styles)[3] = FluentDark[0]; - (Loaded as Styles)[4] = FluentDark[1]; + (Loaded as Styles)![1] = _fluentDark[0]; + (Loaded as Styles)![2] = _fluentDark[1]; } } @@ -73,23 +78,17 @@ namespace Avalonia.Themes.Fluent if (_loaded == null) { _isLoading = true; - Styles? resultStyle = new Styles(); - - resultStyle.AddRange(SharedStyles); + Styles? resultStyle = new Styles() { _sharedStyles }; if (Mode == FluentThemeMode.Light) { - for (int i = 0; i < FluentLight.Count; i++) - { - resultStyle.Add(FluentLight[i]); - } + resultStyle.Add(_fluentLight[0]); + resultStyle.Add(_fluentLight[1]); } else if (Mode == FluentThemeMode.Dark) { - for (int i = 0; i < FluentDark.Count; i++) - { - resultStyle.Add(FluentDark[i]); - } + resultStyle.Add(_fluentDark[0]); + resultStyle.Add(_fluentDark[1]); } _loaded = resultStyle; _isLoading = false; @@ -137,43 +136,47 @@ namespace Avalonia.Themes.Fluent void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner); void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); - private static Styles SharedStyles = new Styles + private void InitStyles(Uri baseUri) { - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + _sharedStyles = new Styles { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml") - }, - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") - }, - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Controls/FluentControls.xaml") - } - }; + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Controls/FluentControls.xaml") + } + }; - private static Styles FluentLight = new Styles - { - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") - }, - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + _fluentLight = new Styles { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml") - } - }; - private static Styles FluentDark = new Styles - { - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") - }, - new StyleInclude(new Uri("resm:Styles?assembly=Avalonia.Themes.Fluent")) + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml") + } + }; + + _fluentDark = new Styles { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml") - } - }; + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml") + } + }; + } } } From 22be08125735ab59155c549f2a534b29bfbf335e Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sun, 19 Dec 2021 13:10:05 +0200 Subject: [PATCH 16/86] more refactorings --- src/Avalonia.Themes.Fluent/FluentTheme.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 08d9dc317d..1c61c03c97 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -59,8 +59,18 @@ namespace Avalonia.Themes.Fluent if (_mode != value) { _mode = value; - (Loaded as Styles)![1] = _fluentDark[0]; - (Loaded as Styles)![2] = _fluentDark[1]; + if (_mode == FluentThemeMode.Dark) + { + (Loaded as Styles)![1] = _fluentDark[0]; + (Loaded as Styles)![2] = _fluentDark[1]; + } + else + { + (Loaded as Styles)![1] = _fluentLight[0]; + (Loaded as Styles)![2] = _fluentLight[1]; + } + + } } From 7fad72cca614198b8e294292236f12cffe3b0b3d Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 22 Dec 2021 19:32:40 +0200 Subject: [PATCH 17/86] fix --- src/Avalonia.Input/TouchDevice.cs | 2 +- .../TouchDeviceTests.cs | 348 ++++++++---------- 2 files changed, 163 insertions(+), 187 deletions(-) diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 0f832d9add..af8851a82b 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -61,7 +61,7 @@ namespace Avalonia.Input var settings = AvaloniaLocator.Current.GetService(); if (settings == null) { - throw new Exception("IPlatformSettings can not be null"); + throw new Exception("IPlatformSettings can not be null."); } if (!_lastClickRect.Contains(args.Position) || ev.Timestamp - _lastClickTime > settings.DoubleClickTime.TotalMilliseconds) diff --git a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs index 6c4416be47..4700ead548 100644 --- a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Disposables; using Avalonia.Input.Raw; using Avalonia.Platform; using Avalonia.UnitTests; @@ -12,56 +13,47 @@ namespace Avalonia.Input.UnitTests [Fact] public void Tapped_Event_Is_Fired_With_Touch() { - using (UnitTestApplication.Start( - new TestServices(inputManager: new InputManager()))) + using var app = UnitTestApp(new TimeSpan(200)); + var root = new TestRoot(); + var touchDevice = new TouchDevice(); + + var isTapped = false; + var executedTimes = 0; + root.Tapped += (a, e) => { - var root = new TestRoot(); - var touchDevice = new TouchDevice(); + isTapped = true; + executedTimes++; + }; + TapOnce(InputManager.Instance, touchDevice, root); + Assert.True(isTapped); + Assert.Equal(1, executedTimes); - var isTapped = false; - var executedTimes = 0; - root.Tapped += (a, e) => - { - isTapped = true; - executedTimes++; - }; - TapOnce(InputManager.Instance, touchDevice, root); - Assert.True(isTapped); - Assert.Equal(1, executedTimes); - } } [Fact] public void DoubleTapped_Event_Is_Fired_With_Touch() { - var platformSettingsMock = new Mock(); - platformSettingsMock.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200)); - AvaloniaLocator.CurrentMutable.BindToSelf(this) - .Bind().ToConstant(platformSettingsMock.Object); - using (UnitTestApplication.Start( - new TestServices(inputManager: new InputManager()))) + using var app = UnitTestApp(new TimeSpan(200)); + var root = new TestRoot(); + var touchDevice = new TouchDevice(); + + var isDoubleTapped = false; + var doubleTappedExecutedTimes = 0; + var tappedExecutedTimes = 0; + root.DoubleTapped += (a, e) => { - var root = new TestRoot(); - var touchDevice = new TouchDevice(); - - var isDoubleTapped = false; - var doubleTappedExecutedTimes = 0; - var tappedExecutedTimes = 0; - root.DoubleTapped += (a, e) => - { - isDoubleTapped = true; - doubleTappedExecutedTimes++; - }; - root.Tapped += (a, e) => - { - tappedExecutedTimes++; - }; - TapOnce(InputManager.Instance, touchDevice, root); - TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 1); - Assert.Equal(1, tappedExecutedTimes); - Assert.True(isDoubleTapped); - Assert.Equal(1, doubleTappedExecutedTimes); - } + isDoubleTapped = true; + doubleTappedExecutedTimes++; + }; + root.Tapped += (a, e) => + { + tappedExecutedTimes++; + }; + TapOnce(InputManager.Instance, touchDevice, root); + TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 1); + Assert.Equal(1, tappedExecutedTimes); + Assert.True(isDoubleTapped); + Assert.Equal(1, doubleTappedExecutedTimes); } [Theory] @@ -72,172 +64,156 @@ namespace Avalonia.Input.UnitTests [InlineData(5)] public void PointerPressed_Counts_Clicks_Correctly(int clickCount) { - var platformSettingsMock = new Mock(); - platformSettingsMock.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200)); - AvaloniaLocator.CurrentMutable.BindToSelf(this) - .Bind().ToConstant(platformSettingsMock.Object); - using (UnitTestApplication.Start( - new TestServices(inputManager: new InputManager()))) - { - var root = new TestRoot(); - var touchDevice = new TouchDevice(); + using var app = UnitTestApp(new TimeSpan(200)); + var root = new TestRoot(); + var touchDevice = new TouchDevice(); - var pointerPressedExecutedTimes = 0; - var pointerPressedClicks = 0; - root.PointerPressed += (a, e) => - { - pointerPressedClicks = e.ClickCount; - pointerPressedExecutedTimes++; - }; - for (int i = 0; i < clickCount; i++) - { - TapOnce(InputManager.Instance, touchDevice, root, touchPointId: i); - } - - Assert.Equal(clickCount, pointerPressedExecutedTimes); - Assert.Equal(pointerPressedClicks, clickCount); + var pointerPressedExecutedTimes = 0; + var pointerPressedClicks = 0; + root.PointerPressed += (a, e) => + { + pointerPressedClicks = e.ClickCount; + pointerPressedExecutedTimes++; + }; + for (int i = 0; i < clickCount; i++) + { + TapOnce(InputManager.Instance, touchDevice, root, touchPointId: i); } + + Assert.Equal(clickCount, pointerPressedExecutedTimes); + Assert.Equal(pointerPressedClicks, clickCount); } [Fact] public void DoubleTapped_Not_Fired_When_Click_Too_Late() { - var platformSettingsMock = new Mock(); - platformSettingsMock.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(0, 0, 0, 0, 20)); - AvaloniaLocator.CurrentMutable.BindToSelf(this) - .Bind().ToConstant(platformSettingsMock.Object); - using (UnitTestApplication.Start( - new TestServices(inputManager: new InputManager()))) + using var app = UnitTestApp(new TimeSpan(0, 0, 0, 0, 20)); + var root = new TestRoot(); + var touchDevice = new TouchDevice(); + + var isDoubleTapped = false; + var doubleTappedExecutedTimes = 0; + var tappedExecutedTimes = 0; + root.DoubleTapped += (a, e) => + { + isDoubleTapped = true; + doubleTappedExecutedTimes++; + }; + root.Tapped += (a, e) => { - var root = new TestRoot(); - var touchDevice = new TouchDevice(); + tappedExecutedTimes++; + }; + TapOnce(InputManager.Instance, touchDevice, root); + TapOnce(InputManager.Instance, touchDevice, root, 21, 1); + Assert.Equal(2, tappedExecutedTimes); + Assert.False(isDoubleTapped); + Assert.Equal(0, doubleTappedExecutedTimes); - var isDoubleTapped = false; - var doubleTappedExecutedTimes = 0; - var tappedExecutedTimes = 0; - root.DoubleTapped += (a, e) => - { - isDoubleTapped = true; - doubleTappedExecutedTimes++; - }; - root.Tapped += (a, e) => - { - tappedExecutedTimes++; - }; - TapOnce(InputManager.Instance, touchDevice, root); - TapOnce(InputManager.Instance, touchDevice, root, 21, 1); - Assert.Equal(2, tappedExecutedTimes); - Assert.False(isDoubleTapped); - Assert.Equal(0, doubleTappedExecutedTimes); - } } [Fact] public void DoubleTapped_Not_Fired_When_Second_Click_Is_From_Different_Touch_Contact() { - var tmp = new Mock(); - tmp.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200)); - AvaloniaLocator.CurrentMutable.BindToSelf(this) - .Bind().ToConstant(tmp.Object); - using (UnitTestApplication.Start( - new TestServices(inputManager: new InputManager()))) + using var app = UnitTestApp(new TimeSpan(200)); + var root = new TestRoot(); + var touchDevice = new TouchDevice(); + + var isDoubleTapped = false; + var doubleTappedExecutedTimes = 0; + var tappedExecutedTimes = 0; + root.DoubleTapped += (a, e) => { - var root = new TestRoot(); - var touchDevice = new TouchDevice(); - - var isDoubleTapped = false; - var doubleTappedExecutedTimes = 0; - var tappedExecutedTimes = 0; - root.DoubleTapped += (a, e) => - { - isDoubleTapped = true; - doubleTappedExecutedTimes++; - }; - root.Tapped += (a, e) => - { - tappedExecutedTimes++; - }; - SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchBegin, 0, 1); - SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchEnd, 0, 1); - Assert.Equal(2, tappedExecutedTimes); - Assert.False(isDoubleTapped); - Assert.Equal(0, doubleTappedExecutedTimes); - } + isDoubleTapped = true; + doubleTappedExecutedTimes++; + }; + root.Tapped += (a, e) => + { + tappedExecutedTimes++; + }; + SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchBegin, 0, 1); + SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchEnd, 0, 1); + Assert.Equal(2, tappedExecutedTimes); + Assert.False(isDoubleTapped); + Assert.Equal(0, doubleTappedExecutedTimes); } [Fact] public void Click_Counting_Should_Work_Correctly_With_Few_Touch_Contacts() { - var tmp = new Mock(); - tmp.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200)); - AvaloniaLocator.CurrentMutable.BindToSelf(this) - .Bind().ToConstant(tmp.Object); - using (UnitTestApplication.Start( - new TestServices(inputManager: new InputManager()))) - { - var root = new TestRoot(); - var touchDevice = new TouchDevice(); + using var app = UnitTestApp(new TimeSpan(200)); - var pointerPressedExecutedTimes = 0; - var tappedExecutedTimes = 0; - var isDoubleTapped = false; - var doubleTappedExecutedTimes = 0; - root.PointerPressed += (a, e) => - { - pointerPressedExecutedTimes++; - switch (pointerPressedExecutedTimes) - { - case <= 2: - Assert.True(e.ClickCount == 1); - break; - case 3: - Assert.True(e.ClickCount == 2); - break; - case 4: - Assert.True(e.ClickCount == 3); - break; - case 5: - Assert.True(e.ClickCount == 4); - break; - case 6: - Assert.True(e.ClickCount == 5); - break; - case 7: - Assert.True(e.ClickCount == 1); - break; - case 8: - Assert.True(e.ClickCount == 1); - break; - case 9: - Assert.True(e.ClickCount == 2); - break; - default: - break; - } - }; - root.DoubleTapped += (a, e) => - { - isDoubleTapped = true; - doubleTappedExecutedTimes++; - }; - root.Tapped += (a, e) => - { - tappedExecutedTimes++; - }; - SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchBegin, 0, 1); - SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchEnd, 0, 1); - TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 2); - TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 3); - TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 4); - SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchBegin, 5, 6, 7); - SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchEnd, 5, 6, 7); - TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 8); - Assert.Equal(6, tappedExecutedTimes); - Assert.Equal(9, pointerPressedExecutedTimes); - Assert.True(isDoubleTapped); - Assert.Equal(3, doubleTappedExecutedTimes); + var root = new TestRoot(); + var touchDevice = new TouchDevice(); - } + var pointerPressedExecutedTimes = 0; + var tappedExecutedTimes = 0; + var isDoubleTapped = false; + var doubleTappedExecutedTimes = 0; + root.PointerPressed += (a, e) => + { + pointerPressedExecutedTimes++; + switch (pointerPressedExecutedTimes) + { + case <= 2: + Assert.True(e.ClickCount == 1); + break; + case 3: + Assert.True(e.ClickCount == 2); + break; + case 4: + Assert.True(e.ClickCount == 3); + break; + case 5: + Assert.True(e.ClickCount == 4); + break; + case 6: + Assert.True(e.ClickCount == 5); + break; + case 7: + Assert.True(e.ClickCount == 1); + break; + case 8: + Assert.True(e.ClickCount == 1); + break; + case 9: + Assert.True(e.ClickCount == 2); + break; + default: + break; + } + }; + root.DoubleTapped += (a, e) => + { + isDoubleTapped = true; + doubleTappedExecutedTimes++; + }; + root.Tapped += (a, e) => + { + tappedExecutedTimes++; + }; + SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchBegin, 0, 1); + SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchEnd, 0, 1); + TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 2); + TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 3); + TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 4); + SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchBegin, 5, 6, 7); + SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchEnd, 5, 6, 7); + TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 8); + Assert.Equal(6, tappedExecutedTimes); + Assert.Equal(9, pointerPressedExecutedTimes); + Assert.True(isDoubleTapped); + Assert.Equal(3, doubleTappedExecutedTimes); + } + private IDisposable UnitTestApp(TimeSpan doubleClickTime = new TimeSpan()) + { + var unitTestApp = UnitTestApplication.Start( + new TestServices(inputManager: new InputManager())); + var scope = AvaloniaLocator.EnterScope(); + var iSettingsMock = new Mock(); + iSettingsMock.Setup(x => x.DoubleClickTime).Returns(doubleClickTime); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().ToConstant(iSettingsMock.Object); + return new CompositeDisposable(unitTestApp, scope); } private static void SendXTouchContactsWithIds(IInputManager inputManager, TouchDevice device, IInputRoot root, RawPointerEventType type, params long[] touchPointIds) { From c2145db99bad500028cd87411745874c946fc774 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 22 Dec 2021 22:41:52 +0200 Subject: [PATCH 18/86] more fixes --- tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs index 4700ead548..dc9eb9d3b4 100644 --- a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive.Disposables; using Avalonia.Input.Raw; using Avalonia.Platform; using Avalonia.UnitTests; @@ -208,12 +207,11 @@ namespace Avalonia.Input.UnitTests { var unitTestApp = UnitTestApplication.Start( new TestServices(inputManager: new InputManager())); - var scope = AvaloniaLocator.EnterScope(); var iSettingsMock = new Mock(); iSettingsMock.Setup(x => x.DoubleClickTime).Returns(doubleClickTime); AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(iSettingsMock.Object); - return new CompositeDisposable(unitTestApp, scope); + return unitTestApp; } private static void SendXTouchContactsWithIds(IInputManager inputManager, TouchDevice device, IInputRoot root, RawPointerEventType type, params long[] touchPointIds) { From 5992d89b7d528e4b0b2cea543510fb94d781bf5b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 22 Dec 2021 17:11:28 -0600 Subject: [PATCH 19/86] Add a DataType property on CompiledBindingExtension to enable specifying the start type explicitly on a binding. Fixes #6158 --- .../Transformers/AvaloniaXamlIlBindingPathTransformer.cs | 9 ++++++++- .../MarkupExtensions/CompiledBindingExtension.cs | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs index 4d21a29eb9..048a6220c5 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using XamlX.Ast; using XamlX.Transform; +using XamlX.Transform.Transformers; using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers @@ -15,7 +16,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { IXamlType startType = null; var sourceProperty = binding.Children.OfType().FirstOrDefault(c => c.Property.Name == "Source"); - if ((sourceProperty?.Values.Count ?? 0) == 1) + var dataTypeProperty = binding.Children.OfType().FirstOrDefault(c => c.Property.Name == "DataType"); + if (sourceProperty?.Values.Count is 1) { var sourceValue = sourceProperty.Values[0]; switch (sourceValue) @@ -99,6 +101,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } + if (dataTypeProperty?.Values.Count is 1 && dataTypeProperty.Values[0] is XamlAstTextNode text) + { + startType = TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type; + } + Func startTypeResolver = startType is not null ? () => startType : () => { var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 41de2355aa..0125156750 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -71,5 +71,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public CompiledBindingPath Path { get; set; } public object Source { get; set; } + + public Type DataType { get; set; } } } From 46ffcf9eee6bddeb2710803b85e4c25ad34b81a5 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 22 Dec 2021 17:19:34 -0600 Subject: [PATCH 20/86] Support the DataType property in a binding in the DataContext property. --- .../AvaloniaXamlIlDataContextTypeTransformer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index 24dd3f7771..cf691db860 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -155,6 +155,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { Func startTypeResolver = () => { + var dataTypeProperty = obj.Children.OfType().FirstOrDefault(c => c.Property.Name == "DataType"); + if (dataTypeProperty?.Values.Count is 1 && dataTypeProperty.Values[0] is XamlAstTextNode text) + { + return TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type; + } + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); if (parentDataContextNode is null) { From 240050dd8a7395bdab3c309d00b86fd3f0689ea6 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Dec 2021 10:42:05 +0300 Subject: [PATCH 21/86] Stopgap measure to fix #7245 --- src/Avalonia.X11/X11Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 982bca6439..211a8e4f79 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1157,7 +1157,7 @@ namespace Avalonia.X11 public ITextInputMethodImpl TextInputMethod => _ime; public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => - _transparencyHelper.SetTransparencyRequest(transparencyLevel); + _transparencyHelper?.SetTransparencyRequest(transparencyLevel); public void SetWindowManagerAddShadowHint(bool enabled) { From 6ef87ff047f6fddc7afdc2bd2ff073f4141cb313 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 23 Dec 2021 10:23:26 +0100 Subject: [PATCH 22/86] Improve ButtonSpinner OnPointerWheel - Only handle the PointerWheelEvent if the control has the focus - When the pointer wheel should change the selection, mark the event as handled. So any surrounding ScrollViewer does not do the scroll. - Make the Control Focusable by default in Style --- src/Avalonia.Controls/ButtonSpinner.cs | 7 ++++--- src/Avalonia.Themes.Default/ButtonSpinner.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 5fe2cf3704..5a967ab488 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -1,4 +1,4 @@ -using System; +using System; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -196,13 +196,14 @@ namespace Avalonia.Controls protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { base.OnPointerWheelChanged(e); - if (!e.Handled && AllowSpin) + + if (AllowSpin && IsFocused) { if (e.Delta.Y != 0) { var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true); OnSpin(spinnerEventArgs); - e.Handled = spinnerEventArgs.Handled; + e.Handled = true; } } } diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml index ce2b85d2b5..561ca8bedd 100644 --- a/src/Avalonia.Themes.Default/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml @@ -5,6 +5,7 @@ + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 085de2ae7a..b21b9d80f0 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -16,12 +16,12 @@ namespace ControlCatalog DataContext = new ApplicationViewModel(); } - private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") }; - private static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + public static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") }; @@ -31,8 +31,7 @@ namespace ControlCatalog new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml") - }, - DataGridFluent + } }; public static Styles FluentLight = new Styles @@ -40,8 +39,7 @@ namespace ControlCatalog new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml") - }, - DataGridFluent + } }; public static Styles DefaultLight = new Styles @@ -65,8 +63,7 @@ namespace ControlCatalog new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") - }, - DataGridDefault + } }; public static Styles DefaultDark = new Styles @@ -90,8 +87,7 @@ namespace ControlCatalog new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") - }, - DataGridDefault + } }; public override void Initialize() diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 3898e9da85..b36c2b6997 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -3,15 +3,12 @@ using System.Collections; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using Avalonia.Markup.Xaml.MarkupExtensions; -using Avalonia.Markup.Xaml.Styling; -using Avalonia.Markup.Xaml.XamlIl; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using ControlCatalog.Pages; -using ControlCatalog.Models; using Avalonia.Themes.Fluent; +using ControlCatalog.Models; +using ControlCatalog.Pages; namespace ControlCatalog { @@ -44,7 +41,47 @@ namespace ControlCatalog { if (themes.SelectedItem is CatalogTheme theme) { - (Application.Current.Styles[0] as FluentTheme).Mode = FluentThemeMode.Dark; + var themeStyle = Application.Current.Styles[0]; + if (theme == CatalogTheme.FluentLight) + { + if (themeStyle is FluentTheme fluentTheme) + { + if (fluentTheme.Mode == FluentThemeMode.Dark) + { + fluentTheme.Mode = FluentThemeMode.Light; + } + } + else + { + Application.Current.Styles[0] = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); + Application.Current.Styles[1] = App.DataGridFluent; + } + } + else if (theme == CatalogTheme.FluentDark) + { + if (themeStyle is FluentTheme fluentTheme) + { + if (fluentTheme.Mode == FluentThemeMode.Light) + { + fluentTheme.Mode = FluentThemeMode.Dark; + } + } + else + { + Application.Current.Styles[0] = new FluentTheme(new Uri("avares://ControlCatalog/Styles")) { Mode = FluentThemeMode.Dark }; + Application.Current.Styles[1] = App.DataGridFluent; + } + } + else if (theme == CatalogTheme.DefaultLight) + { + Application.Current.Styles[0] = App.DefaultLight; + Application.Current.Styles[1] = App.DataGridDefault; + } + else if (theme == CatalogTheme.DefaultDark) + { + Application.Current.Styles[0] = App.DefaultDark; + Application.Current.Styles[1] = App.DataGridDefault; + } } }; diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 1c61c03c97..5790de42b1 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -18,7 +18,7 @@ namespace Avalonia.Themes.Fluent /// /// Includes the fluent theme in an application. /// - public class FluentTheme : IStyle, IResourceProvider + public class FluentTheme : AvaloniaObject, IStyle, IResourceProvider { private readonly Uri _baseUri; private Styles _fluentDark = new(); @@ -26,7 +26,6 @@ namespace Avalonia.Themes.Fluent private Styles _sharedStyles = new(); private bool _isLoading; private IStyle? _loaded; - private FluentThemeMode _mode; /// /// Initializes a new instance of the class. @@ -48,31 +47,32 @@ namespace Avalonia.Themes.Fluent InitStyles(_baseUri); } + + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode)); /// /// Gets or sets the mode of the fluent theme (light, dark). /// public FluentThemeMode Mode { - get => _mode; - set + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ModeProperty) { - if (_mode != value) + if (Mode == FluentThemeMode.Dark) { - _mode = value; - if (_mode == FluentThemeMode.Dark) - { - (Loaded as Styles)![1] = _fluentDark[0]; - (Loaded as Styles)![2] = _fluentDark[1]; - } - else - { - (Loaded as Styles)![1] = _fluentLight[0]; - (Loaded as Styles)![2] = _fluentLight[1]; - } - - + (Loaded as Styles)![1] = _fluentDark[0]; + (Loaded as Styles)![2] = _fluentDark[1]; + } + else + { + (Loaded as Styles)![1] = _fluentLight[0]; + (Loaded as Styles)![2] = _fluentLight[1]; } - } } From 5172771a79a032e9df38c25fa3636d042c6e57b2 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 23 Dec 2021 15:53:14 +0200 Subject: [PATCH 26/86] refactoring --- src/Avalonia.Themes.Fluent/FluentTheme.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 5790de42b1..53be41e4d1 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -88,23 +88,19 @@ namespace Avalonia.Themes.Fluent if (_loaded == null) { _isLoading = true; - Styles? resultStyle = new Styles() { _sharedStyles }; if (Mode == FluentThemeMode.Light) { - resultStyle.Add(_fluentLight[0]); - resultStyle.Add(_fluentLight[1]); + _loaded = new Styles() { _sharedStyles , _fluentLight[0], _fluentLight[1] }; } else if (Mode == FluentThemeMode.Dark) { - resultStyle.Add(_fluentDark[0]); - resultStyle.Add(_fluentDark[1]); + _loaded = new Styles() { _sharedStyles, _fluentDark[0], _fluentDark[1] }; } - _loaded = resultStyle; _isLoading = false; } - return _loaded; + return _loaded!; } } From c6264191137fff72ca9ffd1f2a1b523d4ef79a7c Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 23 Dec 2021 16:00:43 +0200 Subject: [PATCH 27/86] more refactorings --- samples/ControlCatalog/App.xaml | 2 -- samples/ControlCatalog/App.xaml.cs | 20 +++++--------------- samples/ControlCatalog/MainView.xaml.cs | 4 ++-- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index ab3aa5a4ae..227b31bf20 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -5,8 +5,6 @@ x:CompileBindings="True" x:Class="ControlCatalog.App"> - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index b21b9d80f0..4e86c60285 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; +using Avalonia.Themes.Fluent; using ControlCatalog.ViewModels; namespace ControlCatalog @@ -26,21 +27,9 @@ namespace ControlCatalog Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") }; - public static Styles FluentDark = new Styles - { - new StyleInclude(new Uri("avares://ControlCatalog/Styles")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml") - } - }; + public static FluentTheme FluentDark = new FluentTheme(new Uri("avares://ControlCatalog/Styles")) { Mode = FluentThemeMode.Dark }; - public static Styles FluentLight = new Styles - { - new StyleInclude(new Uri("avares://ControlCatalog/Styles")) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml") - } - }; + public static FluentTheme FluentLight = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); public static Styles DefaultLight = new Styles { @@ -92,7 +81,8 @@ namespace ControlCatalog public override void Initialize() { - + Styles.Insert(0, FluentLight); + Styles.Insert(1, DataGridFluent); AvaloniaXamlLoader.Load(this); } diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index b36c2b6997..696062db3f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -53,7 +53,7 @@ namespace ControlCatalog } else { - Application.Current.Styles[0] = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); + Application.Current.Styles[0] = App.FluentLight; Application.Current.Styles[1] = App.DataGridFluent; } } @@ -68,7 +68,7 @@ namespace ControlCatalog } else { - Application.Current.Styles[0] = new FluentTheme(new Uri("avares://ControlCatalog/Styles")) { Mode = FluentThemeMode.Dark }; + Application.Current.Styles[0] = App.FluentDark; Application.Current.Styles[1] = App.DataGridFluent; } } From 4fbb93a03cac66ceb63d9426be1f2e84663827ef Mon Sep 17 00:00:00 2001 From: Giacomo Gozzi Date: Thu, 23 Dec 2021 17:49:00 +0300 Subject: [PATCH 28/86] Fix #7227 --- .../GestureRecognizers/ScrollGestureRecognizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index 7532676f18..f8cedb636f 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -71,7 +71,7 @@ namespace Avalonia.Input.GestureRecognizers EndGesture(); _tracking = e.Pointer; _gestureId = ScrollGestureEventArgs.GetNextFreeId();; - _trackedRootPoint = e.GetPosition(null); + _trackedRootPoint = e.GetPosition(_target); } } From 7daccf91ae96422fba7208428b1df8e61424a20a Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 23 Dec 2021 17:37:11 +0200 Subject: [PATCH 29/86] cleanup --- samples/ControlCatalog/App.xaml.cs | 6 ++---- samples/ControlCatalog/MainView.xaml.cs | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 4e86c60285..aba22a4af6 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -27,9 +27,7 @@ namespace ControlCatalog Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml") }; - public static FluentTheme FluentDark = new FluentTheme(new Uri("avares://ControlCatalog/Styles")) { Mode = FluentThemeMode.Dark }; - - public static FluentTheme FluentLight = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); + public static FluentTheme Fluent = new FluentTheme(new Uri("avares://ControlCatalog/Styles")); public static Styles DefaultLight = new Styles { @@ -81,7 +79,7 @@ namespace ControlCatalog public override void Initialize() { - Styles.Insert(0, FluentLight); + Styles.Insert(0, Fluent); Styles.Insert(1, DataGridFluent); AvaloniaXamlLoader.Load(this); } diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 696062db3f..d36ec3d272 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -53,7 +53,7 @@ namespace ControlCatalog } else { - Application.Current.Styles[0] = App.FluentLight; + Application.Current.Styles[0] = App.Fluent; Application.Current.Styles[1] = App.DataGridFluent; } } @@ -68,7 +68,8 @@ namespace ControlCatalog } else { - Application.Current.Styles[0] = App.FluentDark; + App.Fluent.Mode = FluentThemeMode.Dark; + Application.Current.Styles[0] = App.Fluent; Application.Current.Styles[1] = App.DataGridFluent; } } From 3bfafc5f9397893ca247b149091a1ba02167fd6b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 23 Dec 2021 17:56:30 +0200 Subject: [PATCH 30/86] more cleanup --- samples/ControlCatalog/MainView.xaml.cs | 28 +++++++------------------ 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index d36ec3d272..0579355831 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -44,34 +44,22 @@ namespace ControlCatalog var themeStyle = Application.Current.Styles[0]; if (theme == CatalogTheme.FluentLight) { - if (themeStyle is FluentTheme fluentTheme) + if (App.Fluent.Mode != FluentThemeMode.Light) { - if (fluentTheme.Mode == FluentThemeMode.Dark) - { - fluentTheme.Mode = FluentThemeMode.Light; - } - } - else - { - Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.DataGridFluent; + App.Fluent.Mode = FluentThemeMode.Light; } + Application.Current.Styles[0] = App.Fluent; + Application.Current.Styles[1] = App.DataGridFluent; } else if (theme == CatalogTheme.FluentDark) { - if (themeStyle is FluentTheme fluentTheme) - { - if (fluentTheme.Mode == FluentThemeMode.Light) - { - fluentTheme.Mode = FluentThemeMode.Dark; - } - } - else + + if (App.Fluent.Mode != FluentThemeMode.Dark) { App.Fluent.Mode = FluentThemeMode.Dark; - Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.DataGridFluent; } + Application.Current.Styles[0] = App.Fluent; + Application.Current.Styles[1] = App.DataGridFluent; } else if (theme == CatalogTheme.DefaultLight) { From 1ed495afe2d01b40a512506532bafe86c5b6f903 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Dec 2021 20:39:35 +0300 Subject: [PATCH 31/86] Use microcom generator from nuget --- Avalonia.sln | 27 - build/MicroCom.targets | 35 -- nukebuild/MicroComGen.cs | 8 +- nukebuild/_build.csproj | 5 +- src/Avalonia.MicroCom/MicroComRuntime.cs | 6 + src/Avalonia.MicroCom/MicroComVtblBase.cs | 5 + src/Avalonia.Native/Avalonia.Native.csproj | 5 +- src/Directory.Build.props | 6 + src/Shared/ModuleInitializer.cs | 10 + .../Avalonia.Win32/Avalonia.Win32.csproj | 6 +- src/tools/MicroComGenerator/Ast.cs | 241 --------- src/tools/MicroComGenerator/AstParser.cs | 232 --------- .../CSharpGen.InterfaceGen.cs | 484 ------------------ .../MicroComGenerator/CSharpGen.Utils.cs | 111 ---- src/tools/MicroComGenerator/CSharpGen.cs | 155 ------ src/tools/MicroComGenerator/CppGen.cs | 119 ----- src/tools/MicroComGenerator/Extensions.cs | 97 ---- .../MicroComGenerator.csproj | 10 - src/tools/MicroComGenerator/ParseException.cs | 27 - src/tools/MicroComGenerator/Program.cs | 52 -- src/tools/MicroComGenerator/TokenParser.cs | 417 --------------- 21 files changed, 38 insertions(+), 2020 deletions(-) delete mode 100644 build/MicroCom.targets create mode 100644 src/Shared/ModuleInitializer.cs delete mode 100644 src/tools/MicroComGenerator/Ast.cs delete mode 100644 src/tools/MicroComGenerator/AstParser.cs delete mode 100644 src/tools/MicroComGenerator/CSharpGen.InterfaceGen.cs delete mode 100644 src/tools/MicroComGenerator/CSharpGen.Utils.cs delete mode 100644 src/tools/MicroComGenerator/CSharpGen.cs delete mode 100644 src/tools/MicroComGenerator/CppGen.cs delete mode 100644 src/tools/MicroComGenerator/Extensions.cs delete mode 100644 src/tools/MicroComGenerator/MicroComGenerator.csproj delete mode 100644 src/tools/MicroComGenerator/ParseException.cs delete mode 100644 src/tools/MicroComGenerator/Program.cs delete mode 100644 src/tools/MicroComGenerator/TokenParser.cs diff --git a/Avalonia.sln b/Avalonia.sln index 3c2fc7437b..4e7b4cc318 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -221,8 +221,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}" @@ -2027,30 +2025,6 @@ Global {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhone.Build.0 = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhone.Build.0 = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|Any CPU.Build.0 = Release|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhone.ActiveCfg = Release|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhone.Build.0 = Release|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -2253,7 +2227,6 @@ Global {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098} {25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} {C08E9894-AA92-426E-BF56-033E262CAD3E} = {9B9E3891-2366-4253-A952-D08BCEB71098} diff --git a/build/MicroCom.targets b/build/MicroCom.targets deleted file mode 100644 index 029d7f95f5..0000000000 --- a/build/MicroCom.targets +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - false - all - true - TargetFramework=net6.0 - - - - - - - - - - - - - - - - - - <_AvaloniaPatchComInterop>true - - - diff --git a/nukebuild/MicroComGen.cs b/nukebuild/MicroComGen.cs index 06c8acbf23..b1e546cb97 100644 --- a/nukebuild/MicroComGen.cs +++ b/nukebuild/MicroComGen.cs @@ -1,14 +1,14 @@ using System.IO; -using MicroComGenerator; +using MicroCom.CodeGenerator; using Nuke.Common; partial class Build : NukeBuild { Target GenerateCppHeaders => _ => _.Executes(() => { - var text = File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl"); - var ast = AstParser.Parse(text); + var file = MicroComCodeGenerator.Parse( + File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl")); File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h", - CppGen.GenerateCpp(ast)); + file.GenerateCppHeader()); }); } \ No newline at end of file diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index b28d3eb700..acb001f2e7 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -16,6 +16,7 @@ + @@ -37,10 +38,6 @@ - - MicroComGenerator\%(Filename)%(Extension) - - diff --git a/src/Avalonia.MicroCom/MicroComRuntime.cs b/src/Avalonia.MicroCom/MicroComRuntime.cs index 85507674d2..e0f524146a 100644 --- a/src/Avalonia.MicroCom/MicroComRuntime.cs +++ b/src/Avalonia.MicroCom/MicroComRuntime.cs @@ -36,7 +36,13 @@ namespace Avalonia.MicroCom public static T CreateProxyFor(void* pObject, bool ownsHandle) => (T)CreateProxyFor(typeof(T), new IntPtr(pObject), ownsHandle); public static T CreateProxyFor(IntPtr pObject, bool ownsHandle) => (T)CreateProxyFor(typeof(T), pObject, ownsHandle); + + public static T CreateProxyOrNullFor(void* pObject, bool ownsHandle) where T : class => + pObject == null ? null : (T)CreateProxyFor(typeof(T), new IntPtr(pObject), ownsHandle); + public static T CreateProxyOrNullFor(IntPtr pObject, bool ownsHandle) where T : class => + pObject == IntPtr.Zero ? null : (T)CreateProxyFor(typeof(T), pObject, ownsHandle); + public static object CreateProxyFor(Type type, IntPtr pObject, bool ownsHandle) => _factories[type](pObject, ownsHandle); public static IntPtr GetNativeIntPtr(this T obj, bool owned = false) where T : IUnknown diff --git a/src/Avalonia.MicroCom/MicroComVtblBase.cs b/src/Avalonia.MicroCom/MicroComVtblBase.cs index 2f0607c0a8..7092f8131a 100644 --- a/src/Avalonia.MicroCom/MicroComVtblBase.cs +++ b/src/Avalonia.MicroCom/MicroComVtblBase.cs @@ -21,6 +21,11 @@ namespace Avalonia.MicroCom AddMethod((AddRefDelegate)Release); } + protected void AddMethod(void* f) + { + _methods.Add(new IntPtr(f)); + } + protected void AddMethod(Delegate d) { GCHandle.Alloc(d); diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index c37bef0488..366ce97955 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -6,6 +6,7 @@ true netstandard2.0;net6.0 true + Avalonia.MicroCom @@ -20,7 +21,7 @@ - + + - diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a3f0c01a51..869602e452 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,4 +2,10 @@ + + + Shared\_ModuleInitializer.cs + false + + diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs new file mode 100644 index 0000000000..c14b150c1a --- /dev/null +++ b/src/Shared/ModuleInitializer.cs @@ -0,0 +1,10 @@ +namespace System.Runtime.CompilerServices +{ +#if !NET5_0_OR_GREATER + internal class ModuleInitializerAttribute : Attribute + { + + } +#endif +} + diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index c8f08a5799..a0290ee5e8 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -8,9 +8,9 @@ - - + + + - diff --git a/src/tools/MicroComGenerator/Ast.cs b/src/tools/MicroComGenerator/Ast.cs deleted file mode 100644 index e9a55308be..0000000000 --- a/src/tools/MicroComGenerator/Ast.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace MicroComGenerator.Ast -{ - public class AstAttributeNode - { - public string Name { get; set; } - public string Value { get; set; } - - public AstAttributeNode(string name, string value) - { - Name = name; - Value = value; - } - - public override string ToString() => $"{Name} = {Value}"; - public AstAttributeNode Clone() => new AstAttributeNode(Name, Value); - } - - public class AstAttributes : List - { - public bool HasAttribute(string a) => this.Any(x => x.Name == a); - - public AstAttributes Clone() - { - var rv= new AstAttributes(); - rv.AddRange(this.Select(x => x.Clone())); - return rv; - } - } - - public interface IAstNodeWithAttributes - { - public AstAttributes Attributes { get; set; } - } - - public class AstEnumNode : List, IAstNodeWithAttributes - { - public AstAttributes Attributes { get; set; } = new AstAttributes(); - public string Name { get; set; } - public override string ToString() => "Enum " + Name; - - public AstEnumNode Clone() - { - var rv = new AstEnumNode { Name = Name, Attributes = Attributes.Clone() }; - rv.AddRange(this.Select(x => x.Clone())); - return rv; - } - } - - public class AstEnumMemberNode - { - public string Name { get; set; } - public string Value { get; set; } - - public AstEnumMemberNode(string name, string value) - { - Name = name; - Value = value; - } - - public override string ToString() => $"Enum member {Name} = {Value}"; - public AstEnumMemberNode Clone() => new AstEnumMemberNode(Name, Value); - } - - public class AstStructNode : List, IAstNodeWithAttributes - { - public AstAttributes Attributes { get; set; } = new AstAttributes(); - public string Name { get; set; } - public override string ToString() => "Struct " + Name; - - public AstStructNode Clone() - { - var rv = new AstStructNode { Name = Name, Attributes = Attributes.Clone() }; - rv.AddRange(this.Select(x => x.Clone())); - return rv; - } - } - - public class AstTypeNode - { - public string Name { get; set; } - public int PointerLevel { get; set; } - public bool IsLink { get; set; } - - public string Format() => Name + new string('*', PointerLevel) - + (IsLink ? "&" : ""); - public override string ToString() => Format(); - public AstTypeNode Clone() => new AstTypeNode() { - Name = Name, - PointerLevel = PointerLevel, - IsLink = IsLink - }; - } - - public class AstStructMemberNode : IAstNodeWithAttributes - { - public string Name { get; set; } - public AstTypeNode Type { get; set; } - - public override string ToString() => $"Struct member {Type.Format()} {Name}"; - public AstStructMemberNode Clone() => new AstStructMemberNode() { Name = Name, Type = Type.Clone() }; - public AstAttributes Attributes { get; set; } = new AstAttributes(); - } - - public class AstInterfaceNode : List, IAstNodeWithAttributes - { - public AstAttributes Attributes { get; set; } = new AstAttributes(); - public string Name { get; set; } - public string Inherits { get; set; } - - public override string ToString() - { - if (Inherits == null) - return Name; - return $"Interface {Name} : {Inherits}"; - } - public AstInterfaceNode Clone() - { - var rv = new AstInterfaceNode { Name = Name, Inherits = Inherits, Attributes = Attributes.Clone() }; - rv.AddRange(this.Select(x => x.Clone())); - return rv; - } - } - - public class AstInterfaceMemberNode : List, IAstNodeWithAttributes - { - public string Name { get; set; } - public AstTypeNode ReturnType { get; set; } - public AstAttributes Attributes { get; set; } = new AstAttributes(); - - public AstInterfaceMemberNode Clone() - { - var rv = new AstInterfaceMemberNode() - { - Name = Name, Attributes = Attributes.Clone(), ReturnType = ReturnType - }; - rv.AddRange(this.Select(x => x.Clone())); - return rv; - } - - public override string ToString() => - $"Interface member {ReturnType.Format()} {Name} ({string.Join(", ", this.Select(x => x.Format()))})"; - } - - public class AstInterfaceMemberArgumentNode : IAstNodeWithAttributes - { - public string Name { get; set; } - public AstTypeNode Type { get; set; } - public AstAttributes Attributes { get; set; } = new AstAttributes(); - - - public string Format() => $"{Type.Format()} {Name}"; - public override string ToString() => "Argument " + Format(); - - public AstInterfaceMemberArgumentNode Clone() => new AstInterfaceMemberArgumentNode - { - Name = Name, Type = Type.Clone(), Attributes = Attributes.Clone() - }; - } - - public static class AstExtensions - { - public static bool HasAttribute(this IAstNodeWithAttributes node, string s) => node.Attributes.HasAttribute(s); - - public static string GetAttribute(this IAstNodeWithAttributes node, string s) - { - var value = node.Attributes.FirstOrDefault(a => a.Name == s)?.Value; - if (value == null) - throw new CodeGenException("Expected attribute " + s + " for node " + node); - return value; - } - - public static string GetAttributeOrDefault(this IAstNodeWithAttributes node, string s) - => node.Attributes.FirstOrDefault(a => a.Name == s)?.Value; - } - - class AstVisitor - { - protected virtual void VisitType(AstTypeNode type) - { - } - - protected virtual void VisitArgument(AstInterfaceMemberArgumentNode argument) - { - VisitType(argument.Type); - } - - protected virtual void VisitInterfaceMember(AstInterfaceMemberNode member) - { - foreach(var a in member) - VisitArgument(a); - VisitType(member.ReturnType); - } - - protected virtual void VisitInterface(AstInterfaceNode iface) - { - foreach(var m in iface) - VisitInterfaceMember(m); - } - - protected virtual void VisitStructMember(AstStructMemberNode member) - { - VisitType(member.Type); - } - - protected virtual void VisitStruct(AstStructNode node) - { - foreach(var m in node) - VisitStructMember(m); - } - - public virtual void VisitAst(AstIdlNode ast) - { - foreach(var iface in ast.Interfaces) - VisitInterface(iface); - foreach (var s in ast.Structs) - VisitStruct(s); - } - - - } - - public class AstIdlNode : IAstNodeWithAttributes - { - public AstAttributes Attributes { get; set; } = new AstAttributes(); - public List Enums { get; set; } = new List(); - public List Structs { get; set; } = new List(); - public List Interfaces { get; set; } = new List(); - - public AstIdlNode Clone() => new AstIdlNode() - { - Attributes = Attributes.Clone(), - Enums = Enums.Select(x => x.Clone()).ToList(), - Structs = Structs.Select(x => x.Clone()).ToList(), - Interfaces = Interfaces.Select(x => x.Clone()).ToList() - }; - } -} diff --git a/src/tools/MicroComGenerator/AstParser.cs b/src/tools/MicroComGenerator/AstParser.cs deleted file mode 100644 index 388a8eb018..0000000000 --- a/src/tools/MicroComGenerator/AstParser.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System.Collections.Generic; -using MicroComGenerator.Ast; - -namespace MicroComGenerator -{ - public class AstParser - { - public static AstIdlNode Parse(string source) - { - var parser = new TokenParser(source); - var idl = new AstIdlNode { Attributes = ParseGlobalAttributes(ref parser) }; - - while (!parser.Eof) - { - var attrs = ParseLocalAttributes(ref parser); - if (parser.TryConsume(";")) - continue; - if (parser.TryParseKeyword("enum")) - idl.Enums.Add(ParseEnum(attrs, ref parser)); - else if (parser.TryParseKeyword("struct")) - idl.Structs.Add(ParseStruct(attrs, ref parser)); - else if (parser.TryParseKeyword("interface")) - idl.Interfaces.Add(ParseInterface(attrs, ref parser)); - else - throw new ParseException("Unexpected character", ref parser); - } - - return idl; - } - - static AstAttributes ParseGlobalAttributes(ref TokenParser parser) - { - var rv = new AstAttributes(); - while (!parser.Eof) - { - parser.SkipWhitespace(); - if (parser.TryConsume('@')) - { - var ident = parser.ParseIdentifier("-"); - var value = parser.ReadToEol().Trim(); - if (value == "@@") - { - parser.Advance(1); - value = ""; - while (true) - { - var l = parser.ReadToEol(); - if (l == "@@") - break; - else - value = value.Length == 0 ? l : (value + "\n" + l); - parser.Advance(1); - } - - } - rv.Add(new AstAttributeNode(ident, value)); - } - else - return rv; - } - - return rv; - } - - static AstAttributes ParseLocalAttributes(ref TokenParser parser) - { - var rv = new AstAttributes(); - while (parser.TryConsume("[")) - { - while (!parser.TryConsume("]") && !parser.Eof) - { - if (parser.TryConsume(',')) - continue; - - // Get identifier - var ident = parser.ParseIdentifier("-"); - - // No value, end of attribute list - if (parser.TryConsume(']')) - { - rv.Add(new AstAttributeNode(ident, null)); - break; - } - // No value, next attribute - else if (parser.TryConsume(',')) - rv.Add(new AstAttributeNode(ident, null)); - // Has value - else if (parser.TryConsume('(')) - { - var value = parser.ReadTo(')'); - parser.Consume(')'); - rv.Add(new AstAttributeNode(ident, value)); - } - else - throw new ParseException("Unexpected character", ref parser); - } - - if (parser.Eof) - throw new ParseException("Unexpected EOF", ref parser); - } - - return rv; - } - - static void EnsureOpenBracket(ref TokenParser parser) - { - if (!parser.TryConsume('{')) - throw new ParseException("{ expected", ref parser); - } - - static AstEnumNode ParseEnum(AstAttributes attrs, ref TokenParser parser) - { - var name = parser.ParseIdentifier(); - EnsureOpenBracket(ref parser); - var rv = new AstEnumNode { Name = name, Attributes = attrs }; - while (!parser.TryConsume('}') && !parser.Eof) - { - if (parser.TryConsume(',')) - continue; - - var ident = parser.ParseIdentifier(); - - // Automatic value - if (parser.TryConsume(',') || parser.Peek == '}') - { - rv.Add(new AstEnumMemberNode(ident, null)); - continue; - } - - if (!parser.TryConsume('=')) - throw new ParseException("Unexpected character", ref parser); - - var value = parser.ReadToAny(",}").Trim(); - rv.Add(new AstEnumMemberNode(ident, value)); - - if (parser.Eof) - throw new ParseException("Unexpected EOF", ref parser); - } - - - return rv; - } - - static AstTypeNode ParseType(ref TokenParser parser) - { - var ident = parser.ParseIdentifier(); - var t = new AstTypeNode { Name = ident }; - while (parser.TryConsume('*')) - t.PointerLevel++; - if (parser.TryConsume("&")) - t.IsLink = true; - return t; - } - - static AstStructNode ParseStruct(AstAttributes attrs, ref TokenParser parser) - { - var name = parser.ParseIdentifier(); - EnsureOpenBracket(ref parser); - var rv = new AstStructNode { Name = name, Attributes = attrs }; - while (!parser.TryConsume('}') && !parser.Eof) - { - var memberAttrs = ParseLocalAttributes(ref parser); - var t = ParseType(ref parser); - bool parsedAtLeastOneMember = false; - while (!parser.TryConsume(';')) - { - // Skip any , - while (parser.TryConsume(',')) { } - - var ident = parser.ParseIdentifier(); - parsedAtLeastOneMember = true; - rv.Add(new AstStructMemberNode { Name = ident, Type = t, Attributes = memberAttrs}); - } - - if (!parsedAtLeastOneMember) - throw new ParseException("Expected at least one enum member with declared type " + t, ref parser); - } - - return rv; - } - - static AstInterfaceNode ParseInterface(AstAttributes interfaceAttrs, ref TokenParser parser) - { - var interfaceName = parser.ParseIdentifier(); - string inheritsFrom = null; - if (parser.TryConsume(":")) - inheritsFrom = parser.ParseIdentifier(); - - EnsureOpenBracket(ref parser); - var rv = new AstInterfaceNode - { - Name = interfaceName, Attributes = interfaceAttrs, Inherits = inheritsFrom - }; - while (!parser.TryConsume('}') && !parser.Eof) - { - var memberAttrs = ParseLocalAttributes(ref parser); - var returnType = ParseType(ref parser); - var name = parser.ParseIdentifier(); - var member = new AstInterfaceMemberNode - { - Name = name, ReturnType = returnType, Attributes = memberAttrs - }; - rv.Add(member); - - parser.Consume('('); - while (true) - { - if (parser.TryConsume(')')) - break; - - var argumentAttrs = ParseLocalAttributes(ref parser); - var type = ParseType(ref parser); - var argName = parser.ParseIdentifier(); - member.Add(new AstInterfaceMemberArgumentNode - { - Name = argName, Type = type, Attributes = argumentAttrs - }); - - if (parser.TryConsume(')')) - break; - if (parser.TryConsume(',')) - continue; - throw new ParseException("Unexpected character", ref parser); - } - - parser.Consume(';'); - } - - return rv; - } - } -} diff --git a/src/tools/MicroComGenerator/CSharpGen.InterfaceGen.cs b/src/tools/MicroComGenerator/CSharpGen.InterfaceGen.cs deleted file mode 100644 index adb8faf938..0000000000 --- a/src/tools/MicroComGenerator/CSharpGen.InterfaceGen.cs +++ /dev/null @@ -1,484 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using MicroComGenerator.Ast; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -// ReSharper disable CoVariantArrayConversion - -// HERE BE DRAGONS - -namespace MicroComGenerator -{ - public partial class CSharpGen - { - abstract class Arg - { - public string Name; - public string NativeType; - public AstAttributes Attributes { get; set; } - public virtual StatementSyntax CreateFixed(StatementSyntax inner) => inner; - - public virtual void PreMarshal(List body) - { - } - - public virtual void PreMarshalForReturn(List body) => - throw new InvalidOperationException("Don't know how to use " + NativeType + " as HRESULT-return"); - - public virtual ExpressionSyntax Value(bool isHresultReturn) => ParseExpression(Name); - public abstract string ManagedType { get; } - public virtual string ReturnManagedType => ManagedType; - - public virtual StatementSyntax[] ReturnMarshalResult() => new[] { ParseStatement("return " + Name + ";") }; - - - public virtual void BackPreMarshal(List body) - { - } - - public virtual ExpressionSyntax BackMarshalValue() => ParseExpression(Name); - public virtual ExpressionSyntax BackMarshalReturn(string resultVar) => ParseExpression(resultVar); - - } - - class InterfaceReturnArg : Arg - { - public string InterfaceType; - public override ExpressionSyntax Value(bool isHresultReturn) => ParseExpression("&" + PName); - public override string ManagedType => InterfaceType; - - private string PName => "__marshal_" + Name; - - public override void PreMarshalForReturn(List body) - { - body.Add(ParseStatement("void* " + PName + " = null;")); - } - - public override StatementSyntax[] ReturnMarshalResult() => new[] - { - ParseStatement("return Avalonia.MicroCom.MicroComRuntime.CreateProxyFor<" + InterfaceType + ">(" + - PName + ", true);") - }; - - public override ExpressionSyntax BackMarshalValue() - { - return ParseExpression("INVALID"); - } - - public override ExpressionSyntax BackMarshalReturn(string resultVar) - { - return ParseExpression($"Avalonia.MicroCom.MicroComRuntime.GetNativePointer({resultVar}, true)"); - } - } - - class InterfaceArg : Arg - { - public string InterfaceType; - - public override ExpressionSyntax Value(bool isHresultReturn) => - ParseExpression("Avalonia.MicroCom.MicroComRuntime.GetNativePointer(" + Name + ")"); - - public override string ManagedType => InterfaceType; - - public override StatementSyntax[] ReturnMarshalResult() => new[] - { - ParseStatement("return Avalonia.MicroCom.MicroComRuntime.CreateProxyFor<" + InterfaceType + ">(" + - Name + ", true);") - }; - - public override ExpressionSyntax BackMarshalValue() - { - return ParseExpression("Avalonia.MicroCom.MicroComRuntime.CreateProxyFor<" + InterfaceType + ">(" + - Name + ", false)"); - } - - public override ExpressionSyntax BackMarshalReturn(string resultVar) - { - return ParseExpression($"Avalonia.MicroCom.MicroComRuntime.GetNativePointer({resultVar}, true)"); - } - } - - class BypassArg : Arg - { - public string Type { get; set; } - public int PointerLevel; - public override string ManagedType => Type + new string('*', PointerLevel); - public override string ReturnManagedType => Type + new string('*', PointerLevel - 1); - - public override ExpressionSyntax Value(bool isHresultReturn) - { - if (isHresultReturn) - return ParseExpression("&" + Name); - return base.Value(false); - } - - public override void PreMarshalForReturn(List body) - { - if (PointerLevel == 0) - base.PreMarshalForReturn(body); - else - body.Add(ParseStatement(Type + new string('*', PointerLevel - 1) + " " + Name + "=default;")); - } - } - - class StringArg : Arg - { - private string BName => "__bytemarshal_" + Name; - private string FName => "__fixedmarshal_" + Name; - - public override void PreMarshal(List body) - { - body.Add(ParseStatement($"var {BName} = new byte[System.Text.Encoding.UTF8.GetByteCount({Name})+1];")); - body.Add(ParseStatement($"System.Text.Encoding.UTF8.GetBytes({Name}, 0, {Name}.Length, {BName}, 0);")); - } - - public override StatementSyntax CreateFixed(StatementSyntax inner) - { - return FixedStatement(DeclareVar("byte*", FName, ParseExpression(BName)), inner); - } - - public override ExpressionSyntax Value(bool isHresultReturn) => ParseExpression(FName); - public override string ManagedType => "string"; - public override ExpressionSyntax BackMarshalValue() - { - return ParseExpression( - $"({Name} == null ? null : System.Runtime.InteropServices.Marshal.PtrToStringAnsi(new IntPtr(" + Name + ")))"); - } - } - - string ConvertNativeType(string type) - { - if (type == "size_t") - return "System.IntPtr"; - if (type == "HRESULT") - return "int"; - return type; - } - - Arg ConvertArg(AstInterfaceMemberArgumentNode node) - { - var arg = ConvertArg(node.Name, node.Type); - arg.Attributes = node.Attributes.Clone(); - return arg; - } - - Arg ConvertArg(string name, AstTypeNode type) - { - type = new AstTypeNode { Name = ConvertNativeType(type.Name), PointerLevel = type.PointerLevel }; - - if (type.PointerLevel == 2) - { - if (IsInterface(type)) - return new InterfaceReturnArg { Name = name, InterfaceType = type.Name, NativeType = "void**" }; - } - else if (type.PointerLevel == 1) - { - if (IsInterface(type)) - return new InterfaceArg { Name = name, InterfaceType = type.Name, NativeType = "void*" }; - if (type.Name == "char") - return new StringArg { Name = name, NativeType = "byte*" }; - } - - return new BypassArg - { - Name = name, Type = type.Name, PointerLevel = type.PointerLevel, NativeType = type.ToString() - }; - } - - - void GenerateInterfaceMember(AstInterfaceMemberNode member, ref InterfaceDeclarationSyntax iface, - ref ClassDeclarationSyntax proxy, ref ClassDeclarationSyntax vtbl, - List vtblCtor, int num) - { - // Prepare method information - if (member.Name == "GetRenderingDevice") - Console.WriteLine(); - var args = member.Select(ConvertArg).ToList(); - var returnArg = ConvertArg("__result", member.ReturnType); - bool isHresult = member.ReturnType.Name == "HRESULT"; - bool isHresultLastArgumentReturn = isHresult - && args.Count > 0 - && (args.Last().Name == "ppv" - || args.Last().Name == "retOut" - || args.Last().Name == "ret" - || args.Last().Attributes.HasAttribute("out") - || args.Last().Attributes.HasAttribute("retval") - ) - && ((member.Last().Type.PointerLevel > 0 - && !IsInterface(member.Last().Type)) - || member.Last().Type.PointerLevel == 2); - - bool isVoidReturn = member.ReturnType.Name == "void" && member.ReturnType.PointerLevel == 0; - - - // Generate method signature - MethodDeclarationSyntax GenerateManagedSig(string returnType, string name, - IEnumerable<(string n, string t)> args) - => MethodDeclaration(ParseTypeName(returnType), name).WithParameterList( - ParameterList( - SeparatedList(args.Select(x => Parameter(Identifier(x.n)).WithType(ParseTypeName(x.t)))))); - - var managedSig = - isHresult ? - GenerateManagedSig(isHresultLastArgumentReturn ? args.Last().ReturnManagedType : "void", - member.Name, - (isHresultLastArgumentReturn ? args.SkipLast(1) : args).Select(a => (a.Name, a.ManagedType))) : - GenerateManagedSig(returnArg.ManagedType, member.Name, args.Select(a => (a.Name, a.ManagedType))); - - iface = iface.AddMembers(managedSig.WithSemicolonToken(Semicolon())); - - // Prepare args for marshaling - var preMarshal = new List(); - if (!isVoidReturn) - preMarshal.Add(ParseStatement(returnArg.NativeType + " __result;")); - - for (var idx = 0; idx < args.Count; idx++) - { - if (isHresultLastArgumentReturn && idx == args.Count - 1) - args[idx].PreMarshalForReturn(preMarshal); - else - args[idx].PreMarshal(preMarshal); - } - - // Generate call expression - ExpressionSyntax callExpr = InvocationExpression(_localInterop.GetCaller(returnArg.NativeType, - args.Select(x => x.NativeType).ToList())) - .AddArgumentListArguments(Argument(ParseExpression("PPV"))) - .AddArgumentListArguments(args - .Select((a, i) => Argument(a.Value(isHresultLastArgumentReturn && i == args.Count - 1))).ToArray()) - .AddArgumentListArguments(Argument(ParseExpression("(*PPV)[base.VTableSize + " + num + "]"))); - - if (!isVoidReturn) - callExpr = CastExpression(ParseTypeName(returnArg.NativeType), callExpr); - - // Save call result if needed - if (!isVoidReturn) - callExpr = AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, ParseExpression("__result"), - callExpr); - - - // Wrap call into fixed() blocks - StatementSyntax callStatement = ExpressionStatement(callExpr); - foreach (var arg in args) - callStatement = arg.CreateFixed(callStatement); - - // Build proxy body - var proxyBody = Block() - .AddStatements(preMarshal.ToArray()) - .AddStatements(callStatement); - - // Process return value - if (!isVoidReturn) - { - if (isHresult) - { - proxyBody = proxyBody.AddStatements( - ParseStatement( - $"if(__result != 0) throw new System.Runtime.InteropServices.COMException(\"{member.Name} failed\", __result);")); - - if (isHresultLastArgumentReturn) - proxyBody = proxyBody.AddStatements(args.Last().ReturnMarshalResult()); - } - else - proxyBody = proxyBody.AddStatements(returnArg.ReturnMarshalResult()); - } - - // Add the proxy method - proxy = proxy.AddMembers(managedSig.AddModifiers(SyntaxKind.PublicKeyword) - .WithBody(proxyBody)); - - - // Generate VTable method - var shadowDelegate = DelegateDeclaration(ParseTypeName(returnArg.NativeType), member.Name + "Delegate") - .AddParameterListParameters(Parameter(Identifier("@this")).WithType(ParseTypeName("IntPtr"))) - .AddParameterListParameters(args.Select(x => - Parameter(Identifier(x.Name)).WithType(ParseTypeName(x.NativeType))).ToArray()) - .AddAttribute("System.Runtime.InteropServices.UnmanagedFunctionPointer", - "System.Runtime.InteropServices.CallingConvention.StdCall"); - - var shadowMethod = MethodDeclaration(shadowDelegate.ReturnType, member.Name) - .WithParameterList(shadowDelegate.ParameterList) - .AddModifiers(Token(SyntaxKind.StaticKeyword)); - - var backPreMarshal = new List(); - foreach (var arg in args) - arg.BackPreMarshal(backPreMarshal); - - backPreMarshal.Add( - ParseStatement($"__target = ({iface.Identifier.Text})Avalonia.MicroCom.MicroComRuntime.GetObjectFromCcw(@this);")); - - var isBackVoidReturn = isVoidReturn || (isHresult && !isHresultLastArgumentReturn); - - StatementSyntax backCallStatement; - - var backCallExpr = - IsPropertyRewriteCandidate(managedSig) ? - ParseExpression("__target." + member.Name.Substring(3)) : - InvocationExpression(ParseExpression("__target." + member.Name)) - .WithArgumentList(ArgumentList(SeparatedList( - (isHresultLastArgumentReturn ? args.SkipLast(1) : args) - .Select(a => - Argument(a.BackMarshalValue()))))); - - if (isBackVoidReturn) - backCallStatement = ExpressionStatement(backCallExpr); - else - { - backCallStatement = LocalDeclarationStatement(DeclareVar("var", "__result", backCallExpr)); - if (isHresultLastArgumentReturn) - { - backCallStatement = Block(backCallStatement, - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - ParseExpression("*" + args.Last().Name), - args.Last().BackMarshalReturn("__result") - ))); - - } - else - backCallStatement = Block(backCallStatement, - ReturnStatement(returnArg.BackMarshalReturn("__result"))); - } - - BlockSyntax backBodyBlock = Block().AddStatements(backPreMarshal.ToArray()).AddStatements(backCallStatement); - - - var exceptions = new List() - { - CatchClause( - CatchDeclaration(ParseTypeName("System.Exception"), Identifier("__exception__")), null, - Block( - ParseStatement( - "Avalonia.MicroCom.MicroComRuntime.UnhandledException(__target, __exception__);"), - isHresult ? ParseStatement("return unchecked((int)0x80004005u);") - : isVoidReturn ? EmptyStatement() : ParseStatement("return default;") - )) - }; - - if (isHresult) - exceptions.Insert(0, CatchClause( - CatchDeclaration(ParseTypeName("System.Runtime.InteropServices.COMException"), - Identifier("__com_exception__")), - null, Block(ParseStatement("return __com_exception__.ErrorCode;")))); - - backBodyBlock = Block( - TryStatement( - List(exceptions)) - .WithBlock(Block(backBodyBlock)) - ); - if (isHresult) - backBodyBlock = backBodyBlock.AddStatements(ParseStatement("return 0;")); - - - backBodyBlock = Block() - .AddStatements(ParseStatement($"{iface.Identifier.Text} __target = null;")) - .AddStatements(backBodyBlock.Statements.ToArray()); - - shadowMethod = shadowMethod.WithBody(backBodyBlock); - - vtbl = vtbl.AddMembers(shadowDelegate).AddMembers(shadowMethod); - vtblCtor.Add(ParseStatement("base.AddMethod((" + shadowDelegate.Identifier.Text + ")" + - shadowMethod.Identifier.Text + ");")); - - - - - } - - class LocalInteropHelper - { - public ClassDeclarationSyntax Class { get; private set; } = ClassDeclaration("LocalInterop"); - private HashSet _existing = new HashSet(); - - public ExpressionSyntax GetCaller(string returnType, List args) - { - string ConvertType(string t) => t.EndsWith("*") ? "void*" : t; - returnType = ConvertType(returnType); - args = args.Select(ConvertType).ToList(); - - var name = "CalliStdCall" + returnType.Replace("*", "_ptr"); - var signature = returnType + "::" + name + "::" + string.Join("::", args); - if (_existing.Add(signature)) - { - Class = Class.AddMembers(MethodDeclaration(ParseTypeName(returnType), name) - .AddModifiers(SyntaxKind.StaticKeyword, SyntaxKind.UnsafeKeyword, SyntaxKind.PublicKeyword) - .AddParameterListParameters(Parameter(Identifier("thisObj")).WithType(ParseTypeName("void*"))) - .AddParameterListParameters(args.Select((x, i) => - Parameter(Identifier("arg" + i)).WithType(ParseTypeName(x))).ToArray()) - .AddParameterListParameters(Parameter(Identifier("methodPtr")).WithType(ParseTypeName("void*"))) - .WithBody(Block(ExpressionStatement(ThrowExpression(ParseExpression("null")))))); - } - - return ParseExpression("LocalInterop." + name); - } - } - - - void GenerateInterface(ref NamespaceDeclarationSyntax ns, ref NamespaceDeclarationSyntax implNs, - AstInterfaceNode iface) - { - var guidString = iface.GetAttribute("uuid"); - var inheritsUnknown = iface.Inherits == null || iface.Inherits == "IUnknown"; - - var ifaceDec = InterfaceDeclaration(iface.Name) - .WithBaseType(inheritsUnknown ? "Avalonia.MicroCom.IUnknown" : iface.Inherits) - .AddModifiers(Token(_visibility), Token(SyntaxKind.UnsafeKeyword), Token(SyntaxKind.PartialKeyword)); - - var proxyClassName = "__MicroCom" + iface.Name + "Proxy"; - var proxy = ClassDeclaration(proxyClassName) - .AddModifiers(Token(SyntaxKind.UnsafeKeyword), Token(_visibility), Token(SyntaxKind.PartialKeyword)) - .WithBaseType(inheritsUnknown ? - "Avalonia.MicroCom.MicroComProxyBase" : - ("__MicroCom" + iface.Inherits + "Proxy")) - .AddBaseListTypes(SimpleBaseType(ParseTypeName(iface.Name))); - - - // Generate vtable - var vtbl = ClassDeclaration("__MicroCom" + iface.Name + "VTable") - .AddModifiers(Token(SyntaxKind.UnsafeKeyword)); - - vtbl = vtbl.WithBaseType(inheritsUnknown ? - "Avalonia.MicroCom.MicroComVtblBase" : - "__MicroCom" + iface.Inherits + "VTable"); - - var vtblCtor = new List(); - for (var idx = 0; idx < iface.Count; idx++) - GenerateInterfaceMember(iface[idx], ref ifaceDec, ref proxy, ref vtbl, vtblCtor, idx); - - vtbl = vtbl.AddMembers( - ConstructorDeclaration(vtbl.Identifier.Text) - .AddModifiers(Token(SyntaxKind.PublicKeyword)) - .WithBody(Block(vtblCtor)) - ) - .AddMembers(MethodDeclaration(ParseTypeName("void"), "__MicroComModuleInit") - .AddModifiers(Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.InternalKeyword)) - .WithExpressionBody(ArrowExpressionClause( - ParseExpression("Avalonia.MicroCom.MicroComRuntime.RegisterVTable(typeof(" + - iface.Name + "), new " + vtbl.Identifier.Text + "().CreateVTable())"))) - .WithSemicolonToken(Semicolon())); - - - // Finalize proxy code - proxy = proxy.AddMembers( - MethodDeclaration(ParseTypeName("void"), "__MicroComModuleInit") - .AddModifiers(Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.InternalKeyword)) - .WithBody(Block( - ParseStatement("Avalonia.MicroCom.MicroComRuntime.Register(typeof(" + - iface.Name + "), new Guid(\"" + guidString + "\"), (p, owns) => new " + - proxyClassName + "(p, owns));") - ))) - .AddMembers(ParseMemberDeclaration("public " + proxyClassName + - "(IntPtr nativePointer, bool ownsHandle) : base(nativePointer, ownsHandle) {}")) - .AddMembers(ParseMemberDeclaration("protected override int VTableSize => base.VTableSize + " + - iface.Count + ";")); - - ns = ns.AddMembers(RewriteMethodsToProperties(ifaceDec)); - implNs = implNs.AddMembers(RewriteMethodsToProperties(proxy), RewriteMethodsToProperties(vtbl)); - } - } -} diff --git a/src/tools/MicroComGenerator/CSharpGen.Utils.cs b/src/tools/MicroComGenerator/CSharpGen.Utils.cs deleted file mode 100644 index 28baaa65f8..0000000000 --- a/src/tools/MicroComGenerator/CSharpGen.Utils.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using MicroComGenerator.Ast; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Formatting; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace MicroComGenerator -{ - public partial class CSharpGen - { - - CompilationUnitSyntax Unit() - => CompilationUnit().WithUsings(List(new[] - { - "System", "System.Text", "System.Collections", "System.Collections.Generic", "Avalonia.MicroCom" - } - .Concat(_extraUsings).Select(u => UsingDirective(IdentifierName(u))))); - - string Format(CompilationUnitSyntax unit) - { - var cw = new AdhocWorkspace(); - return - "#pragma warning disable 108\n" + - Microsoft.CodeAnalysis.Formatting.Formatter.Format(unit.NormalizeWhitespace(), cw, cw.Options - .WithChangedOption(CSharpFormattingOptions.NewLineForMembersInObjectInit, true) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInObjectCollectionArrayInitializers, - true) - .WithChangedOption(CSharpFormattingOptions.NewLineForMembersInAnonymousTypes, true) - .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInMethods, true) - - ).ToFullString(); - } - - - SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); - - static VariableDeclarationSyntax DeclareVar(string type, string name, - ExpressionSyntax initializer = null) - => VariableDeclaration(ParseTypeName(type), - SingletonSeparatedList(VariableDeclarator(name) - .WithInitializer(initializer == null ? null : EqualsValueClause(initializer)))); - - FieldDeclarationSyntax DeclareConstant(string type, string name, LiteralExpressionSyntax value) - => FieldDeclaration( - VariableDeclaration(ParseTypeName(type), - SingletonSeparatedList( - VariableDeclarator(name).WithInitializer(EqualsValueClause(value)) - )) - ).WithSemicolonToken(Semicolon()) - .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))); - - FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => - DeclareField(type, name, null, modifiers); - - FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer, - params SyntaxKind[] modifiers) => - FieldDeclaration( - VariableDeclaration(ParseTypeName(type), - SingletonSeparatedList( - VariableDeclarator(name).WithInitializer(initializer)))) - .WithSemicolonToken(Semicolon()) - .WithModifiers(TokenList(modifiers.Select(x => Token(x)))); - - bool IsPropertyRewriteCandidate(MethodDeclarationSyntax method) - { - - return - method.ReturnType.ToFullString() != "void" - && method.Identifier.Text.StartsWith("Get") - && method.ParameterList.Parameters.Count == 0; - } - - TypeDeclarationSyntax RewriteMethodsToProperties(T decl) where T : TypeDeclarationSyntax - { - var replace = new Dictionary(); - foreach (var method in decl.Members.OfType().ToList()) - { - if (IsPropertyRewriteCandidate(method)) - { - var getter = AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); - if (method.Body != null) - getter = getter.WithBody(method.Body); - else - getter = getter.WithSemicolonToken(Semicolon()); - - replace[method] = PropertyDeclaration(method.ReturnType, - method.Identifier.Text.Substring(3)) - .WithModifiers(method.Modifiers).AddAccessorListAccessors(getter); - - } - } - - return decl.ReplaceNodes(replace.Keys, (m, m2) => replace[m]); - } - - bool IsInterface(string name) - { - if (name == "IUnknown") - return true; - return _idl.Interfaces.Any(i => i.Name == name); - } - - private bool IsInterface(AstTypeNode type) => IsInterface(type.Name); - - } -} diff --git a/src/tools/MicroComGenerator/CSharpGen.cs b/src/tools/MicroComGenerator/CSharpGen.cs deleted file mode 100644 index ff4c351fd9..0000000000 --- a/src/tools/MicroComGenerator/CSharpGen.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using MicroComGenerator.Ast; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -// ReSharper disable CoVariantArrayConversion - -namespace MicroComGenerator -{ - public partial class CSharpGen - { - private readonly AstIdlNode _idl; - private List _extraUsings; - private string _namespace; - private SyntaxKind _visibility; - private LocalInteropHelper _localInterop = new LocalInteropHelper(); - - public CSharpGen(AstIdlNode idl) - { - _idl = idl.Clone(); - new AstRewriter(_idl.Attributes.Where(a => a.Name == "clr-map") - .Select(x => x.Value.Trim().Split(' ')) - .ToDictionary(x => x[0], x => x[1]) - ).VisitAst(_idl); - - _extraUsings = _idl.Attributes.Where(u => u.Name == "clr-using").Select(u => u.Value).ToList(); - _namespace = _idl.GetAttribute("clr-namespace"); - var visibilityString = _idl.GetAttribute("clr-access"); - - if (visibilityString == "internal") - _visibility = SyntaxKind.InternalKeyword; - else if (visibilityString == "public") - _visibility = SyntaxKind.PublicKeyword; - else - throw new CodeGenException("Invalid clr-access attribute"); - } - - class AstRewriter : AstVisitor - { - private readonly Dictionary _typeMap = new Dictionary(); - - public AstRewriter(Dictionary typeMap) - { - _typeMap = typeMap; - } - - void ConvertIntPtr(AstTypeNode type) - { - if (type.Name == "void" && type.PointerLevel > 0) - { - type.Name = "IntPtr"; - type.PointerLevel--; - } - } - - protected override void VisitStructMember(AstStructMemberNode member) - { - if (member.HasAttribute("intptr")) - ConvertIntPtr(member.Type); - base.VisitStructMember(member); - } - - protected override void VisitType(AstTypeNode type) - { - if (type.IsLink) - { - type.PointerLevel++; - type.IsLink = false; - } - - if (_typeMap.TryGetValue(type.Name, out var mapped)) - type.Name = mapped; - - base.VisitType(type); - } - - protected override void VisitArgument(AstInterfaceMemberArgumentNode argument) - { - if (argument.HasAttribute("intptr")) - { - if(argument.Name == "retOut") - Console.WriteLine(); - ConvertIntPtr(argument.Type); - } - - base.VisitArgument(argument); - } - - protected override void VisitInterfaceMember(AstInterfaceMemberNode member) - { - if (member.HasAttribute("intptr")) - ConvertIntPtr(member.ReturnType); - if (member.HasAttribute("propget") && !member.Name.StartsWith("Get")) - member.Name = "Get" + member.Name; - if (member.HasAttribute("propput") && !member.Name.StartsWith("Set")) - member.Name = "Set" + member.Name; - base.VisitInterfaceMember(member); - } - } - - - public string Generate() - { - var ns = NamespaceDeclaration(ParseName(_namespace)); - var implNs = NamespaceDeclaration(ParseName(_namespace + ".Impl")); - ns = GenerateEnums(ns); - ns = GenerateStructs(ns); - foreach (var i in _idl.Interfaces) - GenerateInterface(ref ns, ref implNs, i); - - implNs = implNs.AddMembers(_localInterop.Class); - var unit = Unit().AddMembers(ns, implNs); - - return Format(unit); - } - - NamespaceDeclarationSyntax GenerateEnums(NamespaceDeclarationSyntax ns) - { - return ns.AddMembers(_idl.Enums.Select(e => - { - var dec = EnumDeclaration(e.Name) - .WithModifiers(TokenList(Token(_visibility))) - .WithMembers(SeparatedList(e.Select(m => - { - var member = EnumMemberDeclaration(m.Name); - if (m.Value != null) - return member.WithEqualsValue(EqualsValueClause(ParseExpression(m.Value))); - return member; - }))); - if (e.HasAttribute("flags")) - dec = dec.AddAttribute("System.Flags"); - return dec; - }).ToArray()); - } - - NamespaceDeclarationSyntax GenerateStructs(NamespaceDeclarationSyntax ns) - { - return ns.AddMembers(_idl.Structs.Select(e => - StructDeclaration(e.Name) - .WithModifiers(TokenList(Token(_visibility))) - .AddAttribute("System.Runtime.InteropServices.StructLayout", "System.Runtime.InteropServices.LayoutKind.Sequential") - .AddModifiers(Token(SyntaxKind.UnsafeKeyword)) - .WithMembers(new SyntaxList(SeparatedList(e.Select(m => - DeclareField(m.Type.ToString(), m.Name, SyntaxKind.PublicKeyword))))) - ).ToArray()); - } - - - - } -} diff --git a/src/tools/MicroComGenerator/CppGen.cs b/src/tools/MicroComGenerator/CppGen.cs deleted file mode 100644 index b053088ca9..0000000000 --- a/src/tools/MicroComGenerator/CppGen.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using MicroComGenerator.Ast; - -namespace MicroComGenerator -{ - public class CppGen - { - static string ConvertType(AstTypeNode type) - { - var name = type.Name; - if (name == "byte") - name = "unsigned char"; - else if(name == "uint") - name = "unsigned int"; - - type = type.Clone(); - type.Name = name; - return type.Format(); - } - - public static string GenerateCpp(AstIdlNode idl) - { - var sb = new StringBuilder(); - var preamble = idl.GetAttributeOrDefault("cpp-preamble"); - if (preamble != null) - sb.AppendLine(preamble); - - foreach (var s in idl.Structs) - sb.AppendLine("struct " + s.Name + ";"); - - foreach (var s in idl.Interfaces) - sb.AppendLine("struct " + s.Name + ";"); - - foreach (var en in idl.Enums) - { - sb.Append("enum "); - if (en.Attributes.Any(a => a.Name == "class-enum")) - sb.Append("class "); - sb.AppendLine(en.Name).AppendLine("{"); - - foreach (var m in en) - { - sb.Append(" ").Append(m.Name); - if (m.Value != null) - sb.Append(" = ").Append(m.Value); - sb.AppendLine(","); - } - - sb.AppendLine("};"); - } - - foreach (var s in idl.Structs) - { - sb.Append("struct ").AppendLine(s.Name).AppendLine("{"); - foreach (var m in s) - sb.Append(" ").Append(ConvertType(m.Type)).Append(" ").Append(m.Name).AppendLine(";"); - - sb.AppendLine("};"); - } - - foreach (var i in idl.Interfaces) - { - var guidString = i.GetAttribute("uuid"); - var guid = Guid.Parse(guidString).ToString().Replace("-", ""); - - - sb.Append("COMINTERFACE(").Append(i.Name).Append(", ") - .Append(guid.Substring(0, 8)).Append(", ") - .Append(guid.Substring(8, 4)).Append(", ") - .Append(guid.Substring(12, 4)); - for (var c = 0; c < 8; c++) - { - sb.Append(", ").Append(guid.Substring(16 + c * 2, 2)); - } - - sb.Append(") : "); - if (i.HasAttribute("cpp-virtual-inherits")) - sb.Append("virtual "); - sb.AppendLine(i.Inherits ?? "IUnknown") - .AppendLine("{"); - - foreach (var m in i) - { - sb.Append(" ") - .Append("virtual ") - .Append(ConvertType(m.ReturnType)) - .Append(" ").Append(m.Name).Append(" ("); - if (m.Count == 0) - sb.AppendLine(") = 0;"); - else - { - sb.AppendLine(); - for (var c = 0; c < m.Count; c++) - { - var arg = m[c]; - sb.Append(" "); - if (arg.Attributes.Any(a => a.Name == "const")) - sb.Append("const "); - sb.Append(ConvertType(arg.Type)) - .Append(" ") - .Append(arg.Name); - if (c != m.Count - 1) - sb.Append(", "); - sb.AppendLine(); - } - - sb.AppendLine(" ) = 0;"); - } - } - - sb.AppendLine("};"); - } - - return sb.ToString(); - } - } -} diff --git a/src/tools/MicroComGenerator/Extensions.cs b/src/tools/MicroComGenerator/Extensions.cs deleted file mode 100644 index c8a4c8f45c..0000000000 --- a/src/tools/MicroComGenerator/Extensions.cs +++ /dev/null @@ -1,97 +0,0 @@ - -using System; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace MicroComGenerator -{ - public static class Extensions - { - public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers) - { - if (modifiers == null) - return cl; - return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); - } - - public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers) - { - if (modifiers == null) - return cl; - return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); - } - - public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers) - { - if (modifiers == null) - return cl; - return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); - } - - public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers) - { - if (modifiers == null) - return cl; - return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); - } - - public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers) - { - if (modifiers == null) - return cl; - return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); - } - - public static string WithLowerFirst(this string s) - { - if (string.IsNullOrEmpty(s)) - return s; - return char.ToLowerInvariant(s[0]) + s.Substring(1); - } - - public static ExpressionSyntax MemberAccess(params string[] identifiers) - { - if (identifiers == null || identifiers.Length == 0) - throw new ArgumentException(); - var expr = (ExpressionSyntax)IdentifierName(identifiers[0]); - for (var c = 1; c < identifiers.Length; c++) - expr = MemberAccess(expr, identifiers[c]); - return expr; - } - - public static ExpressionSyntax MemberAccess(ExpressionSyntax expr, params string[] identifiers) - { - foreach (var i in identifiers) - expr = MemberAccess(expr, i); - return expr; - } - - public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax expr, string identifier) => - MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, expr, IdentifierName(identifier)); - - public static ClassDeclarationSyntax WithBaseType(this ClassDeclarationSyntax cl, string bt) - { - return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); - } - - public static InterfaceDeclarationSyntax WithBaseType(this InterfaceDeclarationSyntax cl, string bt) - { - return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); - } - - public static T AddAttribute(this T member, string attribute, params string[] args) where T : MemberDeclarationSyntax - { - return (T)member.AddAttributeLists(AttributeList(SingletonSeparatedList( - Attribute(ParseName(attribute), AttributeArgumentList( - SeparatedList(args.Select(a => AttributeArgument(ParseExpression(a))))))))); - } - - public static string StripPrefix(this string s, string prefix) => string.IsNullOrEmpty(s) - ? s - : s.StartsWith(prefix) - ? s.Substring(prefix.Length) - : s; - } -} diff --git a/src/tools/MicroComGenerator/MicroComGenerator.csproj b/src/tools/MicroComGenerator/MicroComGenerator.csproj deleted file mode 100644 index 68895b96ca..0000000000 --- a/src/tools/MicroComGenerator/MicroComGenerator.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - Exe - net6.0 - - - - - - diff --git a/src/tools/MicroComGenerator/ParseException.cs b/src/tools/MicroComGenerator/ParseException.cs deleted file mode 100644 index cb54918100..0000000000 --- a/src/tools/MicroComGenerator/ParseException.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace MicroComGenerator -{ - class ParseException : Exception - { - public int Line { get; } - public int Position { get; } - - public ParseException(string message, int line, int position) : base($"({line}, {position}) {message}") - { - Line = line; - Position = position; - } - - public ParseException(string message, ref TokenParser parser) : this(message, parser.Line, parser.Position) - { - } - } - - class CodeGenException : Exception - { - public CodeGenException(string message) : base(message) - { - } - } -} diff --git a/src/tools/MicroComGenerator/Program.cs b/src/tools/MicroComGenerator/Program.cs deleted file mode 100644 index 2468b1b5a4..0000000000 --- a/src/tools/MicroComGenerator/Program.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using CommandLine; - -namespace MicroComGenerator -{ - class Program - { - public class Options - { - [Option('i', "input", Required = true, HelpText = "Input IDL file")] - public string Input { get; set; } - - [Option("cpp", Required = false, HelpText = "C++ output file")] - public string CppOutput { get; set; } - - [Option("cs", Required = false, HelpText = "C# output file")] - public string CSharpOutput { get; set; } - - } - - static int Main(string[] args) - { - var p = Parser.Default.ParseArguments(args); - if (p is NotParsed) - { - return 1; - } - - var opts = ((Parsed)p).Value; - - var text = File.ReadAllText(opts.Input); - var ast = AstParser.Parse(text); - - if (opts.CppOutput != null) - File.WriteAllText(opts.CppOutput, CppGen.GenerateCpp(ast)); - - if (opts.CSharpOutput != null) - { - File.WriteAllText(opts.CSharpOutput, new CSharpGen(ast).Generate()); - - // HACK: Can't work out how to get the VS project system's fast up-to-date checks - // to ignore the generated code, so as a workaround set the write time to that of - // the input. - File.SetLastWriteTime(opts.CSharpOutput, File.GetLastWriteTime(opts.Input)); - } - - return 0; - } - } -} diff --git a/src/tools/MicroComGenerator/TokenParser.cs b/src/tools/MicroComGenerator/TokenParser.cs deleted file mode 100644 index ea8850b8e4..0000000000 --- a/src/tools/MicroComGenerator/TokenParser.cs +++ /dev/null @@ -1,417 +0,0 @@ -using System; -using System.Globalization; -using System.IO; - -namespace MicroComGenerator -{ - internal ref struct TokenParser - { - private ReadOnlySpan _s; - public int Position { get; private set; } - public int Line { get; private set; } - public TokenParser(ReadOnlySpan s) - { - _s = s; - Position = 0; - Line = 0; - } - - public void SkipWhitespace() - { - while (true) - { - if(_s.Length == 0) - return; - if (char.IsWhiteSpace(_s[0])) - Advance(1); - else if (_s[0] == '/' && _s.Length>1) - { - if (_s[1] == '/') - SkipOneLineComment(); - else if (_s[1] == '*') - SkipMultiLineComment(); - else - return; - } - else - return; - } - } - - void SkipOneLineComment() - { - while (true) - { - if (_s.Length > 0 && _s[0] != '\n' && _s[0] != '\r') - Advance(1); - else - return; - } - } - - void SkipMultiLineComment() - { - var l = Line; - var p = Position; - while (true) - { - if (_s.Length == 0) - throw new ParseException("No matched */ found for /*", l, p); - - if (_s[0] == '*' && _s.Length > 1 && _s[1] == '/') - { - Advance(2); - return; - } - - Advance(1); - } - } - - static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || - (ch >= 'A' && ch <= 'Z'); - - public void Consume(char c) - { - if (!TryConsume(c)) - throw new ParseException("Expected " + c, Line, Position); - } - public bool TryConsume(char c) - { - SkipWhitespace(); - if (_s.Length == 0 || _s[0] != c) - return false; - - Advance(1); - return true; - } - - public bool TryConsume(string s) - { - SkipWhitespace(); - if (_s.Length < s.Length) - return false; - for (var c = 0; c < s.Length; c++) - { - if (_s[c] != s[c]) - return false; - } - - Advance(s.Length); - return true; - } - - public bool TryConsumeAny(ReadOnlySpan chars, out char token) - { - SkipWhitespace(); - token = default; - if (_s.Length == 0) - return false; - - foreach (var c in chars) - { - if (c == _s[0]) - { - token = c; - Advance(1); - return true; - } - } - - return false; - } - - - public bool TryParseKeyword(string keyword) - { - SkipWhitespace(); - if (keyword.Length > _s.Length) - return false; - for(var c=0; c keyword.Length && IsAlphaNumeric(_s[keyword.Length])) - return false; - - Advance(keyword.Length); - return true; - } - - public bool TryParseKeywordLowerCase(string keywordInLowerCase) - { - SkipWhitespace(); - if (keywordInLowerCase.Length > _s.Length) - return false; - for(var c=0; c keywordInLowerCase.Length && IsAlphaNumeric(_s[keywordInLowerCase.Length])) - return false; - - Advance(keywordInLowerCase.Length); - return true; - } - - public void Advance(int c) - { - while (c > 0) - { - if (_s[0] == '\n') - { - Line++; - Position = 0; - } - else - Position++; - - _s = _s.Slice(1); - c--; - } - } - - public int Length => _s.Length; - public bool Eof - { - get - { - SkipWhitespace(); - return Length == 0; - } - } - - public char Peek - { - get - { - if (_s.Length == 0) - throw new ParseException("Unexpected EOF", Line, Position); - return _s[0]; - } - } - - public string ParseIdentifier(ReadOnlySpan extraValidChars) - { - if (!TryParseIdentifier(extraValidChars, out var ident)) - throw new ParseException("Identifier expected", Line, Position); - return ident.ToString(); - } - - public string ParseIdentifier() - { - if (!TryParseIdentifier(out var ident)) - throw new ParseException("Identifier expected", Line, Position); - return ident.ToString(); - } - - public bool TryParseIdentifier(ReadOnlySpan extraValidChars, out ReadOnlySpan res) - { - res = ReadOnlySpan.Empty; - SkipWhitespace(); - if (_s.Length == 0) - return false; - var first = _s[0]; - if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_')) - return false; - int len = 1; - for (var c = 1; c < _s.Length; c++) - { - var ch = _s[c]; - if (IsAlphaNumeric(ch) || ch == '_') - len++; - else - { - var found = false; - foreach(var vc in extraValidChars) - if (vc == ch) - { - found = true; - break; - } - - if (found) - len++; - else - break; - } - } - - res = _s.Slice(0, len); - Advance(len); - return true; - } - - public bool TryParseIdentifier(out ReadOnlySpan res) - { - res = ReadOnlySpan.Empty; - SkipWhitespace(); - if (_s.Length == 0) - return false; - var first = _s[0]; - if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_')) - return false; - int len = 1; - for (var c = 1; c < _s.Length; c++) - { - var ch = _s[c]; - if (IsAlphaNumeric(ch) || ch == '_') - len++; - else - break; - } - - res = _s.Slice(0, len); - Advance(len); - return true; - } - - public string ReadToEol() - { - var initial = _s; - var len = 0; - while (true) - { - if (_s.Length > 0 && _s[0] != '\n' && _s[0] != '\r') - { - len++; - Advance(1); - } - else - return initial.Slice(0, len).ToString(); - } - } - - public string ReadTo(char c) - { - var initial = _s; - var len = 0; - var l = Line; - var p = Position; - while (true) - { - if (_s.Length == 0) - throw new ParseException("Expected " + c + " before EOF", l, p); - - if (_s[0] != c) - { - len++; - Advance(1); - } - else - return initial.Slice(0, len).ToString(); - } - } - - public string ReadToAny(ReadOnlySpan chars) - { - var initial = _s; - var len = 0; - var l = Line; - var p = Position; - while (true) - { - if (_s.Length == 0) - throw new ParseException("Expected any of '" + chars.ToString() + "' before EOF", l, p); - - var foundTerminator = false; - foreach (var term in chars) - { - if (_s[0] == term) - { - foundTerminator = true; - break; - } - } - - if (!foundTerminator) - { - len++; - Advance(1); - } - else - return initial.Slice(0, len).ToString(); - } - } - - public bool TryParseCall(out ReadOnlySpan res) - { - res = ReadOnlySpan.Empty; - SkipWhitespace(); - if (_s.Length == 0) - return false; - var first = _s[0]; - if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) - return false; - int len = 1; - for (var c = 1; c < _s.Length; c++) - { - var ch = _s[c]; - if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch<= 'Z') || ch == '.') - len++; - else - break; - } - - res = _s.Slice(0, len); - - // Find '(' - for (var c = len; c < _s.Length; c++) - { - if(char.IsWhiteSpace(_s[c])) - continue; - if(_s[c]=='(') - { - Advance(c + 1); - return true; - } - - return false; - - } - - return false; - - } - - - public bool TryParseFloat(out float res) - { - res = 0; - SkipWhitespace(); - if (_s.Length == 0) - return false; - - var len = 0; - var dotCount = 0; - for (var c = 0; c < _s.Length; c++) - { - var ch = _s[c]; - if (ch >= '0' && ch <= '9') - len = c + 1; - else if (ch == '.' && dotCount == 0) - { - len = c + 1; - dotCount++; - } - else - break; - } - - var span = _s.Slice(0, len); - -#if NETSTANDARD2_0 - if (!float.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res)) - return false; -#else - if (!float.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res)) - return false; -#endif - Advance(len); - return true; - } - - public override string ToString() => _s.ToString(); - - } -} From e1e2c52e0d905fa582f30c5d7458eb8e5fff670a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Dec 2021 20:50:05 +0300 Subject: [PATCH 32/86] Detected package downgrade --- nukebuild/_build.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index acb001f2e7..f8e2f358d6 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -15,7 +15,6 @@ - From 767de9f642c9355b82054cfaae8e2cb7ddd2eb6f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Dec 2021 20:55:56 +0300 Subject: [PATCH 33/86] Namespace --- src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index a0290ee5e8..8dfdd3159c 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -3,6 +3,7 @@ netstandard2.0;net6.0 true Avalonia.Win32 + Avalonia.MicroCom From 8f60013a1e22bfa6a2f098a66d4a7e781ff11af3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Dec 2021 21:05:04 +0300 Subject: [PATCH 34/86] CI: generate c++ headers with nuke --- azure-pipelines.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 903146cdd7..497402fe4b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -58,8 +58,10 @@ jobs: displayName: 'Generate avalonia-native' inputs: script: | - export PATH="`pwd`/sdk:$PATH" - cd src/tools/MicroComGenerator; dotnet run -f net6.0 -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h + export COREHOST_TRACE=0 + export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + export DOTNET_CLI_TELEMETRY_OPTOUT=1 + ./build.sh --target GenerateCppHeaders --configuration Release - task: Xcode@5 inputs: From 9e5050cab99a0a55fbfb07ac12998feb18765df1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Dec 2021 00:16:34 +0300 Subject: [PATCH 35/86] Fixed some of Rider's complaints --- src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs | 2 +- .../Media/TextFormatting/Unicode/LineBreakEnumerator.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs index 66223e513d..88b1e3c807 100644 --- a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs +++ b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs @@ -180,7 +180,7 @@ namespace Avalonia.Utilities { var r = _data[c]; if (r?.TryGetTarget(out var sub) == true) - sub.OnEvent(sender, eventArgs); + sub!.OnEvent(sender, eventArgs); else needCompact = true; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 40891a700d..0f6b81975a 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -80,12 +80,12 @@ namespace Avalonia.Media.TextFormatting.Unicode } } - var shouldBreak = GetSimpleBreak() ?? (bool?)GetPairTableBreak(lastClass); + var shouldBreak = GetSimpleBreak() ?? GetPairTableBreak(lastClass); // Rule LB8a _lb8a = _nextClass == LineBreakClass.ZWJ; - if (shouldBreak.Value) + if (shouldBreak) { Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); return true; From 494cb0110559c85c0793830fcf1bf902ce725a8a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 24 Dec 2021 11:12:13 +0300 Subject: [PATCH 36/86] Updated microcom --- nukebuild/_build.csproj | 2 +- src/Avalonia.Native/Avalonia.Native.csproj | 2 +- src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index f8e2f358d6..52b60b7d0f 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 366ce97955..9d2471b0c5 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index 8dfdd3159c..10edf42897 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -9,7 +9,7 @@ - + From 69525b8d55c87481b91c8d22f6f72e088a31d8a8 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 24 Dec 2021 14:43:58 +0100 Subject: [PATCH 37/86] Use IsKeyboradFocusWithin instead of IsFocused --- src/Avalonia.Controls/ButtonSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 5a967ab488..f1c1a55a17 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -197,7 +197,7 @@ namespace Avalonia.Controls { base.OnPointerWheelChanged(e); - if (AllowSpin && IsFocused) + if (AllowSpin && IsKeyboardFocusWithin) { if (e.Delta.Y != 0) { From fe42fbfe6b3d7fcd6951a7aca60749c83b05f42d Mon Sep 17 00:00:00 2001 From: Sergey Volkov Date: Sat, 25 Dec 2021 00:56:20 +0300 Subject: [PATCH 38/86] Add ViewContract to ViewModelViewHost --- src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 52 +++++++++++--- .../ViewModelViewHostTest.cs | 72 ++++++++++++++++++- 2 files changed, 113 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index c88323d674..16dee00ebc 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -3,7 +3,7 @@ using System.Reactive.Disposables; using ReactiveUI; using Splat; -namespace Avalonia.ReactiveUI +namespace Avalonia.ReactiveUI { /// /// This content control will automatically load the View associated with @@ -18,6 +18,12 @@ namespace Avalonia.ReactiveUI public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); + /// + /// for the property. + /// + public static readonly StyledProperty ViewContractProperty = + AvaloniaProperty.Register(nameof(ViewContract)); + /// /// Initializes a new instance of the class. /// @@ -25,8 +31,8 @@ namespace Avalonia.ReactiveUI { this.WhenActivated(disposables => { - this.WhenAnyValue(x => x.ViewModel) - .Subscribe(NavigateToViewModel) + this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract) + .Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2)) .DisposeWith(disposables); }); } @@ -39,7 +45,16 @@ namespace Avalonia.ReactiveUI get => GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } - + + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => GetValue(ViewContractProperty); + set => SetValue(ViewContractProperty, value); + } + /// /// Gets or sets the view locator. /// @@ -49,7 +64,8 @@ namespace Avalonia.ReactiveUI /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - private void NavigateToViewModel(object? viewModel) + /// The contract for view resolution. + private void NavigateToViewModel(object? viewModel, string? contract) { if (viewModel == null) { @@ -57,17 +73,33 @@ namespace Avalonia.ReactiveUI Content = DefaultContent; return; } - + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var viewInstance = viewLocator.ResolveView(viewModel); + var viewInstance = viewLocator.ResolveView(viewModel, contract); if (viewInstance == null) { - this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + if (contract == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + } + else + { + this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); + } + Content = DefaultContent; return; } - - this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + + if (contract == null) + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + } + else + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); + } + viewInstance.ViewModel = viewModel; if (viewInstance is IStyledElement styled) styled.DataContext = viewModel; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs index 35d1cbf62d..858c476227 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -12,15 +12,23 @@ namespace Avalonia.ReactiveUI.UnitTests public class FirstView : ReactiveUserControl { } + public class AlternativeFirstView : ReactiveUserControl { } + public class SecondViewModel : ReactiveObject { } public class SecondView : ReactiveUserControl { } + public class AlternativeSecondView : ReactiveUserControl { } + + public static string AlternativeViewContract => "AlternativeView"; + public ViewModelViewHostTest() { Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor)); Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new AlternativeFirstView(), typeof(IViewFor), AlternativeViewContract); + Locator.CurrentMutable.Register(() => new AlternativeSecondView(), typeof(IViewFor), AlternativeViewContract); } [Fact] @@ -67,5 +75,67 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.Equal(first, ((FirstView)host.Content).DataContext); Assert.Equal(first, ((FirstView)host.Content).ViewModel); } + + [Fact] + public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel_And_Contract() + { + var defaultContent = new TextBlock(); + var host = new ViewModelViewHost + { + DefaultContent = defaultContent, + PageTransition = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + var first = new FirstViewModel(); + host.ViewModel = first; + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.Equal(typeof(AlternativeFirstView), host.Content.GetType()); + Assert.Equal(first, ((AlternativeFirstView)host.Content).DataContext); + Assert.Equal(first, ((AlternativeFirstView)host.Content).ViewModel); + + var second = new SecondViewModel(); + host.ViewModel = second; + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondView), host.Content.GetType()); + Assert.Equal(second, ((SecondView)host.Content).DataContext); + Assert.Equal(second, ((SecondView)host.Content).ViewModel); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.Equal(typeof(AlternativeSecondView), host.Content.GetType()); + Assert.Equal(second, ((AlternativeSecondView)host.Content).DataContext); + Assert.Equal(second, ((AlternativeSecondView)host.Content).ViewModel); + + host.ViewModel = null; + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + } } -} \ No newline at end of file +} From af902ae969573099b67dc506d7516ca2dde1a165 Mon Sep 17 00:00:00 2001 From: Sergey Volkov Date: Sat, 25 Dec 2021 00:56:36 +0300 Subject: [PATCH 39/86] Add ViewContract to RoutedViewHost --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 57 ++++++++++++--- .../RoutedViewHostTest.cs | 73 +++++++++++++++++++ 2 files changed, 119 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 63456bc13a..a475cf5eac 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -57,7 +57,13 @@ namespace Avalonia.ReactiveUI /// public static readonly StyledProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); - + + /// + /// for the property. + /// + public static readonly StyledProperty ViewContractProperty = + AvaloniaProperty.Register(nameof(ViewContract)); + /// /// Initializes a new instance of the class. /// @@ -70,15 +76,18 @@ namespace Avalonia.ReactiveUI .Where(router => router == null)! .Cast(); + var viewContract = this.WhenAnyValue(x => x.ViewContract); + this.WhenAnyValue(x => x.Router) .Where(router => router != null) .SelectMany(router => router!.CurrentViewModel) .Merge(routerRemoved) - .Subscribe(NavigateToViewModel) + .CombineLatest(viewContract) + .Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second)) .DisposeWith(disposables); }); } - + /// /// Gets or sets the of the view model stack. /// @@ -87,17 +96,27 @@ namespace Avalonia.ReactiveUI get => GetValue(RouterProperty); set => SetValue(RouterProperty, value); } - + + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => GetValue(ViewContractProperty); + set => SetValue(ViewContractProperty, value); + } + /// /// Gets or sets the ReactiveUI view locator used by this router. /// public IViewLocator? ViewLocator { get; set; } - + /// /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - private void NavigateToViewModel(object? viewModel) + /// The contract for view resolution. + private void NavigateToViewModel(object? viewModel, string? contract) { if (Router == null) { @@ -112,17 +131,33 @@ namespace Avalonia.ReactiveUI Content = DefaultContent; return; } - + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var viewInstance = viewLocator.ResolveView(viewModel); + var viewInstance = viewLocator.ResolveView(viewModel, contract); if (viewInstance == null) { - this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + if (contract == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + } + else + { + this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); + } + Content = DefaultContent; return; } - - this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + + if (contract == null) + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + } + else + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); + } + viewInstance.ViewModel = viewModel; if (viewInstance is IDataContextProvider provider) provider.DataContext = viewModel; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index b82b1b1acc..244b5abc4e 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -29,6 +29,8 @@ namespace Avalonia.ReactiveUI.UnitTests public class FirstRoutableView : ReactiveUserControl { } + public class AlternativeFirstRoutableView : ReactiveUserControl { } + public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel { public string UrlPathSegment => "second"; @@ -38,16 +40,22 @@ namespace Avalonia.ReactiveUI.UnitTests public class SecondRoutableView : ReactiveUserControl { } + public class AlternativeSecondRoutableView : ReactiveUserControl { } + public class ScreenViewModel : ReactiveObject, IScreen { public RoutingState Router { get; } = new RoutingState(); } + public static string AlternativeViewContract => "AlternativeView"; + public RoutedViewHostTest() { Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor)); Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new AlternativeFirstRoutableView(), typeof(IViewFor), AlternativeViewContract); + Locator.CurrentMutable.Register(() => new AlternativeSecondRoutableView(), typeof(IViewFor), AlternativeViewContract); } [Fact] @@ -101,6 +109,71 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.Equal(defaultContent, host.Content); } + [Fact] + public void RoutedViewHost_Should_Stay_In_Sync_With_RoutingState_And_Contract() + { + var screen = new ScreenViewModel(); + var defaultContent = new TextBlock(); + var host = new RoutedViewHost + { + Router = screen.Router, + DefaultContent = defaultContent, + PageTransition = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(defaultContent, host.Content); + + var first = new FirstRoutableViewModel(); + screen.Router.Navigate.Execute(first).Subscribe(); + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(first, ((FirstRoutableView)host.Content).DataContext); + Assert.Equal(first, ((FirstRoutableView)host.Content).ViewModel); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext); + Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel); + + var second = new SecondRoutableViewModel(); + screen.Router.Navigate.Execute(second).Subscribe(); + + host.ViewContract = null; + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(second, ((SecondRoutableView)host.Content).DataContext); + Assert.Equal(second, ((SecondRoutableView)host.Content).ViewModel); + + host.ViewContract = AlternativeViewContract; + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).DataContext); + Assert.Equal(second, ((AlternativeSecondRoutableView)host.Content).ViewModel); + + screen.Router.NavigateBack.Execute(Unit.Default).Subscribe(); + + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).DataContext); + Assert.Equal(first, ((AlternativeFirstRoutableView)host.Content).ViewModel); + + screen.Router.NavigateBack.Execute(Unit.Default).Subscribe(); + + Assert.NotNull(host.Content); + Assert.IsType(host.Content); + Assert.Equal(defaultContent, host.Content); + } + [Fact] public void RoutedViewHost_Should_Show_Default_Content_When_Router_Is_Null() { From c39cc1b0833fa74c6a911a75426de1b64c00910a Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Sat, 25 Dec 2021 07:13:23 +0100 Subject: [PATCH 40/86] feat(DevTools): Allow to attach DevTools at Application (#6771) * feat(DevTools): Allow to attach DevTools at Application * fixes(DevTools): PointerOverElement when attach to Application * feat: Add AvaloniaVersion * feat: IsDevelopmentBuild * feat: Add some useful properties to Application Decorator * fixes: Hide Layout Viewer when select appliction node * fix: removed MachineId * fixes: DesignerSupportTests fails * fix(DevTools): Update XML Comment and nits * fix(DevTools): Avoid interaction of Layout Visualizer with keyboard when it is hidden. * Added some comment * fixes: Code formatting * fixes(DevTools): Strip unnecessary property from Application Decorator * fixes(ControlCatalog): remove AttachDevTools from DecoratedWindow * fixes(DevTools): Application doesn't close when the last window closed when DevTools is open * fixes: Missing Application properties decoration * fixes(DevTools): Nullable annotations * fixes(DevTools): Unified the behavior of AttachDevTools * fixes(DevTools): typo * fixes(DevTools): Null Annotation Co-authored-by: Max Katz --- samples/ControlCatalog/App.xaml.cs | 5 + .../ControlCatalog/DecoratedWindow.xaml.cs | 1 - samples/ControlCatalog/MainWindow.xaml.cs | 5 +- .../DevToolsExtensions.cs | 44 +++++++ .../Diagnostics/Behaviors/ColumnDefinition.cs | 47 ++++++++ .../Diagnostics/Controls/Application.cs | 109 ++++++++++++++++++ .../Diagnostics/DevTools.cs | 95 ++++++++++++--- .../Diagnostics/DevToolsOptions.cs | 1 + .../Diagnostics/KeyGestureExtesions.cs | 28 +++++ .../ViewModels/ControlDetailsViewModel.cs | 27 ++--- .../Diagnostics/ViewModels/LogicalTreeNode.cs | 90 ++++++++++++++- .../Diagnostics/ViewModels/MainViewModel.cs | 66 ++++++++--- .../Diagnostics/ViewModels/TreeNode.cs | 9 +- .../ViewModels/TreeNodeCollection.cs | 14 +++ .../Diagnostics/ViewModels/VisualTreeNode.cs | 80 ++++++++++++- .../Diagnostics/Views/ControlDetailsView.xaml | 22 +++- .../Diagnostics/Views/MainView.xaml | 2 +- .../Diagnostics/Views/MainWindow.xaml.cs | 21 ++-- .../Diagnostics/Views/TreePageView.xaml.cs | 7 +- 19 files changed, 593 insertions(+), 80 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index aba22a4af6..816356bee9 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -94,6 +94,11 @@ namespace ControlCatalog singleViewLifetime.MainView = new MainView(); base.OnFrameworkInitializationCompleted(); + + this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + { + StartupScreenIndex = 1, + }); } } } diff --git a/samples/ControlCatalog/DecoratedWindow.xaml.cs b/samples/ControlCatalog/DecoratedWindow.xaml.cs index bdf5b8fbee..a1383b9107 100644 --- a/samples/ControlCatalog/DecoratedWindow.xaml.cs +++ b/samples/ControlCatalog/DecoratedWindow.xaml.cs @@ -11,7 +11,6 @@ namespace ControlCatalog public DecoratedWindow() { this.InitializeComponent(); - this.AttachDevTools(); } void SetupSide(string name, StandardCursorType cursor, WindowEdge edge) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index dd62698cc7..20591103b7 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -17,10 +17,7 @@ namespace ControlCatalog public MainWindow() { this.InitializeComponent(); - this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() - { - StartupScreenIndex = 1, - }); + //Renderer.DrawFps = true; //Renderer.DrawDirtyRects = Renderer.DrawFps = true; diff --git a/src/Avalonia.Diagnostics/DevToolsExtensions.cs b/src/Avalonia.Diagnostics/DevToolsExtensions.cs index aa585dca40..68deaf8335 100644 --- a/src/Avalonia.Diagnostics/DevToolsExtensions.cs +++ b/src/Avalonia.Diagnostics/DevToolsExtensions.cs @@ -37,5 +37,49 @@ namespace Avalonia { DevTools.Attach(root, options); } + + /// + /// Attaches DevTools to a Application, to be opened with the specified options. + /// + /// The Application to attach DevTools to. + public static void AttachDevTools(this Application application) + { + DevTools.Attach(application, new DevToolsOptions()); + } + + /// + /// Attaches DevTools to a Application, to be opened with the specified options. + /// + /// The Application to attach DevTools to. + /// Additional settings of DevTools. + /// + /// Attach DevTools should only be called after application initialization is complete. A good point is + /// + /// + /// + /// public class App : Application + /// { + /// public override void OnFrameworkInitializationCompleted() + /// { + /// if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + /// { + /// desktopLifetime.MainWindow = new MainWindow(); + /// } + /// else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + /// singleViewLifetime.MainView = new MainView(); + /// + /// base.OnFrameworkInitializationCompleted(); + /// this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + /// { + /// StartupScreenIndex = 1, + /// }); + /// } + /// } + /// + /// + public static void AttachDevTools(this Application application, DevToolsOptions options) + { + DevTools.Attach(application, options); + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs b/src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs new file mode 100644 index 0000000000..beccf022d7 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs @@ -0,0 +1,47 @@ +namespace Avalonia.Diagnostics.Behaviors +{ + /// + /// See discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773 + /// + static class ColumnDefinition + { + private readonly static Avalonia.Controls.GridLength ZeroWidth = + new Avalonia.Controls.GridLength(0, Avalonia.Controls.GridUnitType.Pixel); + + private readonly static AttachedProperty LastWidthProperty = + AvaloniaProperty.RegisterAttached("LastWidth" + , typeof(ColumnDefinition) + , default); + + public readonly static AttachedProperty IsVisibleProperty = + AvaloniaProperty.RegisterAttached("IsVisible" + , typeof(ColumnDefinition) + , true + , coerce: (element, visibility) => + { + + var lastWidth = element.GetValue(LastWidthProperty); + if (visibility == true && lastWidth is { }) + { + element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, lastWidth); + } + else if (visibility == false) + { + element.SetValue(LastWidthProperty, element.GetValue(Avalonia.Controls.ColumnDefinition.WidthProperty)); + element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, ZeroWidth); + } + return visibility; + } + ); + + public static bool GetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition) + { + return columnDefinition.GetValue(IsVisibleProperty); + } + + public static void SetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition, bool visibility) + { + columnDefinition.SetValue(IsVisibleProperty, visibility); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs new file mode 100644 index 0000000000..b2f2974440 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -0,0 +1,109 @@ +using System; +using Avalonia.Controls; +using Lifetimes = Avalonia.Controls.ApplicationLifetimes; +using App = Avalonia.Application; + +namespace Avalonia.Diagnostics.Controls +{ + class Application : AvaloniaObject + , Input.ICloseable + + { + private readonly App _application; + private static readonly Version s_version = typeof(IAvaloniaObject).Assembly?.GetName()?.Version + ?? Version.Parse("0.0.00"); + public event EventHandler? Closed; + + public Application(App application) + { + _application = application; + + if (_application.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller) + { + EventHandler eh = default!; + eh = (s, e) => + { + controller.Exit -= eh; + Closed?.Invoke(s, e); + }; + controller.Exit += eh; + } + + } + + internal App Instance => _application; + + /// + /// Defines the property. + /// + public object? DataContext => + _application.DataContext; + + /// + /// Gets or sets the application's global data templates. + /// + /// + /// The application's global data templates. + /// + public Avalonia.Controls.Templates.DataTemplates DataTemplates => + _application.DataTemplates; + + /// + /// Gets the application's focus manager. + /// + /// + /// The application's focus manager. + /// + public Input.IFocusManager? FocusManager => + _application.FocusManager; + + /// + /// Gets the application's input manager. + /// + /// + /// The application's input manager. + /// + public Input.InputManager? InputManager => + _application.InputManager; + + /// + /// Gets the application clipboard. + /// + public Input.Platform.IClipboard? Clipboard => + _application.Clipboard; + + /// + /// Gets the application's global resource dictionary. + /// + public IResourceDictionary Resources => + _application.Resources; + + /// + /// Gets the application's global styles. + /// + /// + /// The application's global styles. + /// + /// + /// Global styles apply to all windows in the application. + /// + public Styling.Styles Styles => + _application.Styles; + + /// + /// Application lifetime, use it for things like setting the main window and exiting the app from code + /// Currently supported lifetimes are: + /// - + /// - + /// - + /// + public Lifetimes.IApplicationLifetime? ApplicationLifetime => + _application.ApplicationLifetime; + + /// + /// Application name to be used for various platform-specific purposes + /// + public string? Name => + _application.Name; + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 2a386f106e..683c2e6549 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -1,17 +1,21 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.Views; using Avalonia.Input; +using Avalonia.Input.Raw; using Avalonia.Interactivity; namespace Avalonia.Diagnostics { public static class DevTools { - private static readonly Dictionary s_open = - new Dictionary(); + private static readonly Dictionary s_open = + new Dictionary(); + + private static bool s_attachedToApplication; public static IDisposable Attach(TopLevel root, KeyGesture gesture) { @@ -23,6 +27,11 @@ namespace Avalonia.Diagnostics public static IDisposable Attach(TopLevel root, DevToolsOptions options) { + if (s_attachedToApplication == true) + { + throw new ArgumentException("DevTools already attached to application", nameof(root)); + } + void PreviewKeyDown(object? sender, KeyEventArgs e) { if (options.Gesture.Matches(e)) @@ -37,45 +46,95 @@ namespace Avalonia.Diagnostics RoutingStrategies.Tunnel); } - public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions()); + public static IDisposable Open(TopLevel root) => + Open(Application.Current,new DevToolsOptions(),root as Window); + + public static IDisposable Open(TopLevel root, DevToolsOptions options) => + Open(Application.Current, options, root as Window); - public static IDisposable Open(TopLevel root, DevToolsOptions options) + private static void DevToolsClosed(object? sender, EventArgs e) { - if (s_open.TryGetValue(root, out var window)) + var window = (MainWindow)sender!; + window.Closed -= DevToolsClosed; + if (window.Root is Controls.Application host) + { + s_open.Remove(host.Instance); + } + else { + s_open.Remove(window.Root!); + } + } + + internal static IDisposable Attach(Application? application, DevToolsOptions options, Window? owner = null) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + var result = Disposable.Empty; + // Skip if call on Design Mode + if (!Avalonia.Controls.Design.IsDesignMode + && !s_attachedToApplication) + { + + var lifeTime = application.ApplicationLifetime + as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime; + + if (lifeTime is null) + { + throw new ArgumentNullException(nameof(Application.ApplicationLifetime)); + } + + if (application.InputManager is { }) + { + s_attachedToApplication = true; + + application.InputManager.PreProcess.OfType().Subscribe(e => + { + if (options.Gesture.Matches(e)) + { + result = Open(application, options, owner); + } + }); + + } + } + return result; + } + + private static IDisposable Open(Application? application, DevToolsOptions options, Window? owner = default) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + if (s_open.TryGetValue(application, out var window)) + { window.Activate(); } else { window = new MainWindow { - Root = root, + Root = new Controls.Application(application), Width = options.Size.Width, Height = options.Size.Height, }; window.SetOptions(options); window.Closed += DevToolsClosed; - s_open.Add(root, window); - - if (options.ShowAsChildWindow && root is Window inspectedWindow) + s_open.Add(application, window); + if (options.ShowAsChildWindow && owner is { }) { - window.Show(inspectedWindow); + window.Show(owner); } else { window.Show(); } } - return Disposable.Create(() => window?.Close()); } - - private static void DevToolsClosed(object? sender, EventArgs e) - { - var window = (MainWindow)sender!; - s_open.Remove(window.Root!); - window.Closed -= DevToolsClosed; - } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index 5336dca65b..4181989388 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -16,6 +16,7 @@ namespace Avalonia.Diagnostics /// Gets or sets a value indicating whether DevTools should be displayed as a child window /// of the window being inspected. The default value is true. /// + /// This setting is ignored if DevTools is attached to public bool ShowAsChildWindow { get; set; } = true; /// diff --git a/src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs b/src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs new file mode 100644 index 0000000000..bb3ebc4708 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs @@ -0,0 +1,28 @@ +using Avalonia.Input; +using Avalonia.Input.Raw; + +namespace Avalonia.Diagnostics +{ + static class KeyGestureExtesions + { + public static bool Matches(this KeyGesture gesture, RawKeyEventArgs keyEvent) => + keyEvent != null && + (KeyModifiers)(keyEvent.Modifiers & RawInputModifiers.KeyboardMask) == gesture.KeyModifiers && + ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(gesture.Key); + + private static Key ResolveNumPadOperationKey(Key key) + { + switch (key) + { + case Key.Add: + return Key.OemPlus; + case Key.Subtract: + return Key.OemMinus; + case Key.Decimal: + return Key.OemPeriod; + default: + return key; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 16293e35d2..c96172d5bb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -16,7 +16,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class ControlDetailsViewModel : ViewModelBase, IDisposable { - private readonly IVisual _control; + private readonly IAvaloniaObject _avaloniaObject; private IDictionary>? _propertyIndex; private PropertyViewModel? _selectedProperty; private DataGridCollectionView? _propertiesView; @@ -28,20 +28,21 @@ namespace Avalonia.Diagnostics.ViewModels private string? _selectedEntityName; private string? _selectedEntityType; - public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) + public ControlDetailsViewModel(TreePageViewModel treePage, IAvaloniaObject avaloniaObject) { - _control = control; + _avaloniaObject = avaloniaObject; - TreePage = treePage; - - Layout = new ControlLayoutViewModel(control); + TreePage = treePage; + Layout = avaloniaObject is IVisual + ? new ControlLayoutViewModel((IVisual)avaloniaObject) + : default; - NavigateToProperty(control, (control as IControl)?.Name ?? control.ToString()); + NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); AppliedStyles = new ObservableCollection(); PseudoClasses = new ObservableCollection(); - if (control is StyledElement styledElement) + if (avaloniaObject is StyledElement styledElement) { styledElement.Classes.CollectionChanged += OnClassesChanged; @@ -181,7 +182,7 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _styleStatus, value); } - public ControlLayoutViewModel Layout { get; } + public ControlLayoutViewModel? Layout { get; } protected override void OnPropertyChanged(PropertyChangedEventArgs e) { @@ -215,17 +216,17 @@ namespace Avalonia.Diagnostics.ViewModels public void Dispose() { - if (_control is INotifyPropertyChanged inpc) + if (_avaloniaObject is INotifyPropertyChanged inpc) { inpc.PropertyChanged -= ControlPropertyChanged; } - if (_control is AvaloniaObject ao) + if (_avaloniaObject is AvaloniaObject ao) { ao.PropertyChanged -= ControlPropertyChanged; } - if (_control is StyledElement se) + if (_avaloniaObject is StyledElement se) { se.Classes.CollectionChanged -= OnClassesChanged; } @@ -278,7 +279,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - Layout.ControlPropertyChanged(sender, e); + Layout?.ControlPropertyChanged(sender, e); } private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index 04215fa8ae..d627eba154 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -1,23 +1,31 @@ using System; +using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; +using Lifetimes = Avalonia.Controls.ApplicationLifetimes; +using System.Linq; namespace Avalonia.Diagnostics.ViewModels { internal class LogicalTreeNode : TreeNode { - public LogicalTreeNode(ILogical logical, TreeNode? parent) - : base((Control)logical, parent) + public LogicalTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent) + : base(avaloniaObject, parent) { - Children = new LogicalTreeNodeCollection(this, logical); + Children = avaloniaObject switch + { + ILogical logical => new LogicalTreeNodeCollection(this, logical), + Controls.Application host => new ApplicationHostLogical(this, host), + _ => TreeNodeCollection.Empty + }; } public override TreeNodeCollection Children { get; } public static LogicalTreeNode[] Create(object control) { - var logical = control as ILogical; + var logical = control as IAvaloniaObject; return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty(); } @@ -41,10 +49,82 @@ namespace Avalonia.Diagnostics.ViewModels protected override void Initialize(AvaloniaList nodes) { _subscription = _control.LogicalChildren.ForEachItem( - (i, item) => nodes.Insert(i, new LogicalTreeNode(item, Owner)), + (i, item) => nodes.Insert(i, new LogicalTreeNode((IAvaloniaObject)item, Owner)), (i, item) => nodes.RemoveAt(i), () => nodes.Clear()); } } + + internal class ApplicationHostLogical : TreeNodeCollection + { + readonly Controls.Application _application; + CompositeDisposable _subscriptions = new CompositeDisposable(2); + public ApplicationHostLogical(TreeNode owner, Controls.Application host) : + base(owner) + { + _application = host; + } + + protected override void Initialize(AvaloniaList nodes) + { + if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single) + { + nodes.Add(new LogicalTreeNode(single.MainView, Owner)); + } + if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic) + { + + for (int i = 0; i < classic.Windows.Count; i++) + { + var window = classic.Windows[i]; + if (window is Views.MainWindow) + { + continue; + } + nodes.Add(new LogicalTreeNode(window, Owner)); + } + _subscriptions = new System.Reactive.Disposables.CompositeDisposable() + { + Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> + { + if (s is Views.MainWindow) + { + return; + } + nodes.Add(new LogicalTreeNode((IAvaloniaObject)s!,Owner)); + }), + Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=> + { + if (s is Views.MainWindow) + { + return; + } + var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s)); + if(!(item is null)) + { + nodes.Remove(item); + } + if(nodes.Count == 0) + { + if (Avalonia.Application.Current?.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller) + { + controller.Shutdown(); + } + else + { + Environment.Exit(0); + } + } + }), + }; + } + } + + public override void Dispose() + { + _subscriptions?.Dispose(); + base.Dispose(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 74f2f05948..d809768a47 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -4,12 +4,14 @@ using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Threading; +using System.Reactive.Linq; +using System.Linq; namespace Avalonia.Diagnostics.ViewModels { internal class MainViewModel : ViewModelBase, IDisposable { - private readonly TopLevel _root; + private readonly AvaloniaObject _root; private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _visualTree; private readonly EventsPageViewModel _events; @@ -17,13 +19,14 @@ namespace Avalonia.Diagnostics.ViewModels private ViewModelBase? _content; private int _selectedTab; private string? _focusedControl; - private string? _pointerOverElement; + private IInputElement? _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; private bool _freezePopups; - - public MainViewModel(TopLevel root) + private string? _pointerOverElementName; + private IInputRoot? _pointerOverRoot; + public MainViewModel(AvaloniaObject root) { _root = root; _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); @@ -35,8 +38,24 @@ namespace Avalonia.Diagnostics.ViewModels if (KeyboardDevice.Instance is not null) KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged; SelectedTab = 0; - _pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty) - .Subscribe(x => PointerOverElement = x?.GetType().Name); + if (root is TopLevel topLevel) + { + _pointerOverSubscription = topLevel.GetObservable(TopLevel.PointerOverElementProperty) + .Subscribe(x => PointerOverElement = x); + + } + else + { +#nullable disable + _pointerOverSubscription = InputManager.Instance.PreProcess + .OfType() + .Subscribe(e => + { + PointerOverRoot = e.Root; + PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault(); + }); +#nullable restore + } Console = new ConsoleViewModel(UpdateConsoleContext); } @@ -51,13 +70,13 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeMarginPadding; set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); } - + public bool ShouldVisualizeDirtyRects { get => _shouldVisualizeDirtyRects; set { - _root.Renderer.DrawDirtyRects = value; + ((TopLevel)_root).Renderer.DrawDirtyRects = value; RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); } } @@ -77,7 +96,7 @@ namespace Avalonia.Diagnostics.ViewModels get => _showFpsOverlay; set { - _root.Renderer.DrawFps = value; + ((TopLevel)_root).Renderer.DrawFps = value; RaiseAndSetIfChanged(ref _showFpsOverlay, value); } } @@ -150,12 +169,28 @@ namespace Avalonia.Diagnostics.ViewModels private set { RaiseAndSetIfChanged(ref _focusedControl, value); } } - public string? PointerOverElement + public IInputRoot? PointerOverRoot + { + get => _pointerOverRoot; + private set => RaiseAndSetIfChanged( ref _pointerOverRoot , value); + } + + public IInputElement? PointerOverElement { get { return _pointerOverElement; } - private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } + private set + { + RaiseAndSetIfChanged(ref _pointerOverElement, value); + PointerOverElementName = value?.GetType()?.Name; + } } - + + public string? PointerOverElementName + { + get => _pointerOverElementName; + private set => RaiseAndSetIfChanged(ref _pointerOverElementName, value); + } + private void UpdateConsoleContext(ConsoleContext context) { context.root = _root; @@ -188,8 +223,11 @@ namespace Avalonia.Diagnostics.ViewModels _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); - _root.Renderer.DrawDirtyRects = false; - _root.Renderer.DrawFps = false; + if (_root is TopLevel top) + { + top.Renderer.DrawDirtyRects = false; + top.Renderer.DrawFps = false; + } } private void UpdateFocusedControl() diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 94707ac189..5459810db9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -16,12 +16,13 @@ namespace Avalonia.Diagnostics.ViewModels private string _classes; private bool _isExpanded; - protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null) + protected TreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null) { _classes = string.Empty; Parent = parent; - Type = customName ?? visual.GetType().Name; - Visual = visual; + var visual = avaloniaObject ; + Type = customName ?? avaloniaObject.GetType().Name; + Visual = visual!; FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; if (visual is IControl control) @@ -76,7 +77,7 @@ namespace Avalonia.Diagnostics.ViewModels get; } - public IVisual Visual + public IAvaloniaObject Visual { get; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs index c007411f49..953afa280b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs @@ -10,6 +10,20 @@ namespace Avalonia.Diagnostics.ViewModels { internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList, IDisposable { + private class EmptyTreeNodeCollection : TreeNodeCollection + { + public EmptyTreeNodeCollection():base(default!) + { + + } + protected override void Initialize(AvaloniaList nodes) + { + + } + } + + static readonly internal TreeNodeCollection Empty = new EmptyTreeNodeCollection(); + private AvaloniaList? _inner; public TreeNodeCollection(TreeNode owner) => Owner = owner; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 6a430897ba..5e1128d4b7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -7,15 +7,22 @@ using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Styling; using Avalonia.VisualTree; +using Lifetimes = Avalonia.Controls.ApplicationLifetimes; +using System.Linq; namespace Avalonia.Diagnostics.ViewModels { internal class VisualTreeNode : TreeNode { - public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null) - : base(visual, parent, customName) + public VisualTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null) + : base(avaloniaObject, parent, customName) { - Children = new VisualTreeNodeCollection(this, visual); + Children = avaloniaObject switch + { + IVisual visual => new VisualTreeNodeCollection(this, visual), + Controls.Application host => new ApplicationHostVisuals(this, host), + _ => TreeNodeCollection.Empty + }; if (Visual is IStyleable styleable) IsInTemplate = styleable.TemplatedParent != null; @@ -27,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels public static VisualTreeNode[] Create(object control) { - return control is IVisual visual ? + return control is IAvaloniaObject visual ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); } @@ -130,7 +137,7 @@ namespace Avalonia.Diagnostics.ViewModels _subscriptions.Add( _control.VisualChildren.ForEachItem( - (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), + (i, item) => nodes.Insert(i, new VisualTreeNode((IAvaloniaObject)item, Owner)), (i, item) => nodes.RemoveAt(i), () => nodes.Clear())); } @@ -147,5 +154,68 @@ namespace Avalonia.Diagnostics.ViewModels public string? CustomName { get; } } } + + internal class ApplicationHostVisuals : TreeNodeCollection + { + readonly Controls.Application _application; + CompositeDisposable _subscriptions = new CompositeDisposable(2); + public ApplicationHostVisuals(TreeNode owner, Controls.Application host) : + base(owner) + { + _application = host; + } + + protected override void Initialize(AvaloniaList nodes) + { + if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single) + { + nodes.Add(new VisualTreeNode(single.MainView, Owner)); + } + if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic) + { + + for (int i = 0; i < classic.Windows.Count; i++) + { + var window = classic.Windows[i]; + if (window is Views.MainWindow) + { + continue; + } + nodes.Add(new VisualTreeNode(window, Owner)); + } + _subscriptions = new System.Reactive.Disposables.CompositeDisposable() + { + Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> + { + if (s is Views.MainWindow) + { + return; + } + nodes.Add(new VisualTreeNode((IAvaloniaObject)s!,Owner)); + }), + Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=> + { + if (s is Views.MainWindow) + { + return; + } + var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s)); + if(!(item is null)) + { + nodes.Remove(item); + } + }), + }; + + + } + } + + public override void Dispose() + { + _subscriptions?.Dispose(); + base.Dispose(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index cf74fa1b34..7f924af144 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -4,6 +4,7 @@ xmlns:local="clr-namespace:Avalonia.Diagnostics.Views" xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" + xmlns:lb="using:Avalonia.Diagnostics.Behaviors" x:Class="Avalonia.Diagnostics.Views.ControlDetailsView" x:Name="Main"> @@ -11,8 +12,20 @@ - - + + + + + + + + @@ -53,9 +66,10 @@ - + - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 6f2ac96a66..7e0dab1d6e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -73,7 +73,7 @@ Pointer Over: - + _frozenPopupStates; - private TopLevel? _root; + private AvaloniaObject? _root; public MainWindow() { @@ -50,23 +50,23 @@ namespace Avalonia.Diagnostics.Views this.Opened += lh; } - public TopLevel? Root + public AvaloniaObject? Root { get => _root; set { if (_root != value) { - if (_root != null) + if (_root is ICloseable oldClosable) { - _root.Closed -= RootClosed; + oldClosable.Closed -= RootClosed; } _root = value; - if (_root != null) + if (_root is ICloseable newClosable) { - _root.Closed += RootClosed; + newClosable.Closed += RootClosed; DataContext = new MainViewModel(_root); } else @@ -91,9 +91,9 @@ namespace Avalonia.Diagnostics.Views _frozenPopupStates.Clear(); - if (_root != null) + if (_root is ICloseable cloneable) { - _root.Closed -= RootClosed; + cloneable.Closed -= RootClosed; _root = null; } @@ -123,7 +123,7 @@ namespace Avalonia.Diagnostics.Views .FirstOrDefault(); } - private static List GetPopupRoots(IVisual root) + private static List GetPopupRoots(TopLevel root) { var popupRoots = new List(); @@ -160,7 +160,8 @@ namespace Avalonia.Diagnostics.Views return; } - var root = Root; + var root = Root as TopLevel + ?? vm.PointerOverRoot as TopLevel; if (root is null) { return; diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 3543b1adea..649b65c521 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -47,7 +47,12 @@ namespace Avalonia.Diagnostics.Views return; } - var visual = (Visual)node.Visual; + var visual = node.Visual as Visual; + + if (visual is null) + { + return; + } _currentLayer = AdornerLayer.GetAdornerLayer(visual); From 1d34536236ee84dace3f8414ff3a73b71cbac654 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sun, 26 Dec 2021 22:47:31 +0300 Subject: [PATCH 41/86] [WASM] Fix wrong DPI value --- src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index 3e9849d4dc..acd23a98b0 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -142,7 +142,7 @@ namespace Avalonia.Web.Blazor public Size ClientSize => _clientSize; public Size? FrameSize => null; - public double RenderScaling => 1; + public double RenderScaling => _currentSurface?.Scaling ?? 1; public IEnumerable Surfaces => new object[] { _currentSurface! }; From a0d0e3ac87d2883f6a3ceb46e375f699f02f31a2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 27 Dec 2021 11:48:22 +0100 Subject: [PATCH 42/86] fix(DevTools): Move 'Show Implemented Interfaces' from Options menu to View menu --- .../Diagnostics/Views/MainView.xaml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 8b5af082e1..965d8e88e4 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -15,6 +15,15 @@ IsEnabled="False" /> + + + + + + + @@ -38,16 +47,6 @@ IsEnabled="False" /> - - - - - - - - From 215425c45a02bd7c0cad7a881d198a4fbac66568 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 27 Dec 2021 12:17:37 +0100 Subject: [PATCH 43/86] fixes(DevTools): NRE when click 'Visualizze dirty rects' and 'Show fps overly' in Option menu --- .../Diagnostics/Controls/Application.cs | 12 ++++++- .../Diagnostics/ViewModels/MainViewModel.cs | 32 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index b2f2974440..316e31b37e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -28,7 +28,12 @@ namespace Avalonia.Diagnostics.Controls }; controller.Exit += eh; } - + RendererRoot = application.ApplicationLifetime switch + { + Lifetimes.IClassicDesktopStyleApplicationLifetime classic => classic.MainWindow.Renderer, + Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as VisualTree.IVisual)?.VisualRoot?.Renderer, + _ => null + }; } internal App Instance => _application; @@ -105,5 +110,10 @@ namespace Avalonia.Diagnostics.Controls /// public string? Name => _application.Name; + + /// + /// Gets the root of the visual tree, if the control is attached to a visual tree. + /// + internal Rendering.IRenderer? RendererRoot { get; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index d809768a47..c607542991 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -76,7 +76,20 @@ namespace Avalonia.Diagnostics.ViewModels get => _shouldVisualizeDirtyRects; set { - ((TopLevel)_root).Renderer.DrawDirtyRects = value; + var changed = true; + if (_root is TopLevel topLevel && topLevel.Renderer is { }) + { + topLevel.Renderer.DrawDirtyRects = value; + } + else if (_root is Controls.Application app && app.RendererRoot is { }) + { + app.RendererRoot.DrawDirtyRects = value; + } + else + { + changed = false; + } + if (changed) RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); } } @@ -96,8 +109,21 @@ namespace Avalonia.Diagnostics.ViewModels get => _showFpsOverlay; set { - ((TopLevel)_root).Renderer.DrawFps = value; - RaiseAndSetIfChanged(ref _showFpsOverlay, value); + var changed = true; + if (_root is TopLevel topLevel && topLevel.Renderer is { }) + { + topLevel.Renderer.DrawFps = value; + } + else if (_root is Controls.Application app && app.RendererRoot is { }) + { + app.RendererRoot.DrawFps = value; + } + else + { + changed = false; + } + if(changed) + RaiseAndSetIfChanged(ref _showFpsOverlay, value); } } From 00a286f0e8bba92869eeec96abe1a2094f19dbae Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 27 Dec 2021 14:15:30 +0200 Subject: [PATCH 44/86] Add separate platform settings for touch. --- .../Avalonia.Android/AndroidPlatform.cs | 8 ++--- src/Avalonia.Input/TouchDevice.cs | 6 ++-- src/Avalonia.Native/AvaloniaNativePlatform.cs | 29 +++++++++++-------- .../Platform/ITouchPlatformSettings.cs | 11 +++++++ src/Avalonia.X11/Stubs.cs | 12 ++++---- src/Avalonia.X11/X11Platform.cs | 4 ++- src/Windows/Avalonia.Win32/Win32Platform.cs | 6 ++-- src/iOS/Avalonia.iOS/Platform.cs | 8 ++--- 8 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 57f22e7a05..a9ba1086d5 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -29,12 +29,12 @@ namespace Avalonia namespace Avalonia.Android { - class AndroidPlatform : IPlatformSettings + class AndroidPlatform : ITouchPlatformSettings { public static readonly AndroidPlatform Instance = new AndroidPlatform(); public static AndroidPlatformOptions Options { get; private set; } - public Size DoubleClickSize => new Size(4, 4); - public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200); + public Size TouchDoubleClickSize => new Size(4, 4); + public TimeSpan TouchDoubleClickTime => TimeSpan.FromMilliseconds(200); public static void Initialize(Type appType, AndroidPlatformOptions options) { @@ -45,7 +45,7 @@ namespace Avalonia.Android .Bind().ToTransient() .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() - .Bind().ToConstant(Instance) + .Bind().ToConstant(Instance) .Bind().ToConstant(new AndroidThreadingInterface()) .Bind().ToTransient() .Bind().ToSingleton() diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index af8851a82b..4970d4db32 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -58,20 +58,20 @@ namespace Avalonia.Input } else { - var settings = AvaloniaLocator.Current.GetService(); + var settings = AvaloniaLocator.Current.GetService(); if (settings == null) { throw new Exception("IPlatformSettings can not be null."); } if (!_lastClickRect.Contains(args.Position) - || ev.Timestamp - _lastClickTime > settings.DoubleClickTime.TotalMilliseconds) + || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds) { _clickCount = 0; } ++_clickCount; _lastClickTime = ev.Timestamp; _lastClickRect = new Rect(args.Position, new Size()) - .Inflate(new Thickness(16, 16)); + .Inflate(new Thickness(settings.TouchDoubleClickSize.Width / 2, settings.TouchDoubleClickSize.Height / 2)); } target.RaiseEvent(new PointerPressedEventArgs(target, pointer, diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 522db1b334..680f64f537 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -11,7 +11,7 @@ using Avalonia.Rendering; namespace Avalonia.Native { - class AvaloniaNativePlatform : IPlatformSettings, IWindowingPlatform + class AvaloniaNativePlatform : IPlatformSettings, ITouchPlatformSettings, IWindowingPlatform { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions _options; @@ -26,9 +26,13 @@ namespace Avalonia.Native public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO + public Size TouchDoubleClickSize => new Size(16, 16); + + public TimeSpan TouchDoubleClickTime => DoubleClickTime; + public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) { - var result = new AvaloniaNativePlatform(MicroComRuntime.CreateProxyFor(factory, true)); + var result = new AvaloniaNativePlatform(MicroComRuntime.CreateProxyFor(factory, true)); result.DoInitialize(options); return result; @@ -55,14 +59,14 @@ namespace Avalonia.Native return Initialize(CreateAvaloniaNative(), options); } - public void SetupApplicationMenuExporter () + public void SetupApplicationMenuExporter() { var exporter = new AvaloniaNativeMenuExporter(_factory); } - public void SetupApplicationName () + public void SetupApplicationName() { - if(!string.IsNullOrWhiteSpace(Application.Current.Name)) + if (!string.IsNullOrWhiteSpace(Application.Current.Name)) { _factory.MacOptions.SetApplicationTitle(Application.Current.Name); } @@ -80,13 +84,13 @@ namespace Avalonia.Native GCHandle.FromIntPtr(handle).Free(); } } - + void DoInitialize(AvaloniaNativePlatformOptions options) { _options = options; - + var applicationPlatform = new AvaloniaNativeApplicationPlatform(); - + _factory.Initialize(new GCHandleDeallocator(), applicationPlatform); if (_factory.MacOptions != null) { @@ -102,6 +106,7 @@ namespace Avalonia.Native .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) .Bind().ToConstant(this) + .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind().ToConstant(new RenderLoop()) @@ -118,7 +123,7 @@ namespace Avalonia.Native hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); - + if (_options.UseGpu) { try @@ -133,7 +138,7 @@ namespace Avalonia.Native } } - public ITrayIconImpl CreateTrayIcon () + public ITrayIconImpl CreateTrayIcon() { return new TrayIconImpl(_factory); } @@ -159,8 +164,8 @@ namespace Avalonia.Native ShowInDock = true; } - public bool ShowInDock - { + public bool ShowInDock + { get => _showInDock; set { diff --git a/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs b/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs new file mode 100644 index 0000000000..bda0dc3c33 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs @@ -0,0 +1,11 @@ +using System; + +namespace Avalonia.Platform +{ + public interface ITouchPlatformSettings + { + Size TouchDoubleClickSize { get; } + + TimeSpan TouchDoubleClickTime { get; } + } +} diff --git a/src/Avalonia.X11/Stubs.cs b/src/Avalonia.X11/Stubs.cs index ec694ba9a8..ca1c037592 100644 --- a/src/Avalonia.X11/Stubs.cs +++ b/src/Avalonia.X11/Stubs.cs @@ -1,17 +1,15 @@ using System; -using System.IO; -using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Input; -using Avalonia.Input.Platform; using Avalonia.Platform; namespace Avalonia.X11 { - class PlatformSettingsStub : IPlatformSettings + class PlatformSettingsStub : IPlatformSettings, ITouchPlatformSettings { public Size DoubleClickSize { get; } = new Size(2, 2); public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + + public Size TouchDoubleClickSize => new Size(16, 16); + + public TimeSpan TouchDoubleClickTime => DoubleClickTime; } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index a18f91d301..e0d84e0a2e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -68,6 +68,7 @@ namespace Avalonia.X11 //TODO: log if (options.UseDBusMenu) DBusHelper.TryInitialize(); + var platformSettingStub = new PlatformSettingsStub(); AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(this) .Bind().ToConstant(new X11PlatformThreading(this)) @@ -77,7 +78,8 @@ namespace Avalonia.X11 .Bind().ToFunc(() => KeyboardDevice) .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) - .Bind().ToConstant(new PlatformSettingsStub()) + .Bind().ToConstant(platformSettingStub) + .Bind().ToConstant(platformSettingStub) .Bind().ToConstant(new X11IconLoader(Info)) .Bind().ToConstant(new GtkSystemDialog()) .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 2947493c41..08d98d3ac1 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -101,7 +101,7 @@ namespace Avalonia namespace Avalonia.Win32 { - public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl + public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, ITouchPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl { private static readonly Win32Platform s_instance = new Win32Platform(); private static Thread _uiThread; @@ -133,7 +133,8 @@ namespace Avalonia.Win32 UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYDOUBLECLK)); public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(UnmanagedMethods.GetDoubleClickTime()); - + public Size TouchDoubleClickSize => new Size(16,16); + public TimeSpan TouchDoubleClickTime => DoubleClickTime; public static void Initialize() { Initialize(new Win32PlatformOptions()); @@ -146,6 +147,7 @@ namespace Avalonia.Win32 .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(WindowsKeyboardDevice.Instance) + .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop()) diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 2cac5e6bcf..fd5330b663 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -13,10 +13,10 @@ namespace Avalonia.iOS { public static EaglFeature GlFeature; public static DisplayLinkTimer Timer; - class PlatformSettings : IPlatformSettings + class PlatformSettings : ITouchPlatformSettings { - public Size DoubleClickSize { get; } = new Size(10, 10); - public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + public Size TouchDoubleClickSize => new Size(10, 10); + public TimeSpan TouchDoubleClickTime => TimeSpan.FromMilliseconds(500); } public static void Register() @@ -30,7 +30,7 @@ namespace Avalonia.iOS .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) - .Bind().ToConstant(new PlatformSettings()) + .Bind().ToConstant(new PlatformSettings()) .Bind().ToConstant(new PlatformIconLoaderStub()) .Bind().ToSingleton() .Bind().ToSingleton() From 9af9a3f0813bd59c744e8a1e908c8a14e184761d Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 27 Dec 2021 14:53:15 +0200 Subject: [PATCH 45/86] fix tests --- src/Avalonia.Input/TouchDevice.cs | 2 +- tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 4970d4db32..a3881c42e7 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -61,7 +61,7 @@ namespace Avalonia.Input var settings = AvaloniaLocator.Current.GetService(); if (settings == null) { - throw new Exception("IPlatformSettings can not be null."); + throw new Exception("ITouchPlatformSettings can not be null."); } if (!_lastClickRect.Contains(args.Position) || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds) diff --git a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs index 7f6d711ccf..14b7df7260 100644 --- a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs @@ -208,10 +208,10 @@ namespace Avalonia.Input.UnitTests { var unitTestApp = UnitTestApplication.Start( new TestServices(inputManager: new InputManager())); - var iSettingsMock = new Mock(); - iSettingsMock.Setup(x => x.DoubleClickTime).Returns(doubleClickTime); + var iSettingsMock = new Mock(); + iSettingsMock.Setup(x => x.TouchDoubleClickTime).Returns(doubleClickTime); AvaloniaLocator.CurrentMutable.BindToSelf(this) - .Bind().ToConstant(iSettingsMock.Object); + .Bind().ToConstant(iSettingsMock.Object); return unitTestApp; } From 05021506e6165a004c7eec97e97318bc31203bde Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 27 Dec 2021 11:17:22 -0500 Subject: [PATCH 46/86] Update src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml --- src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 22f9ccf513..381fc68cf3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -15,7 +15,7 @@ IsEnabled="False" /> - + Date: Tue, 28 Dec 2021 13:25:05 +0200 Subject: [PATCH 47/86] add comments --- src/Android/Avalonia.Android/AndroidPlatform.cs | 4 ++++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 ++ src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs | 6 ++++++ src/Avalonia.X11/Stubs.cs | 2 ++ src/Windows/Avalonia.Win32/Win32Platform.cs | 4 ++++ src/iOS/Avalonia.iOS/Platform.cs | 3 +++ 6 files changed, 21 insertions(+) diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index a9ba1086d5..3bf01a0f15 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -33,7 +33,11 @@ namespace Avalonia.Android { public static readonly AndroidPlatform Instance = new AndroidPlatform(); public static AndroidPlatformOptions Options { get; private set; } + + /// public Size TouchDoubleClickSize => new Size(4, 4); + + /// public TimeSpan TouchDoubleClickTime => TimeSpan.FromMilliseconds(200); public static void Initialize(Type appType, AndroidPlatformOptions options) diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 680f64f537..f2d7f741fc 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -26,8 +26,10 @@ namespace Avalonia.Native public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO + /// public Size TouchDoubleClickSize => new Size(16, 16); + /// public TimeSpan TouchDoubleClickTime => DoubleClickTime; public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) diff --git a/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs b/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs index bda0dc3c33..bf65870d1b 100644 --- a/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs +++ b/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs @@ -4,8 +4,14 @@ namespace Avalonia.Platform { public interface ITouchPlatformSettings { + /// + /// Determines the size of the area within that you should click twice in order for a double click to be counted. + /// Size TouchDoubleClickSize { get; } + /// + /// Determines the time span that what will be used to determine the double-click. + /// TimeSpan TouchDoubleClickTime { get; } } } diff --git a/src/Avalonia.X11/Stubs.cs b/src/Avalonia.X11/Stubs.cs index ca1c037592..e98153e5a9 100644 --- a/src/Avalonia.X11/Stubs.cs +++ b/src/Avalonia.X11/Stubs.cs @@ -8,8 +8,10 @@ namespace Avalonia.X11 public Size DoubleClickSize { get; } = new Size(2, 2); public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + /// public Size TouchDoubleClickSize => new Size(16, 16); + /// public TimeSpan TouchDoubleClickTime => DoubleClickTime; } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 08d98d3ac1..d1196bbc8d 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -133,7 +133,11 @@ namespace Avalonia.Win32 UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYDOUBLECLK)); public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(UnmanagedMethods.GetDoubleClickTime()); + + /// public Size TouchDoubleClickSize => new Size(16,16); + + /// public TimeSpan TouchDoubleClickTime => DoubleClickTime; public static void Initialize() { diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index fd5330b663..fa0db4518f 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -15,7 +15,10 @@ namespace Avalonia.iOS public static DisplayLinkTimer Timer; class PlatformSettings : ITouchPlatformSettings { + /// public Size TouchDoubleClickSize => new Size(10, 10); + + /// public TimeSpan TouchDoubleClickTime => TimeSpan.FromMilliseconds(500); } From f9671bd4cc7e03431c50c9181705e4097517375b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 28 Dec 2021 14:31:03 +0200 Subject: [PATCH 48/86] use getrequiredservice --- src/Avalonia.Input/TouchDevice.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index a3881c42e7..84974302eb 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -58,11 +58,8 @@ namespace Avalonia.Input } else { - var settings = AvaloniaLocator.Current.GetService(); - if (settings == null) - { - throw new Exception("ITouchPlatformSettings can not be null."); - } + var settings = AvaloniaLocator.Current.GetRequiredService(); + if (!_lastClickRect.Contains(args.Position) || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds) { From c69d8f07136e20a653fde2f3a5113b96b13bb8dc Mon Sep 17 00:00:00 2001 From: Luthfi Tri Atmaja Date: Wed, 29 Dec 2021 01:44:11 +0700 Subject: [PATCH 49/86] select text when clicking on a TextBox with shift key modifier --- src/Avalonia.Controls/TextBox.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 32428bea53..20d8a94c1a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1006,13 +1006,28 @@ namespace Avalonia.Controls if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border)) { var point = e.GetPosition(_presenter); - var index = CaretIndex = _presenter.GetCaretIndex(point); + var index = _presenter.GetCaretIndex(point); + var clickToSelect = index != CaretIndex && e.KeyModifiers.HasFlag(KeyModifiers.Shift); + + if (!clickToSelect) + { + CaretIndex = index; + } + #pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) #pragma warning restore CS0618 // Type or member is obsolete { case 1: - SelectionStart = SelectionEnd = index; + if (clickToSelect) + { + SelectionStart = Math.Min(index, CaretIndex); + SelectionEnd = Math.Max(index, CaretIndex); + } + else + { + SelectionStart = SelectionEnd = index; + } break; case 2: if (!StringUtils.IsStartOfWord(text, index)) From 21cc80932a9997a4da618a3be103f3a3e0851be8 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 28 Dec 2021 22:19:33 +0200 Subject: [PATCH 50/86] use IPlatformSettings instead of creating a separate interface. --- src/Android/Avalonia.Android/AndroidPlatform.cs | 12 ++++++++---- .../Remote/PreviewerWindowingPlatform.cs | 4 ++++ src/Avalonia.Headless/HeadlessPlatformStubs.cs | 4 ++++ src/Avalonia.Input/TouchDevice.cs | 2 +- src/Avalonia.Native/AvaloniaNativePlatform.cs | 7 +++---- src/Avalonia.Visuals/ApiCompatBaseline.txt | 6 +++++- .../Platform/IPlatformSettings.cs | 10 ++++++++++ .../Platform/ITouchPlatformSettings.cs | 17 ----------------- src/Avalonia.X11/Stubs.cs | 6 +++--- src/Avalonia.X11/X11Platform.cs | 5 ++--- src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs | 4 ++++ .../Avalonia.Web.Blazor/WindowingPlatform.cs | 3 +++ src/Windows/Avalonia.Win32/Win32Platform.cs | 7 +++---- src/iOS/Avalonia.iOS/Platform.cs | 16 +++++++++------- .../TouchDeviceTests.cs | 4 ++-- 15 files changed, 61 insertions(+), 46 deletions(-) delete mode 100644 src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 3bf01a0f15..6a940a54f1 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -29,17 +29,21 @@ namespace Avalonia namespace Avalonia.Android { - class AndroidPlatform : ITouchPlatformSettings + class AndroidPlatform : IPlatformSettings { public static readonly AndroidPlatform Instance = new AndroidPlatform(); public static AndroidPlatformOptions Options { get; private set; } - /// + /// public Size TouchDoubleClickSize => new Size(4, 4); - /// + /// public TimeSpan TouchDoubleClickTime => TimeSpan.FromMilliseconds(200); + public Size DoubleClickSize => TouchDoubleClickSize; + + public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); + public static void Initialize(Type appType, AndroidPlatformOptions options) { Options = options; @@ -49,7 +53,7 @@ namespace Avalonia.Android .Bind().ToTransient() .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() - .Bind().ToConstant(Instance) + .Bind().ToConstant(Instance) .Bind().ToConstant(new AndroidThreadingInterface()) .Bind().ToTransient() .Bind().ToSingleton() diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index ada63f5326..5d8b166b5a 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -64,5 +64,9 @@ namespace Avalonia.DesignerSupport.Remote public Size DoubleClickSize { get; } = new Size(2, 2); public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + + public Size TouchDoubleClickSize => new Size(16, 16); + + public TimeSpan TouchDoubleClickTime => DoubleClickTime; } } diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index ce4c31e27e..605659d464 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -67,6 +67,10 @@ namespace Avalonia.Headless { public Size DoubleClickSize { get; } = new Size(2, 2); public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + + public Size TouchDoubleClickSize => new Size(16,16); + + public TimeSpan TouchDoubleClickTime => DoubleClickTime; } class HeadlessSystemDialogsStub : ISystemDialogImpl diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 84974302eb..0069aa7961 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -58,7 +58,7 @@ namespace Avalonia.Input } else { - var settings = AvaloniaLocator.Current.GetRequiredService(); + var settings = AvaloniaLocator.Current.GetRequiredService(); if (!_lastClickRect.Contains(args.Position) || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds) diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index f2d7f741fc..5fa50f0e7f 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -11,7 +11,7 @@ using Avalonia.Rendering; namespace Avalonia.Native { - class AvaloniaNativePlatform : IPlatformSettings, ITouchPlatformSettings, IWindowingPlatform + class AvaloniaNativePlatform : IPlatformSettings, IWindowingPlatform { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions _options; @@ -26,10 +26,10 @@ namespace Avalonia.Native public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); //TODO - /// + /// public Size TouchDoubleClickSize => new Size(16, 16); - /// + /// public TimeSpan TouchDoubleClickTime => DoubleClickTime; public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) @@ -108,7 +108,6 @@ namespace Avalonia.Native .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) .Bind().ToConstant(this) - .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind().ToConstant(new RenderLoop()) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index ad9a5fd71a..dcb3246a63 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -79,4 +79,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 79 +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime.get()' is present in the implementation but not in the contract. +Total Issues: 84 diff --git a/src/Avalonia.Visuals/Platform/IPlatformSettings.cs b/src/Avalonia.Visuals/Platform/IPlatformSettings.cs index bcb00df5d6..e4b28e6575 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformSettings.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformSettings.cs @@ -7,5 +7,15 @@ namespace Avalonia.Platform Size DoubleClickSize { get; } TimeSpan DoubleClickTime { get; } + + /// + /// Determines the size of the area within that you should click twice in order for a double click to be counted. + /// + Size TouchDoubleClickSize { get; } + + /// + /// Determines the time span that what will be used to determine the double-click. + /// + TimeSpan TouchDoubleClickTime { get; } } } diff --git a/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs b/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs deleted file mode 100644 index bf65870d1b..0000000000 --- a/src/Avalonia.Visuals/Platform/ITouchPlatformSettings.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Avalonia.Platform -{ - public interface ITouchPlatformSettings - { - /// - /// Determines the size of the area within that you should click twice in order for a double click to be counted. - /// - Size TouchDoubleClickSize { get; } - - /// - /// Determines the time span that what will be used to determine the double-click. - /// - TimeSpan TouchDoubleClickTime { get; } - } -} diff --git a/src/Avalonia.X11/Stubs.cs b/src/Avalonia.X11/Stubs.cs index e98153e5a9..f73512f1e8 100644 --- a/src/Avalonia.X11/Stubs.cs +++ b/src/Avalonia.X11/Stubs.cs @@ -3,15 +3,15 @@ using Avalonia.Platform; namespace Avalonia.X11 { - class PlatformSettingsStub : IPlatformSettings, ITouchPlatformSettings + class PlatformSettingsStub : IPlatformSettings { public Size DoubleClickSize { get; } = new Size(2, 2); public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); - /// + /// public Size TouchDoubleClickSize => new Size(16, 16); - /// + /// public TimeSpan TouchDoubleClickTime => DoubleClickTime; } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index e0d84e0a2e..ec3f29c806 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -68,7 +68,7 @@ namespace Avalonia.X11 //TODO: log if (options.UseDBusMenu) DBusHelper.TryInitialize(); - var platformSettingStub = new PlatformSettingsStub(); + AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(this) .Bind().ToConstant(new X11PlatformThreading(this)) @@ -78,8 +78,7 @@ namespace Avalonia.X11 .Bind().ToFunc(() => KeyboardDevice) .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) - .Bind().ToConstant(platformSettingStub) - .Bind().ToConstant(platformSettingStub) + .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) .Bind().ToConstant(new GtkSystemDialog()) .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs index 642be28c69..dd60d5f09d 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -18,5 +18,9 @@ namespace Avalonia.LinuxFramebuffer { public Size DoubleClickSize { get; } = new Size(4, 4); public TimeSpan DoubleClickTime { get; } = new TimeSpan(0, 0, 0, 0, 500); + + public Size TouchDoubleClickSize => new Size(16,16); + + public TimeSpan TouchDoubleClickTime => DoubleClickTime; } } diff --git a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs index f30a36b8c9..979e66a785 100644 --- a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs @@ -51,6 +51,9 @@ namespace Avalonia.Web.Blazor public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + public Size TouchDoubleClickSize => new Size(16, 16); + + public TimeSpan TouchDoubleClickTime => DoubleClickTime; public void RunLoop(CancellationToken cancellationToken) { throw new NotSupportedException(); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index d1196bbc8d..5cfbab40e4 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -101,7 +101,7 @@ namespace Avalonia namespace Avalonia.Win32 { - public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, ITouchPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl + public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl { private static readonly Win32Platform s_instance = new Win32Platform(); private static Thread _uiThread; @@ -134,10 +134,10 @@ namespace Avalonia.Win32 public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(UnmanagedMethods.GetDoubleClickTime()); - /// + /// public Size TouchDoubleClickSize => new Size(16,16); - /// + /// public TimeSpan TouchDoubleClickTime => DoubleClickTime; public static void Initialize() { @@ -151,7 +151,6 @@ namespace Avalonia.Win32 .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(WindowsKeyboardDevice.Instance) - .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop()) diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index fa0db4518f..88f60ace1f 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -1,11 +1,9 @@ using System; -using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; -using Avalonia.Shared.PlatformSupport; namespace Avalonia.iOS { @@ -13,15 +11,19 @@ namespace Avalonia.iOS { public static EaglFeature GlFeature; public static DisplayLinkTimer Timer; - class PlatformSettings : ITouchPlatformSettings + class PlatformSettings : IPlatformSettings { - /// + /// public Size TouchDoubleClickSize => new Size(10, 10); - /// + /// public TimeSpan TouchDoubleClickTime => TimeSpan.FromMilliseconds(500); + + public Size DoubleClickSize => new Size(4, 4); + + public TimeSpan DoubleClickTime => TouchDoubleClickTime; } - + public static void Register() { GlFeature ??= new EaglFeature(); @@ -33,7 +35,7 @@ namespace Avalonia.iOS .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) - .Bind().ToConstant(new PlatformSettings()) + .Bind().ToConstant(new PlatformSettings()) .Bind().ToConstant(new PlatformIconLoaderStub()) .Bind().ToSingleton() .Bind().ToSingleton() diff --git a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs index 14b7df7260..80c5a45c1a 100644 --- a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs @@ -208,10 +208,10 @@ namespace Avalonia.Input.UnitTests { var unitTestApp = UnitTestApplication.Start( new TestServices(inputManager: new InputManager())); - var iSettingsMock = new Mock(); + var iSettingsMock = new Mock(); iSettingsMock.Setup(x => x.TouchDoubleClickTime).Returns(doubleClickTime); AvaloniaLocator.CurrentMutable.BindToSelf(this) - .Bind().ToConstant(iSettingsMock.Object); + .Bind().ToConstant(iSettingsMock.Object); return unitTestApp; } From 509d7c06f70b93d0ec65503255a5d56f9593f8f3 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 29 Dec 2021 11:23:29 +0100 Subject: [PATCH 51/86] fixes: Null Annotaion --- src/Avalonia.Base/Threading/Dispatcher.cs | 2 +- src/Avalonia.Base/Threading/JobRunner.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index b7d5d5a5d2..908f431776 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -113,7 +113,7 @@ namespace Avalonia.Threading /// public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) { - Contract.Requires(action != null); + _ = action ?? throw new ArgumentNullException(nameof(action)); _jobRunner.Post(action, arg, priority); } diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs index 66f6243a35..f2aef0414c 100644 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ b/src/Avalonia.Base/Threading/JobRunner.cs @@ -197,7 +197,7 @@ namespace Avalonia.Threading { private readonly Action _action; private readonly T _parameter; - private readonly TaskCompletionSource _taskCompletionSource; + private readonly TaskCompletionSource? _taskCompletionSource; /// /// Initializes a new instance of the class. @@ -212,7 +212,7 @@ namespace Avalonia.Threading _action = action; _parameter = parameter; Priority = priority; - _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); + _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); } /// @@ -229,7 +229,7 @@ namespace Avalonia.Threading try { _action(_parameter); - _taskCompletionSource.SetResult(null); + _taskCompletionSource.SetResult(default); } catch (Exception e) { From eab9659487f370f8a589568c23d8df8f8d93f260 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Thu, 30 Dec 2021 01:11:11 +0100 Subject: [PATCH 52/86] [DevTools] Allowed to take a screenshot of the currently selected control. (#4762) * [DevTools] Allowed to take a screenshot of the currently selected control. * feat(DevTools): Allowed to customize screenshot root folder. * feat(DevTools): Allowed screenshot filename by convention * fixes(DevTools): Mark Shot as async and catch eventualy RenderTo excepion * fixes: allignment of code comment * fixes: CS0173 when build on linux and macOS * Refactoring Shot method * fixes: typo in DefaultScreenshotRoot * fixes: assembly detection * fixes: code comment * fixes: nullable warnings * fixes: merge issue * fixes: removed unnecessary using * fixes: removed Dispatcher.UIThread.InvokeAsync * fixes: unexpected to the user results after call RenderTo * fixes: screenshot Clip * fixes: typo * fixes: remove workaraund * Apply suggestions from code review * feat(DevTools): Add Shot Button * feat(DevTools): Custom Screenshot handler * fixes(DevTools): Renamed Screenshots.FileHandler to Screenshots.FileConvetionHandler * feat(DevTools): Screenshots.BaseRenderToStreamHandler * feat(DevTools): Screenshots.FilePickerHandler * fixes(DevTools): marked VisualExtensions as internal * fixes(DevTools): Strip out FileConvetionHandler * fixes(DevTools): FilePickerHandler promote Title and ScreenshotsRoot properties to ctor * fixes(DevTools): Remove screenshot HotKey description form status bar * fixes(DevTools): Xml Comment * fixes(DevTools): Moved Screenshot command to File menu * fixes(DevTools): Removed reference to Avalonia.Dialogs Co-authored-by: Giuseppe Lippolis Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com> Co-authored-by: Max Katz --- .../Diagnostics/Convetions.cs | 17 ++++ .../Diagnostics/DevToolsOptions.cs | 10 ++- .../Diagnostics/IScreenshotHandler.cs | 17 ++++ .../Screenshots/BaseRenderToStreamHandler.cs | 29 +++++++ .../Screenshots/FilePickerHandler.cs | 85 +++++++++++++++++++ .../Diagnostics/ViewModels/MainViewModel.cs | 34 ++++++++ .../Diagnostics/Views/MainView.xaml | 30 +++++++ .../Diagnostics/Views/MainWindow.xaml | 4 +- .../Diagnostics/VisualExtensions.cs | 71 ++++++++++++++++ 9 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Convetions.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/IScreenshotHandler.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs diff --git a/src/Avalonia.Diagnostics/Diagnostics/Convetions.cs b/src/Avalonia.Diagnostics/Diagnostics/Convetions.cs new file mode 100644 index 0000000000..b41755f239 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Convetions.cs @@ -0,0 +1,17 @@ +using System; +using System.Reflection; +using Avalonia.Controls; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics +{ + static class Convetions + { + public static string DefaultScreenshotsRoot => + System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures, Environment.SpecialFolderOption.Create), + "Screenshots"); + + public static IScreenshotHandler DefaultScreenshotHandler { get; } = + new Screenshots.FilePickerHandler(); + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index 4181989388..33166f1147 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -1,4 +1,5 @@ -using Avalonia.Input; +using System; +using Avalonia.Input; namespace Avalonia.Diagnostics { @@ -28,5 +29,12 @@ namespace Avalonia.Diagnostics /// Get or set the startup screen index where the DevTools window will be displayed. /// public int? StartupScreenIndex { get; set; } + + /// + /// Allow to customizze SreenshotHandler + /// + /// Default handler is + public IScreenshotHandler ScreenshotHandler { get; set; } + = Convetions.DefaultScreenshotHandler; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/IScreenshotHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/IScreenshotHandler.cs new file mode 100644 index 0000000000..3503ab56b3 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/IScreenshotHandler.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace Avalonia.Diagnostics +{ + /// + /// Allowed to define custom handler for Shreeshot + /// + public interface IScreenshotHandler + { + /// + /// Handle the Screenshot + /// + /// + Task Take(IControl control); + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs new file mode 100644 index 0000000000..c111b045d3 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace Avalonia.Diagnostics.Screenshots +{ + /// + /// Base class for render Screenshto to stream + /// + public abstract class BaseRenderToStreamHandler : IScreenshotHandler + { + + /// + /// Get stream + /// + /// + /// stream to render the control + protected abstract Task GetStream(IControl control); + + public async Task Take(IControl control) + { + using var output = await GetStream(control); + if (output is { }) + { + control.RenderTo(output); + await output.FlushAsync(); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs new file mode 100644 index 0000000000..fb57058ae9 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Lifetimes = Avalonia.Controls.ApplicationLifetimes; + +namespace Avalonia.Diagnostics.Screenshots +{ + /// + /// Show a FileSavePicker to select where save screenshot + /// + public sealed class FilePickerHandler : BaseRenderToStreamHandler + { + /// + /// Instance FilePickerHandler + /// + public FilePickerHandler() + { + + } + /// + /// Instance FilePickerHandler with specificated parameter + /// + /// SaveFilePicker Title + /// + public FilePickerHandler(string? title + , string? screenshotRoot = default + ) + { + if (title is { }) + Title = title; + if (screenshotRoot is { }) + ScreenshotsRoot = screenshotRoot; + } + /// + /// Get the root folder where screeshots well be stored. + /// The default root folder is [Environment.SpecialFolder.MyPictures]/Screenshots. + /// + public string ScreenshotsRoot { get; } + = Convetions.DefaultScreenshotsRoot; + + /// + /// SaveFilePicker Title + /// + public string Title { get; } = "Save Screenshot to ..."; + + Window GetWindow(IControl control) + { + var window = control.VisualRoot as Window; + var app = Application.Current; + if (app?.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime desktop) + { + window = desktop.Windows.FirstOrDefault(w => w is Views.MainWindow); + } + return window!; + } + + protected async override Task GetStream(IControl control) + { + Stream? output = default; + var result = await new SaveFileDialog() + { + Title = Title, + Filters = new() { new FileDialogFilter() { Name = "PNG", Extensions = new() { "png" } } }, + Directory = ScreenshotsRoot, + }.ShowAsync(GetWindow(control)); + if (!string.IsNullOrWhiteSpace(result)) + { + var foldler = Path.GetDirectoryName(result); + // Directory information for path, or null if path denotes a root directory or is + // null. Returns System.String.Empty if path does not contain directory information. + if (!string.IsNullOrWhiteSpace(foldler)) + { + if (!Directory.Exists(foldler)) + { + Directory.CreateDirectory(foldler); + } + output = new FileStream(result, FileMode.Create); + } + } + return output; + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 8bc13a9525..c96ddf129e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,8 +1,11 @@ using System; using System.ComponentModel; +using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; +using Avalonia.Metadata; using Avalonia.Threading; using System.Reactive.Linq; using System.Linq; @@ -26,7 +29,9 @@ namespace Avalonia.Diagnostics.ViewModels private bool _freezePopups; private string? _pointerOverElementName; private IInputRoot? _pointerOverRoot; + private IScreenshotHandler? _screenshotHandler; private bool _showPropertyType; + public MainViewModel(AvaloniaObject root) { _root = root; @@ -285,9 +290,38 @@ namespace Avalonia.Diagnostics.ViewModels } public int? StartupScreenIndex { get; private set; } = default; + + [DependsOn(nameof(TreePageViewModel.SelectedNode))] + [DependsOn(nameof(Content))] + bool CanShot(object? parameter) + { + return Content is TreePageViewModel tree + && tree.SelectedNode != null + && tree.SelectedNode.Visual is VisualTree.IVisual visual + && visual.VisualRoot != null; + } + + async void Shot(object? parameter) + { + if ((Content as TreePageViewModel)?.SelectedNode?.Visual is IControl control + && _screenshotHandler is { } + ) + { + try + { + await _screenshotHandler.Take(control); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex.Message); + //TODO: Notify error + } + } + } public void SetOptions(DevToolsOptions options) { + _screenshotHandler = options.ScreenshotHandler; StartupScreenIndex = options.StartupScreenIndex; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 381fc68cf3..91c8eb0983 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -6,6 +6,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 70d70f0b79..45bfe5ff81 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -18,6 +18,8 @@ - + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs new file mode 100644 index 0000000000..a80ed2d4ef --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Media.Imaging; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics +{ + internal static class VisualExtensions + { + /// + /// Render control to the destination stream. + /// + /// Control to be rendered. + /// Destination stream. + /// Dpi quality. + public static void RenderTo(this IControl source, Stream destination, double dpi = 96) + { + if (source.TransformedBounds == null) + { + return; + } + var rect = source.TransformedBounds.Value.Clip; + var top = rect.TopLeft; + var pixelSize = new PixelSize((int)rect.Width, (int)rect.Height); + var dpiVector = new Vector(dpi, dpi); + + // get Visual root + var root = (source.VisualRoot + ?? source.GetVisualRoot()) + as IControl ?? source; + + IDisposable? clipSetter = default; + IDisposable? clipToBoundsSetter = default; + IDisposable? renderTransformOriginSetter = default; + IDisposable? renderTransformSetter = default; + try + { + // Set clip region + var clipRegion = new Media.RectangleGeometry(rect); + clipToBoundsSetter = root.SetValue(Visual.ClipToBoundsProperty, true, BindingPriority.Animation); + clipSetter = root.SetValue(Visual.ClipProperty, clipRegion, BindingPriority.Animation); + + // Translate origin + renderTransformOriginSetter = root.SetValue(Visual.RenderTransformOriginProperty, + new RelativePoint(top, RelativeUnit.Absolute), + BindingPriority.Animation); + + renderTransformSetter = root.SetValue(Visual.RenderTransformProperty, + new Media.TranslateTransform(-top.X, -top.Y), + BindingPriority.Animation); + + using (var bitmap = new RenderTargetBitmap(pixelSize, dpiVector)) + { + bitmap.Render(root); + bitmap.Save(destination); + } + } + finally + { + // Restore values before trasformation + renderTransformSetter?.Dispose(); + renderTransformOriginSetter?.Dispose(); + clipSetter?.Dispose(); + clipToBoundsSetter?.Dispose(); + source?.InvalidateVisual(); + } + } + } +} From 3d2c7bc242cd5a546d4b834fc58eee862c395594 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 30 Dec 2021 10:32:39 +0000 Subject: [PATCH 53/86] Ensure Alpha is set for WGL display --- src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs index dc3c16b4a3..bc27589689 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -95,6 +95,7 @@ namespace Avalonia.Win32.OpenGl WGL_DOUBLE_BUFFER_ARB, 1, WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB, WGL_COLOR_BITS_ARB, 32, + WGL_ALPHA_BITS_ARB, 8, WGL_DEPTH_BITS_ARB, 0, WGL_STENCIL_BITS_ARB, 0, 0, // End From 6e5569a8d9a2a9f55b14d6354966c72805cbea4f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Dec 2021 12:52:55 +0000 Subject: [PATCH 54/86] Update azure-pipelines.yml for Azure Pipelines --- azure-pipelines.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 497402fe4b..ce27b130f5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -150,3 +150,9 @@ jobs: artifactName: 'Samples' condition: succeeded() + - task: GitHubComment@0 + displayName: 'Comment Package Version' + inputs: + gitHubConnection: 'GithubComments' + repositoryName: '$(Build.Repository.Name)' + comment: 'You can test this PR using the following package version. 0.10.999-cibuild00$(Build.BuildId)-beta. (feed url: https://nuget.avaloniaui.net/repository/avalonia-all/index.json)' \ No newline at end of file From 9e4d3fa8f23758255970285849238aa05174a341 Mon Sep 17 00:00:00 2001 From: avaloniaui-team <95628075+avaloniaui-team@users.noreply.github.com> Date: Thu, 30 Dec 2021 17:09:53 +0000 Subject: [PATCH 55/86] Update azure-pipelines.yml --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ce27b130f5..05b9a5219e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -155,4 +155,4 @@ jobs: inputs: gitHubConnection: 'GithubComments' repositoryName: '$(Build.Repository.Name)' - comment: 'You can test this PR using the following package version. 0.10.999-cibuild00$(Build.BuildId)-beta. (feed url: https://nuget.avaloniaui.net/repository/avalonia-all/index.json)' \ No newline at end of file + comment: 'You can test this PR using the following package version. `0.10.999-cibuild00$(Build.BuildId)-beta`. (feed url: https://nuget.avaloniaui.net/repository/avalonia-all/index.json)' From 878397978b9b8a25a1c26abc45b8000b2f0e7aff Mon Sep 17 00:00:00 2001 From: abdesol Date: Thu, 30 Dec 2021 20:49:13 +0300 Subject: [PATCH 56/86] Added Progress text to ProgressBar.xaml from Avalonia.Themes.Fluent --- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index 7ad75588e1..a8bca77d3e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -21,6 +21,7 @@ + @@ -29,7 +30,11 @@ - + + + + + From 1dfcce032e42595efbd2b549a20ee5df13aadf5d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 31 Dec 2021 00:42:36 +0000 Subject: [PATCH 57/86] Merge pull request #7291 from AvaloniaUI/features/comment-build-pr Features/comment build pr --- azure-pipelines.yml | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 05b9a5219e..40669f4f53 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,6 +2,33 @@ variables: MSBuildEnableWorkloadResolver: 'false' jobs: + +- job: GetPRNumber + pool: + vmImage: 'windows-2022' + variables: + SolutionDir: '$(Build.SourcesDirectory)' + steps: + + - task: PowerShell@2 + displayName: Get PR Number + inputs: + targetType: 'inline' + script: | + $prId = $env:System_PullRequest_PullRequestNumber + Write-Host "PR Number is:-" $env:System_PullRequest_PullRequestNumber + + if (!([string]::IsNullOrWhiteSpace($prId))) + { + Set-Content -Path $env:Build_ArtifactStagingDirectory\prId.txt -Value $prId + } + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'PRNumber' + publishLocation: 'Container' + - job: Linux pool: vmImage: 'ubuntu-20.04' @@ -150,9 +177,3 @@ jobs: artifactName: 'Samples' condition: succeeded() - - task: GitHubComment@0 - displayName: 'Comment Package Version' - inputs: - gitHubConnection: 'GithubComments' - repositoryName: '$(Build.Repository.Name)' - comment: 'You can test this PR using the following package version. `0.10.999-cibuild00$(Build.BuildId)-beta`. (feed url: https://nuget.avaloniaui.net/repository/avalonia-all/index.json)' From 7ef5296898357b18164d3137341a9ad98451ed79 Mon Sep 17 00:00:00 2001 From: abdesol Date: Fri, 31 Dec 2021 07:00:22 +0300 Subject: [PATCH 58/86] Changed progress text foreground to template binding of the progress bar foreground --- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index a8bca77d3e..e971eb46f2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -21,7 +21,6 @@ - @@ -30,11 +29,10 @@ + + + - - - - From d8c89df26b850eb57cf29e7b3c993cc2993554eb Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sat, 1 Jan 2022 10:32:24 +0300 Subject: [PATCH 59/86] [WASM] Cursors --- .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 1 + src/Web/Avalonia.Web.Blazor/Cursor.cs | 92 +++++++++++++++++++ .../RazorViewTopLevelImpl.cs | 9 +- src/Web/Avalonia.Web.Blazor/WinStubs.cs | 21 ----- .../Avalonia.Web.Blazor/WindowingPlatform.cs | 2 +- 5 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 src/Web/Avalonia.Web.Blazor/Cursor.cs diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index bc2a2f3103..a682a25915 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -257,6 +257,7 @@ namespace Avalonia.Web.Blazor _inputHelper.Hide(); _inputHelper.SetCursor("default"); + _topLevelImpl.SetCssCursor = _inputHelper.SetCursor; Console.WriteLine("starting html canvas setup"); _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web.Blazor/Cursor.cs b/src/Web/Avalonia.Web.Blazor/Cursor.cs new file mode 100644 index 0000000000..cd9010dfc2 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Cursor.cs @@ -0,0 +1,92 @@ +using Avalonia.Input; +using Avalonia.Platform; + +namespace Avalonia.Web.Blazor +{ + public class CssCursor : ICursorImpl + { + public string? Value { get; set; } + + public CssCursor(StandardCursorType type) + { + Value = ToKeyword(type); + } + + /// + /// Create a cursor from base64 image + /// + public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback) + { + Value = $"url(data:image/{format},{base64}) {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; + } + + /// + /// Create a cursor from url to *.cur file. + /// + public CssCursor(string url, StandardCursorType fallback) + { + Value = $"url('{url}'), {ToKeyword(fallback)}"; + } + + /// + /// Create a cursor from png/svg and hotspot position + /// + public CssCursor(string url, PixelPoint hotSpot, StandardCursorType fallback) + { + Value = $"url('{url}') {hotSpot.X} {hotSpot.Y}, {ToKeyword(fallback)}"; + } + + private static string ToKeyword(StandardCursorType type) => type switch + { + StandardCursorType.Hand => "pointer", + StandardCursorType.Cross => "crosshair", + StandardCursorType.Help => "help", + StandardCursorType.Ibeam => "text", + StandardCursorType.No => "not-allowed", + StandardCursorType.None => "none", + StandardCursorType.Wait => "progress", + StandardCursorType.AppStarting => "wait", + + StandardCursorType.DragMove => "move", + StandardCursorType.DragCopy => "copy", + StandardCursorType.DragLink => "alias", + + StandardCursorType.UpArrow => "default",/*not found matching one*/ + StandardCursorType.SizeWestEast => "ew-resize", + StandardCursorType.SizeNorthSouth => "ns-resize", + StandardCursorType.SizeAll => "move", + + StandardCursorType.TopSide => "n-resize", + StandardCursorType.BottomSide => "s-resize", + StandardCursorType.LeftSide => "w-resize", + StandardCursorType.RightSide => "e-resize", + StandardCursorType.TopLeftCorner => "nw-resize", + StandardCursorType.TopRightCorner => "ne-resize", + StandardCursorType.BottomLeftCorner => "sw-resize", + StandardCursorType.BottomRightCorner => "se-resize", + + _ => "default", + }; + + public void Dispose() {} + } + + internal class CssCursorFactory : ICursorFactory + { + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + using var imageStream = new MemoryStream(); + cursor.Save(imageStream); + + //not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser. + var base64String = Convert.ToBase64String(imageStream.ToArray()); + return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow); + } + + ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) + { + return new CssCursor(cursorType); + } + } +} + diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index acd23a98b0..1b1fca7e26 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -126,8 +126,12 @@ namespace Avalonia.Web.Blazor public void SetCursor(ICursorImpl cursor) { - // nop - + var cur = cursor as CssCursor; + if (cur == null || cur.Value == null) + { + throw new NotSupportedException(); + } + SetCssCursor?.Invoke(cur.Value); } public IPopupImpl? CreatePopup() @@ -146,6 +150,7 @@ namespace Avalonia.Web.Blazor public IEnumerable Surfaces => new object[] { _currentSurface! }; + public Action? SetCssCursor { get; set; } public Action? Input { get; set; } public Action? Paint { get; set; } public Action? Resized { get; set; } diff --git a/src/Web/Avalonia.Web.Blazor/WinStubs.cs b/src/Web/Avalonia.Web.Blazor/WinStubs.cs index 13d83a9ee3..7b2bff6bfd 100644 --- a/src/Web/Avalonia.Web.Blazor/WinStubs.cs +++ b/src/Web/Avalonia.Web.Blazor/WinStubs.cs @@ -23,27 +23,6 @@ namespace Avalonia.Web.Blazor public Task GetDataAsync(string format) => Task.FromResult(new ()); } - internal class CursorStub : ICursorImpl - { - public void Dispose() - { - - } - } - - internal class CursorFactoryStub : ICursorFactory - { - public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) - { - return new CursorStub(); - } - - ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) - { - return new CursorStub(); - } - } - internal class IconLoaderStub : IPlatformIconLoader { private class IconStub : IWindowIconImpl diff --git a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs index 979e66a785..ac970d067f 100644 --- a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs @@ -35,7 +35,7 @@ namespace Avalonia.Web.Blazor s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable .Bind().ToSingleton() - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(s_keyboard) .Bind().ToConstant(instance) .Bind().ToConstant(instance) From e9d861bc6659640d9e61797ad8ddc65298d68c14 Mon Sep 17 00:00:00 2001 From: Abdella Solomon <69455299+Abdesol@users.noreply.github.com> Date: Sat, 1 Jan 2022 12:45:54 +0300 Subject: [PATCH 60/86] Changed text foreground of progressbar from template binding to SystemControlForegroundBaseHighBrush --- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml index e971eb46f2..48b450a338 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml @@ -30,7 +30,7 @@ - + From 100af8ffe307ba7664c3f8c0adfe695f62fab4ce Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 1 Jan 2022 12:46:26 +0300 Subject: [PATCH 61/86] [WASM] fix cursors. works fine --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 6 ++++-- src/Web/Avalonia.Web.Blazor/Cursor.cs | 5 +++-- src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 11 ++++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index a682a25915..4b0ada1f27 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -22,6 +22,7 @@ namespace Avalonia.Web.Blazor private DpiWatcherInterop _dpiWatcher = null!; private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null!; private InputHelperInterop _inputHelper = null!; + private InputHelperInterop _canvasHelper = null!; private ElementReference _htmlCanvas; private ElementReference _inputElement; private double _dpi; @@ -254,10 +255,11 @@ namespace Avalonia.Web.Blazor Threading.Dispatcher.UIThread.Post(async () => { _inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement); + _canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas); _inputHelper.Hide(); - _inputHelper.SetCursor("default"); - _topLevelImpl.SetCssCursor = _inputHelper.SetCursor; + _canvasHelper.SetCursor("default"); + _topLevelImpl.SetCssCursor = _canvasHelper.SetCursor; Console.WriteLine("starting html canvas setup"); _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web.Blazor/Cursor.cs b/src/Web/Avalonia.Web.Blazor/Cursor.cs index cd9010dfc2..d921b2fa6c 100644 --- a/src/Web/Avalonia.Web.Blazor/Cursor.cs +++ b/src/Web/Avalonia.Web.Blazor/Cursor.cs @@ -5,6 +5,7 @@ namespace Avalonia.Web.Blazor { public class CssCursor : ICursorImpl { + public const string Default = "default"; public string? Value { get; set; } public CssCursor(StandardCursorType type) @@ -17,7 +18,7 @@ namespace Avalonia.Web.Blazor /// public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback) { - Value = $"url(data:image/{format},{base64}) {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; + Value = $"url(\"data:image/{format};base64,{base64}\") {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; } /// @@ -65,7 +66,7 @@ namespace Avalonia.Web.Blazor StandardCursorType.BottomLeftCorner => "sw-resize", StandardCursorType.BottomRightCorner => "se-resize", - _ => "default", + _ => Default, }; public void Dispose() {} diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index 1b1fca7e26..ac5f5044d8 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -21,6 +21,7 @@ namespace Avalonia.Web.Blazor private readonly Stopwatch _sw = Stopwatch.StartNew(); private readonly ITextInputMethodImpl _textInputMethod; private readonly TouchDevice _touchDevice; + private string _currentCursor = CssCursor.Default; public RazorViewTopLevelImpl(ITextInputMethodImpl textInputMethod) { @@ -127,11 +128,15 @@ namespace Avalonia.Web.Blazor public void SetCursor(ICursorImpl cursor) { var cur = cursor as CssCursor; - if (cur == null || cur.Value == null) + var val = CssCursor.Default; + if (cur != null && cur.Value != null) { - throw new NotSupportedException(); + val = cur.Value; + } + if (_currentCursor != val) + { + SetCssCursor?.Invoke(val); } - SetCssCursor?.Invoke(cur.Value); } public IPopupImpl? CreatePopup() From e7a36d011fd9b222ad1f762d822e0b74772aecd5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 1 Jan 2022 19:48:16 +0000 Subject: [PATCH 62/86] fix controlcatalog.web --- samples/ControlCatalog.Web/ControlCatalog.Web.csproj | 1 + samples/ControlCatalog/App.xaml.cs | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj index d463dfa84a..199fa85ad2 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -2,6 +2,7 @@ net6.0 enable + True diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 816356bee9..505f486a6d 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -89,16 +89,18 @@ namespace ControlCatalog if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { desktopLifetime.MainWindow = new MainWindow(); + + this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() + { + StartupScreenIndex = 1, + }); } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + { singleViewLifetime.MainView = new MainView(); + } base.OnFrameworkInitializationCompleted(); - - this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() - { - StartupScreenIndex = 1, - }); } } } From c51086401454b08895164525fb3aad99f88ac6bb Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sun, 2 Jan 2022 05:20:39 +0300 Subject: [PATCH 63/86] [WASM] Fix cursors in macOS, fix default cursor set logic --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 6 +++++- src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 8 ++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 4b0ada1f27..7644514687 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -259,7 +259,11 @@ namespace Avalonia.Web.Blazor _inputHelper.Hide(); _canvasHelper.SetCursor("default"); - _topLevelImpl.SetCssCursor = _canvasHelper.SetCursor; + _topLevelImpl.SetCssCursor = x => + { + _inputHelper.SetCursor(x);//macOS + _canvasHelper.SetCursor(x);//windows + }; Console.WriteLine("starting html canvas setup"); _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index ac5f5044d8..1d667c0f0c 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -127,15 +127,11 @@ namespace Avalonia.Web.Blazor public void SetCursor(ICursorImpl cursor) { - var cur = cursor as CssCursor; - var val = CssCursor.Default; - if (cur != null && cur.Value != null) - { - val = cur.Value; - } + var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; if (_currentCursor != val) { SetCssCursor?.Invoke(val); + _currentCursor = val; } } From 509b9d8f09eb1928608fe75354a43b6a11e0bf2c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Jan 2022 11:50:43 +0100 Subject: [PATCH 64/86] Add additional null checks to WindowBaseImpl. Fixes #7231. --- src/Avalonia.Native/WindowImplBase.cs | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 4a3baa2788..87b7a7608e 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -28,18 +28,18 @@ namespace Avalonia.Native public string HandleDescriptor => "NSWindow"; - public IntPtr NSView => _native.ObtainNSViewHandle(); + public IntPtr NSView => _native?.ObtainNSViewHandle() ?? IntPtr.Zero; - public IntPtr NSWindow => _native.ObtainNSWindowHandle(); + public IntPtr NSWindow => _native?.ObtainNSWindowHandle() ?? IntPtr.Zero; public IntPtr GetNSViewRetained() { - return _native.ObtainNSViewHandleRetained(); + return _native?.ObtainNSViewHandleRetained() ?? IntPtr.Zero; } public IntPtr GetNSWindowRetained() { - return _native.ObtainNSWindowHandleRetained(); + return _native?.ObtainNSWindowHandleRetained() ?? IntPtr.Zero; } } @@ -260,7 +260,7 @@ namespace Avalonia.Native public void Activate() { - _native.Activate(); + _native?.Activate(); } public bool RawTextInputEvent(uint timeStamp, string text) @@ -322,7 +322,7 @@ namespace Avalonia.Native public void Resize(Size clientSize, PlatformResizeReason reason) { - _native.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); + _native?.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); } public IRenderer CreateRenderer(IRenderRoot root) @@ -367,14 +367,14 @@ namespace Avalonia.Native public virtual void Show(bool activate, bool isDialog) { - _native.Show(activate.AsComBool(), isDialog.AsComBool()); + _native?.Show(activate.AsComBool(), isDialog.AsComBool()); } public PixelPoint Position { - get => _native.Position.ToAvaloniaPixelPoint(); - set => _native.SetPosition(value.ToAvnPoint()); + get => _native?.Position.ToAvaloniaPixelPoint() ?? default; + set => _native?.SetPosition(value.ToAvnPoint()); } public Point PointToClient(PixelPoint point) @@ -389,12 +389,12 @@ namespace Avalonia.Native public void Hide() { - _native.Hide(); + _native?.Hide(); } public void BeginMoveDrag(PointerPressedEventArgs e) { - _native.BeginMoveDrag(); + _native?.BeginMoveDrag(); } public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(1)) @@ -402,7 +402,7 @@ namespace Avalonia.Native public void SetTopmost(bool value) { - _native.SetTopMost(value.AsComBool()); + _native?.SetTopMost(value.AsComBool()); } public double RenderScaling => _native?.Scaling ?? 1; @@ -438,7 +438,7 @@ namespace Avalonia.Native public void SetMinMaxSize(Size minSize, Size maxSize) { - _native.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); + _native?.SetMinMaxSize(minSize.ToAvnSize(), maxSize.ToAvnSize()); } public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) @@ -449,7 +449,7 @@ namespace Avalonia.Native internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard, IAvnDndResultCallback callback, IntPtr sourceHandle) { - _native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); + _native?.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); } public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) From e6c08a13d70f7173494b83489806e2e1f10b442d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:04:06 +0300 Subject: [PATCH 65/86] Added reflection-free API for weak events --- .../NotifyCollectionChangedExtensions.cs | 21 +-- .../Data/Core/IndexerNodeBase.cs | 10 +- .../Core/Plugins/IndeiValidationPlugin.cs | 20 +- .../Plugins/InpcPropertyAccessorPlugin.cs | 24 +-- .../Utilities/IWeakEventSubscriber.cs | 12 ++ src/Avalonia.Base/Utilities/WeakEvent.cs | 175 ++++++++++++++++++ src/Avalonia.Base/Utilities/WeakEvents.cs | 40 ++++ src/Avalonia.Base/Utilities/WeakObservable.cs | 35 +++- .../Utilities/WeakSubscriptionManager.cs | 1 + src/Avalonia.Controls/NativeMenuItem.cs | 10 +- .../Repeater/ItemsRepeater.cs | 32 ++-- src/Avalonia.Controls/TopLevel.cs | 15 +- .../Utils/CollectionChangedEventManager.cs | 15 +- src/Avalonia.Layout/AttachedLayout.cs | 17 ++ .../PropertyInfoAccessorFactory.cs | 36 +--- .../Avalonia.Base.UnitTests/WeakEventTests.cs | 75 ++++++++ 16 files changed, 421 insertions(+), 117 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs create mode 100644 src/Avalonia.Base/Utilities/WeakEvent.cs create mode 100644 src/Avalonia.Base/Utilities/WeakEvents.cs create mode 100644 tests/Avalonia.Base.UnitTests/WeakEventTests.cs diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index dcd32ddd76..689fcc89a4 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -59,7 +59,7 @@ namespace Avalonia.Collections } private class WeakCollectionChangedObservable : LightweightObservableBase, - IWeakSubscriber + IWeakEventSubscriber { private WeakReference _sourceReference; @@ -68,31 +68,22 @@ namespace Avalonia.Collections _sourceReference = source; } - public void OnEvent(object? sender, NotifyCollectionChangedEventArgs e) + public void OnEvent(object? sender, + WeakEvent ev, + NotifyCollectionChangedEventArgs e) { PublishNext(e); } - protected override void Initialize() { if (_sourceReference.TryGetTarget(out var instance)) - { - WeakSubscriptionManager.Subscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakEvents.CollectionChanged.Subscribe(instance, this); } protected override void Deinitialize() { if (_sourceReference.TryGetTarget(out var instance)) - { - WeakSubscriptionManager.Unsubscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakEvents.CollectionChanged.Unsubscribe(instance, this); } } } diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index e197e29103..a808827896 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -23,18 +23,16 @@ namespace Avalonia.Data.Core if (incc != null) { - inputs.Add(WeakObservable.FromEventPattern( - incc, - nameof(incc.CollectionChanged)) + inputs.Add(WeakObservable.FromEventPattern( + incc, WeakEvents.CollectionChanged) .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Select(_ => GetValue(target))); } if (inpc != null) { - inputs.Add(WeakObservable.FromEventPattern( - inpc, - nameof(inpc.PropertyChanged)) + inputs.Add(WeakObservable.FromEventPattern( + inpc, WeakEvents.PropertyChanged) .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) .Select(_ => GetValue(target))); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 9f827daf94..1e7a0d5c8f 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -11,6 +11,12 @@ namespace Avalonia.Data.Core.Plugins /// public class IndeiValidationPlugin : IDataValidationPlugin { + private static readonly WeakEvent + ErrorsChangedWeakEvent = WeakEvent.Register( + (s, h) => s.ErrorsChanged += h, + (s, h) => s.ErrorsChanged -= h + ); + /// public bool Match(WeakReference reference, string memberName) { @@ -25,7 +31,7 @@ namespace Avalonia.Data.Core.Plugins return new Validator(reference, name, accessor); } - private class Validator : DataValidationBase, IWeakSubscriber + private class Validator : DataValidationBase, IWeakEventSubscriber { private readonly WeakReference _reference; private readonly string _name; @@ -37,7 +43,7 @@ namespace Avalonia.Data.Core.Plugins _name = name; } - void IWeakSubscriber.OnEvent(object? sender, DataErrorsChangedEventArgs e) + void IWeakEventSubscriber.OnEvent(object? notifyDataErrorInfo, WeakEvent ev, DataErrorsChangedEventArgs e) { if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) { @@ -51,10 +57,7 @@ namespace Avalonia.Data.Core.Plugins if (target != null) { - WeakSubscriptionManager.Subscribe( - target, - nameof(target.ErrorsChanged), - this); + ErrorsChangedWeakEvent.Subscribe(target, this); } base.SubscribeCore(); @@ -66,10 +69,7 @@ namespace Avalonia.Data.Core.Plugins if (target != null) { - WeakSubscriptionManager.Unsubscribe( - target, - nameof(target.ErrorsChanged), - this); + ErrorsChangedWeakEvent.Unsubscribe(target, this); } base.UnsubscribeCore(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index fd532f3014..33cecd10a7 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.ComponentModel; using System.Reflection; using Avalonia.Utilities; @@ -85,7 +86,7 @@ namespace Avalonia.Data.Core.Plugins return found; } - private class Accessor : PropertyAccessorBase, IWeakSubscriber + private class Accessor : PropertyAccessorBase, IWeakEventSubscriber { private readonly WeakReference _reference; private readonly PropertyInfo _property; @@ -129,7 +130,8 @@ namespace Avalonia.Data.Core.Plugins return false; } - void IWeakSubscriber.OnEvent(object? sender, PropertyChangedEventArgs e) + void IWeakEventSubscriber. + OnEvent(object? notifyPropertyChanged, WeakEvent ev, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { @@ -148,13 +150,8 @@ namespace Avalonia.Data.Core.Plugins { var inpc = GetReferenceTarget() as INotifyPropertyChanged; - if (inpc != null) - { - WeakSubscriptionManager.Unsubscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } + if (inpc != null) + WeakEvents.PropertyChanged.Unsubscribe(inpc, this); } private object? GetReferenceTarget() @@ -178,13 +175,8 @@ namespace Avalonia.Data.Core.Plugins { var inpc = GetReferenceTarget() as INotifyPropertyChanged; - if (inpc != null) - { - WeakSubscriptionManager.Subscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } + if (inpc != null) + WeakEvents.PropertyChanged.Subscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs new file mode 100644 index 0000000000..e48c0cb111 --- /dev/null +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Utilities; + +/// +/// Defines a listener to a event subscribed vis the . +/// +/// The type of the event arguments. +public interface IWeakEventSubscriber where TEventArgs : EventArgs +{ + void OnEvent(object? sender, WeakEvent ev, TEventArgs e); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs new file mode 100644 index 0000000000..430bc92838 --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities; + +/// +/// Manages subscriptions to events using weak listeners. +/// +public class WeakEvent : WeakEvent where TEventArgs : EventArgs where TSender : class +{ + private readonly Func, Action> _subscribe; + + readonly ConditionalWeakTable _subscriptions = new(); + + internal WeakEvent( + Action> subscribe, + Action> unsubscribe) + { + _subscribe = (t, s) => + { + subscribe(t, s); + return () => unsubscribe(t, s); + }; + } + + internal WeakEvent(Func, Action> subscribe) + { + _subscribe = subscribe; + } + + public void Subscribe(TSender target, IWeakEventSubscriber subscriber) + { + if (!_subscriptions.TryGetValue(target, out var subscription)) + _subscriptions.Add(target, subscription = new Subscription(this, target)); + subscription.Add(new WeakReference>(subscriber)); + } + + public void Unsubscribe(TSender target, IWeakEventSubscriber subscriber) + { + if (_subscriptions.TryGetValue(target, out var subscription)) + subscription.Remove(subscriber); + } + + private class Subscription + { + private readonly WeakEvent _ev; + private readonly TSender _target; + + private WeakReference>?[] _data = + new WeakReference>[16]; + private int _count; + private readonly Action _unsubscribe; + + public Subscription(WeakEvent ev, TSender target) + { + _ev = ev; + _target = target; + + _unsubscribe = ev._subscribe(target, OnEvent); + } + + void Destroy() + { + _unsubscribe(); + _ev._subscriptions.Remove(_target); + } + + public void Add(WeakReference> s) + { + if (_count == _data.Length) + { + //Extend capacity + var extendedData = new WeakReference>?[_data.Length * 2]; + Array.Copy(_data, extendedData, _data.Length); + _data = extendedData; + } + + _data[_count] = s; + _count++; + } + + public void Remove(IWeakEventSubscriber s) + { + var removed = false; + + for (int c = 0; c < _count; ++c) + { + var reference = _data[c]; + + if (reference != null && reference.TryGetTarget(out var instance) && instance == s) + { + _data[c] = null; + removed = true; + } + } + + if (removed) + { + Compact(); + } + } + + void Compact() + { + int empty = -1; + for (var c = 0; c < _count; c++) + { + var r = _data[c]; + //Mark current index as first empty + if (r == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r != null && empty != -1) + { + _data[c] = null; + _data[empty] = r; + empty++; + } + } + + if (empty != -1) + _count = empty; + if (_count == 0) + Destroy(); + } + + void OnEvent(object? sender, TEventArgs eventArgs) + { + var needCompact = false; + for (var c = 0; c < _count; c++) + { + var r = _data[c]; + if (r?.TryGetTarget(out var sub) == true) + sub!.OnEvent(_target, _ev, eventArgs); + else + needCompact = true; + } + + if (needCompact) + Compact(); + } + } + +} + +public class WeakEvent +{ + public static WeakEvent Register( + Action> subscribe, + Action> unsubscribe) where TSender : class where TEventArgs : EventArgs + { + return new WeakEvent(subscribe, unsubscribe); + } + + public static WeakEvent Register( + Func, Action> subscribe) where TSender : class where TEventArgs : EventArgs + { + return new WeakEvent(subscribe); + } + + public static WeakEvent Register( + Action subscribe, + Action unsubscribe) where TSender : class + { + return Register((s, h) => + { + EventHandler handler = (_, e) => h(s, e); + subscribe(s, handler); + return () => unsubscribe(s, handler); + }); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakEvents.cs b/src/Avalonia.Base/Utilities/WeakEvents.cs new file mode 100644 index 0000000000..d1b5e7f12d --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEvents.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Windows.Input; + +namespace Avalonia.Utilities; + +public class WeakEvents +{ + /// + /// Represents CollectionChanged event from + /// + public static readonly WeakEvent + CollectionChanged = WeakEvent.Register( + (c, s) => + { + NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e); + c.CollectionChanged += handler; + return () => c.CollectionChanged -= handler; + }); + + /// + /// Represents PropertyChanged event from + /// + public static readonly WeakEvent + PropertyChanged = WeakEvent.Register( + (s, h) => + { + PropertyChangedEventHandler handler = (_, e) => h(s, e); + s.PropertyChanged += handler; + return () => s.PropertyChanged -= handler; + }); + + /// + /// Represents CanExecuteChanged event from + /// + public static readonly WeakEvent CommandCanExecuteChanged = + WeakEvent.Register((s, h) => s.CanExecuteChanged += h, + (s, h) => s.CanExecuteChanged -= h); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/WeakObservable.cs b/src/Avalonia.Base/Utilities/WeakObservable.cs index 52edc7ad1a..6bf1d4082f 100644 --- a/src/Avalonia.Base/Utilities/WeakObservable.cs +++ b/src/Avalonia.Base/Utilities/WeakObservable.cs @@ -18,6 +18,7 @@ namespace Avalonia.Utilities /// Object instance that exposes the event to convert. /// Name of the event to convert. /// + [Obsolete("Use WeakEvent-based overload")] public static IObservable> FromEventPattern( TTarget target, string eventName) @@ -34,7 +35,9 @@ namespace Avalonia.Utilities }).Publish().RefCount(); } - private class Handler : IWeakSubscriber where TEventArgs : EventArgs + private class Handler + : IWeakSubscriber, + IWeakEventSubscriber where TEventArgs : EventArgs { private IObserver> _observer; @@ -47,6 +50,36 @@ namespace Avalonia.Utilities { _observer.OnNext(new EventPattern(sender, e)); } + + public void OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + _observer.OnNext(new EventPattern(sender, e)); + } } + + /// + /// Converts a WeakEvent conforming to the standard .NET event pattern into an observable + /// sequence, subscribing weakly. + /// + /// The type of target. + /// The type of the event args. + /// Object instance that exposes the event to convert. + /// The weak event to convert. + /// + public static IObservable> FromEventPattern( + TTarget target, WeakEvent ev) + where TEventArgs : EventArgs where TTarget : class + { + _ = target ?? throw new ArgumentNullException(nameof(target)); + _ = ev ?? throw new ArgumentNullException(nameof(ev)); + + return Observable.Create>(observer => + { + var handler = new Handler(observer); + ev.Subscribe(target, handler); + return () => ev.Unsubscribe(target, handler); + }).Publish().RefCount(); + } + } } diff --git a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs index 88b1e3c807..dc9e86cc32 100644 --- a/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs +++ b/src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs @@ -19,6 +19,7 @@ namespace Avalonia.Utilities /// The event source. /// The name of the event. /// The subscriber. + [Obsolete("Use WeakEvent")] public static void Subscribe(TTarget target, string eventName, IWeakSubscriber subscriber) where TEventArgs : EventArgs { diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 2ceaeb6dba..4d048f0fb0 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls } - class CanExecuteChangedSubscriber : IWeakSubscriber + class CanExecuteChangedSubscriber : IWeakEventSubscriber { private readonly NativeMenuItem _parent; @@ -42,7 +42,7 @@ namespace Avalonia.Controls _parent = parent; } - public void OnEvent(object sender, EventArgs e) + public void OnEvent(object? sender, WeakEvent ev, EventArgs e) { _parent.CanExecuteChanged(); } @@ -160,14 +160,12 @@ namespace Avalonia.Controls set { if (_command != null) - WeakSubscriptionManager.Unsubscribe(_command, - nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber); SetAndRaise(CommandProperty, ref _command, value); if (_command != null) - WeakSubscriptionManager.Subscribe(_command, - nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber); CanExecuteChanged(); } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index ecc0fa3a48..b40cf26df5 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider + public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber { /// /// Defines the property. @@ -723,14 +723,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(AttachedLayout.MeasureInvalidated), - InvalidateMeasureForLayout); - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(AttachedLayout.ArrangeInvalidated), - InvalidateArrangeForLayout); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -748,14 +742,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - WeakEventHandlerManager.Subscribe( - newValue, - nameof(AttachedLayout.MeasureInvalidated), - InvalidateMeasureForLayout); - WeakEventHandlerManager.Subscribe( - newValue, - nameof(AttachedLayout.ArrangeInvalidated), - InvalidateArrangeForLayout); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -806,9 +794,13 @@ namespace Avalonia.Controls _viewportManager.OnBringIntoViewRequested(e); } - private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure(); - - private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange(); + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) + { + if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + InvalidateMeasure(); + } private VirtualizingLayoutContext GetLayoutContext() { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5d9a0c8eed..eaee5bdb50 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls IStyleHost, ILogicalRoot, ITextInputMethodRoot, - IWeakSubscriber + IWeakEventSubscriber { /// /// Defines the property. @@ -74,6 +74,12 @@ namespace Avalonia.Controls public static readonly StyledProperty TransparencyBackgroundFallbackProperty = AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White); + private static readonly WeakEvent + ResourcesChangedWeakEvent = WeakEvent.Register( + (s, h) => s.ResourcesChanged += h, + (s, h) => s.ResourcesChanged -= h + ); + private readonly IInputManager _inputManager; private readonly IAccessKeyHandler _accessKeyHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; @@ -178,10 +184,7 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - WeakSubscriptionManager.Subscribe( - applicationResources, - nameof(IResourceHost.ResourcesChanged), - this); + ResourcesChangedWeakEvent.Subscribe(applicationResources, this); } impl.LostFocus += PlatformImpl_LostFocus; @@ -286,7 +289,7 @@ namespace Avalonia.Controls /// IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakSubscriber.OnEvent(object sender, ResourcesChangedEventArgs e) + void IWeakEventSubscriber.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e) { ((ILogical)this).NotifyResourcesChanged(e); } diff --git a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs index 1a190391b7..74705a0262 100644 --- a/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs +++ b/src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs @@ -83,7 +83,7 @@ namespace Avalonia.Controls.Utils "Collection listener not registered for this collection/listener combination."); } - private class Entry : IWeakSubscriber, IDisposable + private class Entry : IWeakEventSubscriber, IDisposable { private INotifyCollectionChanged _collection; @@ -91,23 +91,18 @@ namespace Avalonia.Controls.Utils { _collection = collection; Listeners = new List>(); - WeakSubscriptionManager.Subscribe( - _collection, - nameof(INotifyCollectionChanged.CollectionChanged), - this); + WeakEvents.CollectionChanged.Subscribe(_collection, this); } public List> Listeners { get; } public void Dispose() { - WeakSubscriptionManager.Unsubscribe( - _collection, - nameof(INotifyCollectionChanged.CollectionChanged), - this); + WeakEvents.CollectionChanged.Unsubscribe(_collection, this); } - void IWeakSubscriber.OnEvent(object? sender, NotifyCollectionChangedEventArgs e) + void IWeakEventSubscriber. + OnEvent(object? notifyCollectionChanged, WeakEvent ev, NotifyCollectionChangedEventArgs e) { static void Notify( INotifyCollectionChanged incc, diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs index 6c884641f8..ece8bbe805 100644 --- a/src/Avalonia.Layout/AttachedLayout.cs +++ b/src/Avalonia.Layout/AttachedLayout.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using Avalonia.Utilities; namespace Avalonia.Layout { @@ -19,10 +20,26 @@ namespace Avalonia.Layout /// public event EventHandler? MeasureInvalidated; + /// + /// Occurs when the measurement state (layout) has been invalidated. + /// + public static readonly WeakEvent MeasureInvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.MeasureInvalidated += h, + (s, h) => s.MeasureInvalidated -= h); + /// /// Occurs when the arrange state (layout) has been invalidated. /// public event EventHandler? ArrangeInvalidated; + + /// + /// Occurs when the arrange state (layout) has been invalidated. + /// + public static readonly WeakEvent ArrangeInvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.ArrangeInvalidated += h, + (s, h) => s.ArrangeInvalidated -= h); /// /// Initializes any per-container state the layout requires when it is attached to an diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs index b3f78bfbe3..c21a2d4299 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -72,7 +72,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings } } - internal class InpcPropertyAccessor : PropertyAccessorBase + internal class InpcPropertyAccessor : PropertyAccessorBase, IWeakEventSubscriber { protected readonly WeakReference _reference; private readonly IPropertyInfo _property; @@ -110,7 +110,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return false; } - void OnNotifyPropertyChanged(object sender, PropertyChangedEventArgs e) + public void OnEvent(object sender, WeakEvent ev, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { @@ -128,10 +128,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) { - WeakEventHandlerManager.Unsubscribe( - inpc, - nameof(INotifyPropertyChanged.PropertyChanged), - OnNotifyPropertyChanged); + WeakEvents.PropertyChanged.Unsubscribe(inpc, this); } } @@ -148,16 +145,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings private void SubscribeToChanges() { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) - { - WeakEventHandlerManager.Subscribe( - inpc, - nameof(INotifyPropertyChanged.PropertyChanged), - OnNotifyPropertyChanged); - } + WeakEvents.PropertyChanged.Subscribe(inpc, this); } } - internal class IndexerAccessor : InpcPropertyAccessor + internal class IndexerAccessor : InpcPropertyAccessor, IWeakEventSubscriber { private int _index; @@ -172,27 +164,17 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { base.SubscribeCore(); if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) - { - WeakEventHandlerManager.Subscribe( - incc, - nameof(INotifyCollectionChanged.CollectionChanged), - OnNotifyCollectionChanged); - } + WeakEvents.CollectionChanged.Subscribe(incc, this); } protected override void UnsubscribeCore() { base.UnsubscribeCore(); if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) - { - WeakEventHandlerManager.Unsubscribe( - incc, - nameof(INotifyCollectionChanged.CollectionChanged), - OnNotifyCollectionChanged); - } + WeakEvents.CollectionChanged.Unsubscribe(incc, this); } - - void OnNotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + + public void OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs args) { if (ShouldNotifyListeners(args)) { diff --git a/tests/Avalonia.Base.UnitTests/WeakEventTests.cs b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs new file mode 100644 index 0000000000..81009f7c5f --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class WeakEventManagerTests + { + class EventSource + { + public event EventHandler Event; + + public void Fire() + { + Event?.Invoke(this, new EventArgs()); + } + + public static readonly WeakEvent WeakEvent = WeakEvent.Register( + (t, s) => t.Event += s, + (t, s) => t.Event -= s); + } + + class Subscriber : IWeakEventSubscriber + { + private readonly Action _onEvent; + + public Subscriber(Action onEvent) + { + _onEvent = onEvent; + } + + public void OnEvent(object sender, WeakEvent ev, EventArgs args) + { + _onEvent?.Invoke(); + } + } + + [Fact] + public void EventShouldBePassedToSubscriber() + { + bool handled = false; + var subscriber = new Subscriber(() => handled = true); + var source = new EventSource(); + EventSource.WeakEvent.Subscribe(source, subscriber); + + source.Fire(); + Assert.True(handled); + } + + + [Fact] + public void EventHandlerShouldNotBeKeptAlive() + { + bool handled = false; + var source = new EventSource(); + AddSubscriber(source, () => handled = true); + for (int c = 0; c < 10; c++) + { + GC.Collect(); + GC.Collect(3, GCCollectionMode.Forced, true); + } + source.Fire(); + Assert.False(handled); + } + + private void AddSubscriber(EventSource source, Action func) + { + EventSource.WeakEvent.Subscribe(source, new Subscriber(func)); + } + } +} From c2d627360cc256e503c2d78810dffd380af4b781 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Mon, 3 Jan 2022 19:15:45 +0300 Subject: [PATCH 66/86] [MacOS] fix quit menu item copy paste mistake --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index bda1c91750..5015c92e46 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -131,7 +131,7 @@ namespace Avalonia.Native }; quitItem.Click += (sender, args) => { - _applicationCommands.ShowAll(); + (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); }; result.Add(quitItem); } From 1221356df31623fd2cb036c8194ef18ee780012f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:20:44 +0300 Subject: [PATCH 67/86] apicompat --- src/Avalonia.Controls/ApiCompatBaseline.txt | 7 ++++++- src/Avalonia.Dialogs/ApiCompatBaseline.txt | 3 +++ tests/Avalonia.Base.UnitTests/WeakEventTests.cs | 10 +++++----- 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Dialogs/ApiCompatBaseline.txt diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index dd41c30e85..9b7d37e108 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -29,15 +29,20 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Window' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. @@ -57,4 +62,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 58 +Total Issues: 63 diff --git a/src/Avalonia.Dialogs/ApiCompatBaseline.txt b/src/Avalonia.Dialogs/ApiCompatBaseline.txt new file mode 100644 index 0000000000..9cb1b47015 --- /dev/null +++ b/src/Avalonia.Dialogs/ApiCompatBaseline.txt @@ -0,0 +1,3 @@ +Compat issues with assembly Avalonia.Dialogs: +CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Dialogs.AboutAvaloniaDialog' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. +Total Issues: 1 diff --git a/tests/Avalonia.Base.UnitTests/WeakEventTests.cs b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs index 81009f7c5f..2663b4858f 100644 --- a/tests/Avalonia.Base.UnitTests/WeakEventTests.cs +++ b/tests/Avalonia.Base.UnitTests/WeakEventTests.cs @@ -8,18 +8,18 @@ using Xunit; namespace Avalonia.Base.UnitTests { - public class WeakEventManagerTests + public class WeakEventTests { class EventSource { - public event EventHandler Event; + public event EventHandler Event; public void Fire() { Event?.Invoke(this, new EventArgs()); } - public static readonly WeakEvent WeakEvent = WeakEvent.Register( + public static readonly WeakEvent WeakEv = WeakEvent.Register( (t, s) => t.Event += s, (t, s) => t.Event -= s); } @@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests bool handled = false; var subscriber = new Subscriber(() => handled = true); var source = new EventSource(); - EventSource.WeakEvent.Subscribe(source, subscriber); + EventSource.WeakEv.Subscribe(source, subscriber); source.Fire(); Assert.True(handled); @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests private void AddSubscriber(EventSource source, Action func) { - EventSource.WeakEvent.Subscribe(source, new Subscriber(func)); + EventSource.WeakEv.Subscribe(source, new Subscriber(func)); } } } From 059e367cbfa9026dcfdf8f239d28cbce44cf1ed0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:24:31 +0300 Subject: [PATCH 68/86] Schedule weak event list Compact to be called later with Background dispatcher priority --- src/Avalonia.Base/Utilities/WeakEvent.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 430bc92838..c353e11263 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using Avalonia.Threading; namespace Avalonia.Utilities; @@ -53,6 +54,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event new WeakReference>[16]; private int _count; private readonly Action _unsubscribe; + private bool _compactScheduled; public Subscription(WeakEvent ev, TSender target) { @@ -99,12 +101,21 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if (removed) { - Compact(); + ScheduleCompact(); } } + void ScheduleCompact() + { + if(_compactScheduled) + return; + _compactScheduled = true; + Dispatcher.UIThread.Post(Compact, DispatcherPriority.Background); + } + void Compact() { + _compactScheduled = false; int empty = -1; for (var c = 0; c < _count; c++) { @@ -140,7 +151,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event } if (needCompact) - Compact(); + ScheduleCompact(); } } From f010322a066b570eb1d979c4b1013934575149ed Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 19:50:22 +0300 Subject: [PATCH 69/86] Force compacting WeakEvent subscriber list in before assertions in tests --- .../ExpressionObserverTests_DataValidation.cs | 3 +++ .../Core/ExpressionObserverTests_Indexer.cs | 12 +++++++++ .../ExpressionObserverTests_Observable.cs | 11 +++++++- .../Core/ExpressionObserverTests_Property.cs | 27 +++++++++++++++++-- .../Plugins/IndeiValidationPluginTests.cs | 3 +++ 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index ecc43aa3a5..43192584af 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Markup.Parsers; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -67,6 +68,8 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(1, data.ErrorsChangedSubscriptionCount); sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs index 6289ec46c7..20a4cb6d98 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs @@ -9,6 +9,7 @@ using Avalonia.Data.Core; using Avalonia.UnitTests; using Xunit; using Avalonia.Markup.Parsers; +using Avalonia.Threading; namespace Avalonia.Base.UnitTests.Data.Core { @@ -110,6 +111,9 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo.Add("baz"); } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -127,6 +131,8 @@ namespace Avalonia.Base.UnitTests.Data.Core { data.Foo.RemoveAt(0); } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(new[] { "foo", "bar" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -145,6 +151,9 @@ namespace Avalonia.Base.UnitTests.Data.Core { data.Foo[1] = "baz"; } + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(new[] { "bar", "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -202,6 +211,9 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo["foo"] = "bar2"; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + var expected = new[] { "bar", "bar2" }; Assert.Equal(expected, result); Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs index a70d4574a6..4f88d2de7c 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs @@ -5,6 +5,7 @@ using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Markup.Parsers; +using Avalonia.Threading; using Avalonia.UnitTests; using Xunit; @@ -68,6 +69,8 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { "foo" }, result); sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); GC.KeepAlive(data); @@ -109,10 +112,16 @@ namespace Avalonia.Base.UnitTests.Data.Core var sub = target.Subscribe(x => result.Add(x)); data1.Next.OnNext(data2); sync.ExecutePostedCallbacks(); - + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(new[] { new BindingNotification("foo") }, result); sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data1.PropertyChangedSubscriptionCount); GC.KeepAlive(data1); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 32cdd21e04..a9c62a3c4a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -10,6 +10,7 @@ using Avalonia.UnitTests; using Xunit; using System.Threading.Tasks; using Avalonia.Markup.Parsers; +using Avalonia.Threading; namespace Avalonia.Base.UnitTests.Data.Core { @@ -182,6 +183,9 @@ namespace Avalonia.Base.UnitTests.Data.Core sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); GC.KeepAlive(data); @@ -209,8 +213,11 @@ namespace Avalonia.Base.UnitTests.Data.Core data.RaisePropertyChanged(null); Assert.Equal(new[] { "foo", "bar", "bar" }, result); - + sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); @@ -231,7 +238,9 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { "bar", "baz", null }, result); sub.Dispose(); - + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); @@ -253,6 +262,9 @@ namespace Avalonia.Base.UnitTests.Data.Core Assert.Equal(new[] { "bar", "baz", null }, result); sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); @@ -297,6 +309,9 @@ namespace Avalonia.Base.UnitTests.Data.Core sub.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); Assert.Equal(0, old.PropertyChangedSubscriptionCount); @@ -329,6 +344,9 @@ namespace Avalonia.Base.UnitTests.Data.Core result); sub.Dispose(); + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); @@ -412,6 +430,9 @@ namespace Avalonia.Base.UnitTests.Data.Core sub1.Dispose(); sub2.Dispose(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); GC.KeepAlive(data); @@ -535,6 +556,8 @@ namespace Avalonia.Base.UnitTests.Data.Core }, result); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, first.PropertyChangedSubscriptionCount); Assert.Equal(0, second.PropertyChangedSubscriptionCount); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index d8eddf6330..e8f1f38b90 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using Avalonia.Data; using Avalonia.Data.Core.Plugins; +using Avalonia.Threading; using Xunit; namespace Avalonia.Base.UnitTests.Data.Core.Plugins @@ -57,6 +58,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins validator.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); validator.Unsubscribe(); + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } From b6219db4ad844f7e461fbcabcfa07d00d39c0d92 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 20:44:37 +0300 Subject: [PATCH 70/86] More Dispatcher.UIThread.RunJobs in tests --- .../Data/BindingTests.cs | 7 +++++++ .../ExpressionObserverBuilderTests_Indexer.cs | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index a0dd565a87..055de999e2 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -12,6 +12,7 @@ using System.Runtime.CompilerServices; using Avalonia.UnitTests; using Avalonia.Data.Converters; using Avalonia.Data.Core; +using Avalonia.Threading; namespace Avalonia.Markup.UnitTests.Data { @@ -160,6 +161,9 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, new Binding("Foo", BindingMode.OneTime)); target.DataContext = source; + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, source.SubscriberCount); } @@ -608,6 +612,9 @@ namespace Avalonia.Markup.UnitTests.Data root.DataContext = source; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(0, source.SubscriberCount); } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs index 39d6152b69..dbf6ef2ce9 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs @@ -10,6 +10,7 @@ using System.Collections.ObjectModel; using System.Reactive.Linq; using System.Text; using System.Threading.Tasks; +using Avalonia.Threading; using Xunit; namespace Avalonia.Markup.UnitTests.Parsers @@ -159,7 +160,10 @@ namespace Avalonia.Markup.UnitTests.Parsers { data.Foo.Add("baz"); } - + + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -178,6 +182,9 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo.RemoveAt(0); } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { "foo", "bar" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -196,6 +203,9 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo[1] = "baz"; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + Assert.Equal(new[] { "bar", "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); @@ -252,6 +262,9 @@ namespace Avalonia.Markup.UnitTests.Parsers data.Foo["foo"] = "bar2"; } + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + var expected = new[] { "bar", "bar2" }; Assert.Equal(expected, result); Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); From e516fe2e6043cce99926981dfdfc52fe09379878 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 20:45:55 +0300 Subject: [PATCH 71/86] More Dispatcher.UIThread.RunJobjs() in tests --- tests/Avalonia.LeakTests/AvaloniaObjectTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs index 54f9a87f94..19208b15f3 100644 --- a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs +++ b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Subjects; +using Avalonia.Threading; using JetBrains.dotMemoryUnit; using Xunit; using Xunit.Abstractions; @@ -56,7 +57,9 @@ namespace Avalonia.LeakTests completeSource(); GC.Collect(); - + // Forces WeakEvent compact + Dispatcher.UIThread.RunJobs(); + GC.Collect(); Assert.False(weakSource.IsAlive); } From a175fbbe3ad2ad82be446925760baf12b9c65ce7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Jan 2022 19:06:39 +0100 Subject: [PATCH 72/86] Check for null visual parent. (#7298) It shouldn't be possible to come across a null visual parent here because the visual should be attached to the visual tree, but according to #6930, it is. Because we don't have a repro here, defensively return a null value instead of throwing if we hit this case. --- src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index cb916293ac..63c22efc3f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -126,7 +126,12 @@ namespace Avalonia.Rendering.SceneGraph while (node == null && visual.IsVisible) { - visual = visual.VisualParent!; + var parent = visual.VisualParent; + + if (parent is null) + return null; + + visual = parent; node = scene.FindNode(visual); } From 32bf6160dfcb3c0a0550e627a51325b2f935625d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 3 Jan 2022 18:23:40 +0000 Subject: [PATCH 73/86] use IControlledApplicationLifetime --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 5015c92e46..57a8701202 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -131,7 +131,7 @@ namespace Avalonia.Native }; quitItem.Click += (sender, args) => { - (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown(); + (Application.Current.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(); }; result.Add(quitItem); } From dfd0523e36e3f5227305a6da73cd61d6a0943b93 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 3 Jan 2022 22:12:51 +0300 Subject: [PATCH 74/86] Optimize Pen's subscriptions to IAffects render --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 5 +- src/Avalonia.Visuals/Media/Pen.cs | 110 ++++++++------------- 2 files changed, 46 insertions(+), 69 deletions(-) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index dcb3246a63..68e3673cfe 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -7,6 +7,9 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun..ctor()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphTypeface.set(Avalonia.Media.GlyphTypeface)' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'Avalonia.Media.Pen' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Pen.AffectsRender(Avalonia.AvaloniaProperty[])' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Pen.RaiseInvalidated(System.EventArgs)' does not exist in the implementation but it does exist in the contract. TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. @@ -83,4 +86,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avaloni InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime.get()' is present in the implementation but not in the contract. -Total Issues: 84 +Total Issues: 87 diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index 7c966a35cf..f4ae58eff3 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public class Pen : AvaloniaObject, IPen + public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber { /// /// Defines the property. @@ -45,6 +45,9 @@ namespace Avalonia.Media public static readonly StyledProperty MiterLimitProperty = AvaloniaProperty.Register(nameof(MiterLimit), 10.0); + private EventHandler? _invalidated; + private IAffectsRender? _subscribedTo; + /// /// Initializes a new instance of the class. /// @@ -96,17 +99,6 @@ namespace Avalonia.Media DashStyle = dashStyle; } - static Pen() - { - AffectsRender( - BrushProperty, - ThicknessProperty, - DashStyleProperty, - LineCapProperty, - LineJoinProperty, - MiterLimitProperty); - } - /// /// Gets or sets the brush used to draw the stroke. /// @@ -116,6 +108,11 @@ namespace Avalonia.Media set => SetValue(BrushProperty, value); } + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.Invalidated += h, + (s, h) => s.Invalidated -= h); + /// /// Gets or sets the stroke thickness. /// @@ -165,7 +162,19 @@ namespace Avalonia.Media /// /// Raised when the pen changes. /// - public event EventHandler? Invalidated; + public event EventHandler? Invalidated + { + add + { + _invalidated += value; + UpdateBrushSubscription(); + } + remove + { + _invalidated -= value; + UpdateBrushSubscription(); + } + } /// /// Creates an immutable clone of the brush. @@ -182,68 +191,33 @@ namespace Avalonia.Media MiterLimit); } - /// - /// Marks a property as affecting the pen's visual representation. - /// - /// The properties. - /// - /// After a call to this method in a pen's static constructor, any change to the - /// property will cause the event to be raised on the pen. - /// - protected static void AffectsRender(params AvaloniaProperty[] properties) - where T : Pen + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) - { - sender.RaiseInvalidated(EventArgs.Empty); - } - } + _invalidated?.Invoke(this, EventArgs.Empty); + if(change.Property == BrushProperty) + UpdateBrushSubscription(); + base.OnPropertyChanged(change); + } - static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e) + void UpdateBrushSubscription() + { + if ((_invalidated == null || _subscribedTo != Brush) && _subscribedTo != null) { - if (e.Sender is T sender) - { - if (e.OldValue is IAffectsRender oldValue) - { - WeakEventHandlerManager.Unsubscribe( - oldValue, - nameof(oldValue.Invalidated), - sender.AffectsRenderInvalidated); - } - - if (e.NewValue is IAffectsRender newValue) - { - WeakEventHandlerManager.Subscribe( - newValue, - nameof(newValue.Invalidated), - sender.AffectsRenderInvalidated); - } - - sender.RaiseInvalidated(EventArgs.Empty); - } + InvalidatedWeakEvent.Unsubscribe(_subscribedTo, this); + _subscribedTo = null; } - foreach (var property in properties) + if (_invalidated != null && _subscribedTo != Brush && Brush is IAffectsRender affectsRender) { - if (property.CanValueAffectRender()) - { - property.Changed.Subscribe(e => InvalidateAndSubscribe(e)); - } - else - { - property.Changed.Subscribe(e => Invalidate(e)); - } + InvalidatedWeakEvent.Subscribe(affectsRender, this); + _subscribedTo = affectsRender; } } - - /// - /// Raises the event. - /// - /// The event args. - protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); - - private void AffectsRenderInvalidated(object? sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty); + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) + { + if (ev == InvalidatedWeakEvent) + _invalidated?.Invoke(this, EventArgs.Empty); + } } } From 7e45ee1e537e5f0b67c23cba159638dfcf5241a7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 4 Jan 2022 01:19:14 +0300 Subject: [PATCH 75/86] Subscribe to DashStyle too --- src/Avalonia.Visuals/Media/Pen.cs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index f4ae58eff3..65ba851100 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -46,7 +46,8 @@ namespace Avalonia.Media AvaloniaProperty.Register(nameof(MiterLimit), 10.0); private EventHandler? _invalidated; - private IAffectsRender? _subscribedTo; + private IAffectsRender? _subscribedToBrush; + private IAffectsRender? _subscribedToDashes; /// /// Initializes a new instance of the class. @@ -167,12 +168,12 @@ namespace Avalonia.Media add { _invalidated += value; - UpdateBrushSubscription(); + UpdateSubscriptions(); } remove { _invalidated -= value; - UpdateBrushSubscription(); + UpdateSubscriptions(); } } @@ -195,24 +196,33 @@ namespace Avalonia.Media { _invalidated?.Invoke(this, EventArgs.Empty); if(change.Property == BrushProperty) - UpdateBrushSubscription(); + UpdateSubscription(ref _subscribedToBrush, Brush); + if(change.Property == DashStyleProperty) + UpdateSubscription(ref _subscribedToDashes, DashStyle); base.OnPropertyChanged(change); } - void UpdateBrushSubscription() + + void UpdateSubscription(ref IAffectsRender? field, object? value) { - if ((_invalidated == null || _subscribedTo != Brush) && _subscribedTo != null) + if ((_invalidated == null || field != value) && field != null) { - InvalidatedWeakEvent.Unsubscribe(_subscribedTo, this); - _subscribedTo = null; + InvalidatedWeakEvent.Unsubscribe(field, this); + field = null; } - if (_invalidated != null && _subscribedTo != Brush && Brush is IAffectsRender affectsRender) + if (_invalidated != null && field != value && value is IAffectsRender affectsRender) { InvalidatedWeakEvent.Subscribe(affectsRender, this); - _subscribedTo = affectsRender; + field = affectsRender; } } + + void UpdateSubscriptions() + { + UpdateSubscription(ref _subscribedToBrush, Brush); + UpdateSubscription(ref _subscribedToDashes, DashStyle); + } void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) { From a08c4888e0e2e1112275d228b42a0a3899e2a5f2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 4 Jan 2022 18:01:32 +0300 Subject: [PATCH 76/86] WeakEvent: cache Compact() delegate --- src/Avalonia.Base/Utilities/WeakEvent.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index c353e11263..0b32015a8a 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -49,6 +49,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { private readonly WeakEvent _ev; private readonly TSender _target; + private readonly Action _compact; private WeakReference>?[] _data = new WeakReference>[16]; @@ -60,7 +61,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { _ev = ev; _target = target; - + _compact = Compact; _unsubscribe = ev._subscribe(target, OnEvent); } @@ -110,7 +111,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event if(_compactScheduled) return; _compactScheduled = true; - Dispatcher.UIThread.Post(Compact, DispatcherPriority.Background); + Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); } void Compact() From c73a6c86c0d0fba2b969a81dd9459ce0d0d02125 Mon Sep 17 00:00:00 2001 From: odalet Date: Tue, 4 Jan 2022 17:35:59 +0100 Subject: [PATCH 77/86] Fixes #7309 - Each time we retrieve a null *PlatformOptions from AvaloniaLocator, return a default instance --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 8 ++++---- src/Avalonia.Native/AvaloniaNativePlatform.cs | 4 ++-- src/Avalonia.X11/Glx/GlxDisplay.cs | 4 ++-- .../LinuxFramebufferPlatform.cs | 4 ++-- src/Windows/Avalonia.Win32/Win32GlManager.cs | 9 ++++----- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 57a8701202..40ffc31728 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -80,8 +80,8 @@ namespace Avalonia.Native }; result.Add(aboutItem); - var macOpts = AvaloniaLocator.Current.GetService(); - if (macOpts == null || !macOpts.DisableDefaultApplicationMenuItems) + var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); + if (!macOpts.DisableDefaultApplicationMenuItems) { result.Add(new NativeMenuItemSeparator()); @@ -142,9 +142,9 @@ namespace Avalonia.Native private void DoLayoutReset(bool forceUpdate = false) { - var macOpts = AvaloniaLocator.Current.GetService(); + var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); - if (macOpts != null && macOpts.DisableNativeMenus) + if (macOpts.DisableNativeMenus) { return; } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 5fa50f0e7f..1eadf70b13 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -96,9 +96,9 @@ namespace Avalonia.Native _factory.Initialize(new GCHandleDeallocator(), applicationPlatform); if (_factory.MacOptions != null) { - var macOpts = AvaloniaLocator.Current.GetService(); + var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); - _factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0); + _factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0); } AvaloniaLocator.CurrentMutable diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index fa8c866c09..fcdc10e999 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -95,8 +95,8 @@ namespace Avalonia.X11.Glx if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1") { - var blacklist = AvaloniaLocator.Current.GetService() - ?.GlxRendererBlacklist; + var opts = AvaloniaLocator.Current.GetService() ?? new X11PlatformOptions(); + var blacklist = opts.GlxRendererBlacklist; if (blacklist != null) foreach (var item in blacklist) if (glInterface.Renderer.Contains(item)) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index f4db6bf48a..4add4c423b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -38,11 +38,11 @@ namespace Avalonia.LinuxFramebuffer if (_fb is IGlOutputBackend gl) AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); - var opts = AvaloniaLocator.Current.GetService(); + var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) - .Bind().ToConstant(new DefaultRenderTimer(opts?.Fps ?? 60)) + .Bind().ToConstant(new DefaultRenderTimer(opts.Fps)) .Bind().ToConstant(new RenderLoop()) .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 289c100d51..0376a41f8c 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -13,19 +13,18 @@ namespace Avalonia.Win32 { AvaloniaLocator.CurrentMutable.Bind().ToLazy(() => { - var opts = AvaloniaLocator.Current.GetService(); - if (opts?.UseWgl == true) + var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); + if (opts.UseWgl) { var wgl = WglPlatformOpenGlInterface.TryCreate(); return wgl; } - if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) + if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) { var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - if (egl != null && - opts?.UseWindowsUIComposition == true) + if (egl != null && opts.UseWindowsUIComposition) { WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); } From 1604be478470c2dffd1c41055ea854fc89fa6b87 Mon Sep 17 00:00:00 2001 From: Tim U Date: Wed, 5 Jan 2022 08:00:03 +0100 Subject: [PATCH 78/86] fix Validation for SelectedDateProperty not working --- src/Avalonia.Controls/Calendar/CalendarDatePicker.cs | 11 +++++------ .../Controls/CalendarDatePicker.xaml | 6 +++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index a856ee071c..cd9c80d3e0 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -185,7 +185,8 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect( nameof(SelectedDate), o => o.SelectedDate, - (o, v) => o.SelectedDate = v); + (o, v) => o.SelectedDate = v, + enableDataValidation: true); public static readonly StyledProperty SelectedDateFormatProperty = AvaloniaProperty.Register( @@ -533,13 +534,11 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { - base.OnPropertyChanged(change); - - if (change.Property == SelectedDateProperty) + if (property == SelectedDateProperty) { - DataValidationErrors.SetError(this, change.NewValue.Error); + DataValidationErrors.SetError(this, value.Error); } } diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index 6c4e94caf1..26c3bbc19f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -33,6 +33,7 @@ + @@ -107,7 +108,6 @@ Padding="{TemplateBinding Padding}" Watermark="{TemplateBinding Watermark}" UseFloatingWatermark="{TemplateBinding UseFloatingWatermark}" - DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Grid.Column="0"/> @@ -136,8 +136,12 @@ DisplayDateEnd="{TemplateBinding DisplayDateEnd}" /> + + From fe21e298afbff5ed03fa0b08c95dad5e2352dd25 Mon Sep 17 00:00:00 2001 From: Tim U Date: Wed, 5 Jan 2022 08:00:43 +0100 Subject: [PATCH 79/86] Show validation in Demo App --- .../Pages/CalendarDatePickerPage.xaml | 5 +++++ .../ViewModels/MainWindowViewModel.cs | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml index 3e50bf8a08..2fe16ba8e3 100644 --- a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml @@ -1,5 +1,7 @@ A control for selecting dates with a calendar drop-down @@ -39,6 +41,9 @@ + + + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 4b3cfa9c9d..2b0c30f311 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Notifications; using Avalonia.Dialogs; using Avalonia.Platform; using System; +using System.ComponentModel.DataAnnotations; using MiniMvvm; namespace ControlCatalog.ViewModels @@ -164,5 +165,17 @@ namespace ControlCatalog.ViewModels public MiniCommand ExitCommand { get; } public MiniCommand ToggleMenuItemCheckedCommand { get; } + + private DateTime? _validatedDateExample; + + /// + /// A required DateTime which should demonstrate validation for the DateTimePicker + /// + [Required] + public DateTime? ValidatedDateExample + { + get => _validatedDateExample; + set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value); + } } } From 88db01532f70f209af9a1cb90f8bd13ad36fc6bf Mon Sep 17 00:00:00 2001 From: Tim U Date: Tue, 4 Jan 2022 16:34:41 +0100 Subject: [PATCH 80/86] Implement CellEditingTemplate --- .../DataGridTemplateColumn.cs | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 7e95dd100c..fbdad0a8ad 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -30,6 +30,20 @@ namespace Avalonia.Controls set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); } } + private IDataTemplate _cellEditingCellTemplate; + + public static readonly DirectProperty CellEditingTemplateProperty = + AvaloniaProperty.RegisterDirect( + nameof(CellEditingTemplate), + o => o.CellEditingTemplate, + (o, v) => o.CellEditingTemplate = v); + + public IDataTemplate CellEditingTemplate + { + get => _cellEditingCellTemplate; + set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value); + } + private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e) { var oldValue = (IDataTemplate)e.OldValue; @@ -38,7 +52,7 @@ namespace Avalonia.Controls public DataGridTemplateColumn() { - IsReadOnly = true; + // IsReadOnly = true; } protected override IControl GenerateElement(DataGridCell cell, object dataItem) @@ -60,7 +74,18 @@ namespace Avalonia.Controls protected override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding) { binding = null; - return GenerateElement(cell, dataItem); + if(CellEditingTemplate != null) + { + return CellEditingTemplate.Build(dataItem); + } + if (Design.IsDesignMode) + { + return null; + } + else + { + throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn)); + } } protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs) @@ -70,7 +95,8 @@ namespace Avalonia.Controls protected internal override void RefreshCellContent(IControl element, string propertyName) { - if(propertyName == nameof(CellTemplate) && element.Parent is DataGridCell cell) + var cell = element.Parent as DataGridCell; + if(propertyName == nameof(CellTemplate) && cell is not null) { cell.Content = GenerateElement(cell, cell.DataContext); } From 66a02a37d09b226127b3d7636e18f0d84fbd0f02 Mon Sep 17 00:00:00 2001 From: Tim U Date: Tue, 4 Jan 2022 17:27:28 +0100 Subject: [PATCH 81/86] Handle IsReadOnly correct for DataGridTemplateColumn --- .../DataGridColumn.cs | 2 +- .../DataGridTemplateColumn.cs | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 6b515503aa..8501ce3896 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -448,7 +448,7 @@ namespace Avalonia.Controls internal set; } - public bool IsReadOnly + public virtual bool IsReadOnly { get { diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index fbdad0a8ad..e8ccb7df34 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls { public class DataGridTemplateColumn : DataGridColumn { - IDataTemplate _cellTemplate; + private IDataTemplate _cellTemplate; public static readonly DirectProperty CellTemplateProperty = AvaloniaProperty.RegisterDirect( @@ -54,7 +54,7 @@ namespace Avalonia.Controls { // IsReadOnly = true; } - + protected override IControl GenerateElement(DataGridCell cell, object dataItem) { if(CellTemplate != null) @@ -78,6 +78,10 @@ namespace Avalonia.Controls { return CellEditingTemplate.Build(dataItem); } + else if (CellTemplate != null) + { + return CellTemplate.Build(dataItem); + } if (Design.IsDesignMode) { return null; @@ -103,5 +107,22 @@ namespace Avalonia.Controls base.RefreshCellContent(element, propertyName); } + + public override bool IsReadOnly + { + get + { + if (CellEditingTemplate is null) + { + return true; + } + + return base.IsReadOnly; + } + set + { + base.IsReadOnly = value; + } + } } } From cf6c0991f8f52bcd683328dafa011550df184698 Mon Sep 17 00:00:00 2001 From: Tim U Date: Tue, 4 Jan 2022 17:27:57 +0100 Subject: [PATCH 82/86] Update Demo --- samples/ControlCatalog/Models/Person.cs | 15 +++++++++++++++ samples/ControlCatalog/Pages/DataGridPage.xaml | 12 ++++++++++++ samples/ControlCatalog/Pages/DataGridPage.xaml.cs | 6 +++--- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Models/Person.cs b/samples/ControlCatalog/Models/Person.cs index 47f41bc584..cd70fa3959 100644 --- a/samples/ControlCatalog/Models/Person.cs +++ b/samples/ControlCatalog/Models/Person.cs @@ -16,6 +16,7 @@ namespace ControlCatalog.Models string _firstName; string _lastName; bool _isBanned; + private int _age; public string FirstName { @@ -59,6 +60,20 @@ namespace ControlCatalog.Models } } + + /// + /// Gets or sets the age of the person + /// + public int Age + { + get => _age; + set + { + _age = value; + OnPropertyChanged(nameof(Age)); + } + } + Dictionary> _errorLookup = new Dictionary>(); void SetError(string propertyName, string error) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 63e873d9b5..451a774cb4 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -64,6 +64,18 @@ + + + + + + + + + + + +