Browse Source

Merge branch 'master' into fixex/pointer-popup-positioning

pull/7250/head
Dan Walmsley 5 years ago
committed by GitHub
parent
commit
38d69e1d96
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      build/SourceLink.props
  2. 1
      samples/ControlCatalog/App.xaml
  3. 15
      samples/ControlCatalog/Models/Person.cs
  4. 5
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
  5. 12
      samples/ControlCatalog/Pages/DataGridPage.xaml
  6. 22
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  7. 13
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  8. 16
      src/Avalonia.Animation/Animatable.cs
  9. 46
      src/Avalonia.Animation/Animation.cs
  10. 33
      src/Avalonia.Animation/AnimationInstance`1.cs
  11. 20
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  12. 9
      src/Avalonia.Animation/Animators/Animator`1.cs
  13. 4
      src/Avalonia.Animation/Avalonia.Animation.csproj
  14. 2
      src/Avalonia.Animation/Clock.cs
  15. 8
      src/Avalonia.Animation/Cue.cs
  16. 8
      src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs
  17. 4
      src/Avalonia.Animation/Easing/Easing.cs
  18. 4
      src/Avalonia.Animation/Easing/EasingTypeConverter.cs
  19. 2
      src/Avalonia.Animation/IAnimation.cs
  20. 4
      src/Avalonia.Animation/IAnimationSetter.cs
  21. 4
      src/Avalonia.Animation/IAnimator.cs
  22. 2
      src/Avalonia.Animation/ITransition.cs
  23. 2
      src/Avalonia.Animation/IterationCount.cs
  24. 4
      src/Avalonia.Animation/IterationCountTypeConverter.cs
  25. 4
      src/Avalonia.Animation/KeyFrame.cs
  26. 4
      src/Avalonia.Animation/KeySplineTypeConverter.cs
  27. 21
      src/Avalonia.Animation/Transition.cs
  28. 6
      src/Avalonia.Animation/TransitionInstance.cs
  29. 21
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  30. 10
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  31. 20
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  32. 24
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  33. 2
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  34. 12
      src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs
  35. 187
      src/Avalonia.Base/Utilities/WeakEvent.cs
  36. 40
      src/Avalonia.Base/Utilities/WeakEvents.cs
  37. 35
      src/Avalonia.Base/Utilities/WeakObservable.cs
  38. 1
      src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs
  39. 1
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  40. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  41. 72
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  42. 7
      src/Avalonia.Controls/ApiCompatBaseline.txt
  43. 11
      src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
  44. 10
      src/Avalonia.Controls/NativeMenuItem.cs
  45. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  46. 2
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  47. 32
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  48. 24
      src/Avalonia.Controls/TopLevel.cs
  49. 15
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs
  50. 326
      src/Avalonia.Controls/ValidatingToplevel.cs
  51. 11
      src/Avalonia.Controls/Window.cs
  52. 9
      src/Avalonia.Controls/WindowBase.cs
  53. 2
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
  54. 3
      src/Avalonia.Dialogs/ApiCompatBaseline.txt
  55. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  56. 6
      src/Avalonia.Input/KeyBinding.cs
  57. 17
      src/Avalonia.Layout/AttachedLayout.cs
  58. 115
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  59. 4
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  60. 28
      src/Avalonia.Native/WindowImplBase.cs
  61. 2
      src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml
  62. 6
      src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml
  63. 2
      src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml
  64. 1
      src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
  65. 1
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  66. 4
      src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml
  67. 7
      src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs
  68. 5
      src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs
  69. 5
      src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
  70. 7
      src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
  71. 5
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  72. 118
      src/Avalonia.Visuals/Media/Pen.cs
  73. 7
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  74. 4
      src/Avalonia.X11/Glx/GlxDisplay.cs
  75. 13
      src/Avalonia.X11/X11Window.cs
  76. 4
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  77. 36
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs
  78. 13
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  79. 2
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  80. 2
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  81. 6
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  82. 8
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  83. 9
      src/Windows/Avalonia.Win32/Win32GlManager.cs
  84. 25
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
  85. 3
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  86. 12
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Indexer.cs
  87. 11
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs
  88. 27
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs
  89. 3
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs
  90. 75
      tests/Avalonia.Base.UnitTests/WeakEventTests.cs
  91. 5
      tests/Avalonia.LeakTests/AvaloniaObjectTests.cs
  92. 7
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  93. 15
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Indexer.cs

5
build/SourceLink.props

@ -3,7 +3,6 @@
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>false</IncludeSymbols>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>embedded</DebugType>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
@ -15,6 +14,10 @@
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup>
<DebugType Condition="$(ContinuousIntegrationBuild) == 'true'">embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>

1
samples/ControlCatalog/App.xaml

@ -3,6 +3,7 @@
xmlns:vm="using:ControlCatalog.ViewModels"
x:DataType="vm:ApplicationViewModel"
x:CompileBindings="True"
Name="Avalonia ControlCatalog"
x:Class="ControlCatalog.App">
<Application.Styles>
<Style Selector="TextBlock.h1, TextBlock.h2, TextBlock.h3">

15
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
}
}
/// <summary>
/// Gets or sets the age of the person
/// </summary>
public int Age
{
get => _age;
set
{
_age = value;
OnPropertyChanged(nameof(Age));
}
}
Dictionary<string, List<string>> _errorLookup = new Dictionary<string, List<string>>();
void SetError(string propertyName, string error)

5
samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml

@ -1,5 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
x:DataType="vm:MainWindowViewModel"
x:Class="ControlCatalog.Pages.CalendarDatePickerPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
@ -39,6 +41,9 @@
<TextBlock Text="Disabled"/>
<CalendarDatePicker IsEnabled="False"/>
<TextBlock Text="Validation Example"/>
<CalendarDatePicker SelectedDate="{CompiledBinding ValidatedDateExample, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>

12
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -64,6 +64,18 @@
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" />
<DataGridTemplateColumn Header="Age" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:Person">
<TextBlock Text="{Binding Age, StringFormat='{}{0} years'}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate DataType="local:Person">
<NumericUpDown Value="{Binding Age}" FormatString="N0" HorizontalAlignment="Stretch" Minimum="0" Maximum="120" TemplateApplied="NumericUpDown_OnTemplateApplied" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Name="btnAdd" Margin="12,0,12,12" Content="Add" HorizontalAlignment="Right" />

22
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -6,7 +6,9 @@ using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.Models;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Threading;
namespace ControlCatalog.Pages
{
@ -48,9 +50,9 @@ namespace ControlCatalog.Pages
var items = new List<Person>
{
new Person { FirstName = "John", LastName = "Doe" },
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true },
new Person { FirstName = "Zack", LastName = "Ward" }
new Person { FirstName = "John", LastName = "Doe" , Age = 30},
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 },
new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 }
};
var collectionView3 = new DataGridCollectionView(items);
@ -84,5 +86,19 @@ namespace ControlCatalog.Pages
return Comparer.Default.Compare(x, y);
}
}
private void NumericUpDown_OnTemplateApplied(object sender, TemplateAppliedEventArgs e)
{
// We want to focus the TextBox of the NumericUpDown. To do so we search for this control when the template
// is applied, but we postpone the action until the control is actually loaded.
if (e.NameScope.Find<TextBox>("PART_TextBox") is {} textBox)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
textBox.Focus();
textBox.SelectAll();
}, DispatcherPriority.Loaded);
}
}
}
}

13
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;
/// <summary>
/// A required DateTime which should demonstrate validation for the DateTimePicker
/// </summary>
[Required]
public DateTime? ValidatedDateExample
{
get => _validatedDateExample;
set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value);
}
}
}

16
src/Avalonia.Animation/Animatable.cs

@ -157,7 +157,7 @@ namespace Avalonia.Animation
state.Instance?.Dispose();
state.Instance = transition.Apply(
this,
Clock ?? AvaloniaLocator.Current.GetService<IGlobalClock>(),
Clock ?? AvaloniaLocator.Current.GetRequiredService<IGlobalClock>(),
oldValue,
newValue);
return;
@ -169,7 +169,7 @@ namespace Avalonia.Animation
base.OnPropertyChangedCore(change);
}
private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
private void TransitionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (!_transitionsEnabled)
{
@ -179,14 +179,14 @@ namespace Avalonia.Animation
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddTransitions(e.NewItems);
AddTransitions(e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
RemoveTransitions(e.OldItems);
RemoveTransitions(e.OldItems!);
break;
case NotifyCollectionChangedAction.Replace:
RemoveTransitions(e.OldItems);
AddTransitions(e.NewItems);
RemoveTransitions(e.OldItems!);
AddTransitions(e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
throw new NotSupportedException("Transitions collection cannot be reset.");
@ -204,7 +204,7 @@ namespace Avalonia.Animation
for (var i = 0; i < items.Count; ++i)
{
var t = (ITransition)items[i];
var t = (ITransition)items[i]!;
_transitionState.Add(t, new TransitionState
{
@ -222,7 +222,7 @@ namespace Avalonia.Animation
for (var i = 0; i < items.Count; ++i)
{
var t = (ITransition)items[i];
var t = (ITransition)items[i]!;
if (_transitionState.TryGetValue(t, out var state))
{

46
src/Avalonia.Animation/Animation.cs

@ -203,7 +203,7 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <returns>The property animator type.</returns>
public static Type GetAnimator(IAnimationSetter setter)
public static Type? GetAnimator(IAnimationSetter setter)
{
if (s_animators.TryGetValue(setter, out var type))
{
@ -254,7 +254,7 @@ namespace Avalonia.Animation
Animators.Insert(0, (condition, typeof(TAnimator)));
}
private static Type GetAnimatorType(AvaloniaProperty property)
private static Type? GetAnimatorType(AvaloniaProperty property)
{
foreach (var (condition, type) in Animators)
{
@ -276,6 +276,11 @@ namespace Avalonia.Animation
{
foreach (var setter in keyframe.Setters)
{
if (setter.Property is null)
{
throw new InvalidOperationException("No Setter property assigned.");
}
var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property);
if (handler == null)
@ -305,7 +310,7 @@ namespace Avalonia.Animation
foreach (var (handlerType, property) in handlerList)
{
var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!;
newInstance.Property = property;
newAnimatorInstances.Add(newInstance);
}
@ -321,32 +326,43 @@ namespace Avalonia.Animation
}
/// <inheritdoc/>
public IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
public IDisposable Apply(Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete)
{
var (animators, subscriptions) = InterpretKeyframes(control);
if (animators.Count == 1)
{
subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete));
var subscription = animators[0].Apply(this, control, clock, match, onComplete);
if (subscription is not null)
{
subscriptions.Add(subscription);
}
}
else
{
var completionTasks = onComplete != null ? new List<Task>() : null;
foreach (IAnimator animator in animators)
{
Action animatorOnComplete = null;
Action? animatorOnComplete = null;
if (onComplete != null)
{
var tcs = new TaskCompletionSource<object>();
var tcs = new TaskCompletionSource<object?>();
animatorOnComplete = () => tcs.SetResult(null);
completionTasks.Add(tcs.Task);
completionTasks!.Add(tcs.Task);
}
var subscription = animator.Apply(this, control, clock, match, animatorOnComplete);
if (subscription is not null)
{
subscriptions.Add(subscription);
}
subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete));
}
if (onComplete != null)
{
Task.WhenAll(completionTasks).ContinueWith(
(_, state) => ((Action)state).Invoke(),
Task.WhenAll(completionTasks!).ContinueWith(
(_, state) => ((Action)state!).Invoke(),
onComplete);
}
}
@ -354,25 +370,25 @@ namespace Avalonia.Animation
}
/// <inheritdoc/>
public Task RunAsync(Animatable control, IClock clock = null)
public Task RunAsync(Animatable control, IClock? clock = null)
{
return RunAsync(control, clock, default);
}
/// <inheritdoc/>
public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default)
public Task RunAsync(Animatable control, IClock? clock = null, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.CompletedTask;
}
var run = new TaskCompletionSource<object>();
var run = new TaskCompletionSource<object?>();
if (this.IterationCount == IterationCount.Infinite)
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null, cancellation = null;
IDisposable? subscriptions = null, cancellation = null;
subscriptions = this.Apply(control, clock, Observable.Return(true), () =>
{
run.TrySetResult(null);

33
src/Avalonia.Animation/AnimationInstance`1.cs

@ -31,15 +31,15 @@ namespace Avalonia.Animation
private TimeSpan _initialDelay;
private TimeSpan _iterationDelay;
private TimeSpan _duration;
private Easings.Easing _easeFunc;
private Action _onCompleteAction;
private Easings.Easing? _easeFunc;
private Action? _onCompleteAction;
private Func<double, T, T> _interpolator;
private IDisposable _timerSub;
private IDisposable? _timerSub;
private readonly IClock _baseClock;
private IClock _clock;
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedDelegate;
private IClock? _clock;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChangedDelegate;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action OnComplete, Func<double, T, T> Interpolator)
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action? OnComplete, Func<double, T, T> Interpolator)
{
_animator = animator;
_animation = animation;
@ -47,6 +47,9 @@ namespace Avalonia.Animation
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
_lastInterpValue = default!;
_firstKFValue = default!;
_neutralValue = default!;
FetchProperties();
}
@ -82,7 +85,7 @@ namespace Avalonia.Animation
_targetControl.PropertyChanged -= _propertyChangedDelegate;
_timerSub?.Dispose();
_clock.PlayState = PlayState.Stop;
_clock!.PlayState = PlayState.Stop;
}
protected override void Subscribed()
@ -108,6 +111,8 @@ namespace Avalonia.Animation
private void ApplyFinalFill()
{
if (_animator.Property is null)
throw new InvalidOperationException("Animator has no property specified.");
if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both)
_targetControl.SetValue(_animator.Property, _lastInterpValue, BindingPriority.LocalValue);
}
@ -130,12 +135,12 @@ namespace Avalonia.Animation
private void DoPlayStates()
{
if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop)
if (_clock!.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop)
DoComplete();
if (!_gotFirstKFValue)
{
_firstKFValue = (T)_animator.First().Value;
_firstKFValue = (T)_animator.First().Value!;
_gotFirstKFValue = true;
}
}
@ -169,7 +174,7 @@ namespace Avalonia.Animation
// and snap the last iteration value to exact values.
if ((_currentIteration + 1) > _iterationCount)
{
var easedTime = _easeFunc.Ease(_playbackReversed ? 0.0 : 1.0);
var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
DoComplete();
}
@ -203,7 +208,7 @@ namespace Avalonia.Animation
normalizedTime = 1 - normalizedTime;
// Ease and interpolate
var easedTime = _easeFunc.Ease(normalizedTime);
var easedTime = _easeFunc!.Ease(normalizedTime);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
PublishNext(_lastInterpValue);
@ -223,14 +228,14 @@ namespace Avalonia.Animation
private void UpdateNeutralValue()
{
var property = _animator.Property;
var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified.");
var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue);
_neutralValue = baseValue != AvaloniaProperty.UnsetValue ?
(T)baseValue : (T)_targetControl.GetValue(property);
(T)baseValue! : (T)_targetControl.GetValue(property)!;
}
private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation)
{

20
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -12,22 +12,22 @@ namespace Avalonia.Animation
/// </summary>
public class AnimatorKeyFrame : AvaloniaObject
{
public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public static readonly DirectProperty<AnimatorKeyFrame, object?> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object?>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public AnimatorKeyFrame()
{
}
public AnimatorKeyFrame(Type animatorType, Cue cue)
public AnimatorKeyFrame(Type? animatorType, Cue cue)
{
AnimatorType = animatorType;
Cue = cue;
KeySpline = null;
}
public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline)
public AnimatorKeyFrame(Type? animatorType, Cue cue, KeySpline? keySpline)
{
AnimatorType = animatorType;
Cue = cue;
@ -35,14 +35,14 @@ namespace Avalonia.Animation
}
internal bool isNeutral;
public Type AnimatorType { get; }
public Type? AnimatorType { get; }
public Cue Cue { get; }
public KeySpline KeySpline { get; }
public AvaloniaProperty Property { get; private set; }
public KeySpline? KeySpline { get; }
public AvaloniaProperty? Property { get; private set; }
private object _value;
private object? _value;
public object Value
public object? Value
{
get => _value;
set => SetAndRaise(ValueProperty, ref _value, value);
@ -80,7 +80,7 @@ namespace Avalonia.Animation
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
return (T)typeConv.ConvertTo(Value, typeof(T));
return (T)typeConv.ConvertTo(Value, typeof(T))!;
}
}
}

9
src/Avalonia.Animation/Animators/Animator`1.cs

@ -24,7 +24,7 @@ namespace Avalonia.Animation.Animators
/// <summary>
/// Gets or sets the target property for the keyframe.
/// </summary>
public AvaloniaProperty Property { get; set; }
public AvaloniaProperty? Property { get; set; }
public Animator()
{
@ -33,7 +33,7 @@ namespace Avalonia.Animation.Animators
}
/// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete)
{
if (!_isVerifiedAndConverted)
VerifyConvertKeyFrames();
@ -106,13 +106,16 @@ namespace Avalonia.Animation.Animators
public virtual IDisposable BindAnimation(Animatable control, IObservable<T> instance)
{
if (Property is null)
throw new InvalidOperationException("Animator has no property specified.");
return control.Bind((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
}
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete)
internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete)
{
var instance = new AnimationInstance<T>(
animation,

4
src/Avalonia.Animation/Avalonia.Animation.csproj

@ -2,9 +2,13 @@
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
</Project>

2
src/Avalonia.Animation/Clock.cs

@ -4,7 +4,7 @@ namespace Avalonia.Animation
{
public class Clock : ClockBase
{
public static IClock GlobalClock => AvaloniaLocator.Current.GetService<IGlobalClock>();
public static IClock GlobalClock => AvaloniaLocator.Current.GetRequiredService<IGlobalClock>();
private readonly IDisposable _parentSubscription;

8
src/Avalonia.Animation/Cue.cs

@ -30,7 +30,7 @@ namespace Avalonia.Animation
/// <summary>
/// Parses a string to a <see cref="Cue"/> object.
/// </summary>
public static Cue Parse(string value, CultureInfo culture)
public static Cue Parse(string value, CultureInfo? culture)
{
string v = value;
@ -72,14 +72,14 @@ namespace Avalonia.Animation
public class CueTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return Cue.Parse((string)value, culture);
}
}
}
}

8
src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs

@ -8,15 +8,15 @@ namespace Avalonia.Animation
/// </summary>
internal class DisposeAnimationInstanceSubject<T> : IObserver<bool>, IDisposable
{
private IDisposable _lastInstance;
private IDisposable? _lastInstance;
private bool _lastMatch;
private Animator<T> _animator;
private Animation _animation;
private Animatable _control;
private Action _onComplete;
private IClock _clock;
private Action? _onComplete;
private IClock? _clock;
public DisposeAnimationInstanceSubject(Animator<T> animator, Animation animation, Animatable control, IClock clock, Action onComplete)
public DisposeAnimationInstanceSubject(Animator<T> animator, Animation animation, Animatable control, IClock? clock, Action? onComplete)
{
this._animator = animator;
this._animation = animation;

4
src/Avalonia.Animation/Easing/Easing.cs

@ -15,7 +15,7 @@ namespace Avalonia.Animation.Easings
/// <inheritdoc/>
public abstract double Ease(double progress);
static Dictionary<string, Type> _easingTypes;
static Dictionary<string, Type>? _easingTypes;
static readonly Type s_thisType = typeof(Easing);
@ -48,7 +48,7 @@ namespace Avalonia.Animation.Easings
if (_easingTypes.ContainsKey(e))
{
var type = _easingTypes[e];
return (Easing)Activator.CreateInstance(type);
return (Easing)Activator.CreateInstance(type)!;
}
else
{

4
src/Avalonia.Animation/Easing/EasingTypeConverter.cs

@ -6,12 +6,12 @@ namespace Avalonia.Animation.Easings
{
public class EasingTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return Easing.Parse((string)value);
}

2
src/Avalonia.Animation/IAnimation.cs

@ -12,7 +12,7 @@ namespace Avalonia.Animation
/// <summary>
/// Apply the animation to the specified control and run it when <paramref name="match" /> produces <c>true</c>.
/// </summary>
IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete = null);
IDisposable Apply(Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete = null);
/// <summary>
/// Run the animation on the specified control.

4
src/Avalonia.Animation/IAnimationSetter.cs

@ -2,7 +2,7 @@ namespace Avalonia.Animation
{
public interface IAnimationSetter
{
AvaloniaProperty Property { get; set; }
object Value { get; set; }
AvaloniaProperty? Property { get; set; }
object? Value { get; set; }
}
}

4
src/Avalonia.Animation/IAnimator.cs

@ -11,11 +11,11 @@ namespace Avalonia.Animation
/// <summary>
/// The target property.
/// </summary>
AvaloniaProperty Property {get; set;}
AvaloniaProperty? Property {get; set;}
/// <summary>
/// Applies the current KeyFrame group to the specified control.
/// </summary>
IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete);
IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete);
}
}

2
src/Avalonia.Animation/ITransition.cs

@ -10,7 +10,7 @@ namespace Avalonia.Animation
/// <summary>
/// Applies the transition to the specified <see cref="Animatable"/>.
/// </summary>
IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue);
IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue);
/// <summary>
/// Gets the property to be animated.

2
src/Avalonia.Animation/IterationCount.cs

@ -97,7 +97,7 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="o">The object with which to test equality.</param>
/// <returns>True if the objects are equal, otherwise false.</returns>
public override bool Equals(object o)
public override bool Equals(object? o)
{
if (o == null)
{

4
src/Avalonia.Animation/IterationCountTypeConverter.cs

@ -6,12 +6,12 @@ namespace Avalonia.Animation
{
public class IterationCountTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return IterationCount.Parse((string)value);
}

4
src/Avalonia.Animation/KeyFrame.cs

@ -19,7 +19,7 @@ namespace Avalonia.Animation
{
private TimeSpan _ktimeSpan;
private Cue _kCue;
private KeySpline _kKeySpline;
private KeySpline? _kKeySpline;
public KeyFrame()
{
@ -79,7 +79,7 @@ namespace Avalonia.Animation
/// Gets or sets the KeySpline of this <see cref="KeyFrame"/>.
/// </summary>
/// <value>The key spline.</value>
public KeySpline KeySpline
public KeySpline? KeySpline
{
get
{

4
src/Avalonia.Animation/KeySplineTypeConverter.cs

@ -12,12 +12,12 @@ namespace Avalonia.Animation
/// </summary>
public class KeySplineTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return KeySpline.Parse((string)value, CultureInfo.InvariantCulture);
}

21
src/Avalonia.Animation/Transition.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation.Easings;
namespace Avalonia.Animation
@ -8,7 +9,7 @@ namespace Avalonia.Animation
/// </summary>
public abstract class Transition<T> : AvaloniaObject, ITransition
{
private AvaloniaProperty _prop;
private AvaloniaProperty? _prop;
/// <summary>
/// Gets or sets the duration of the transition.
@ -26,7 +27,8 @@ namespace Avalonia.Animation
public Easing Easing { get; set; } = new LinearEasing();
/// <inheritdocs/>
public AvaloniaProperty Property
[DisallowNull]
public AvaloniaProperty? Property
{
get
{
@ -42,16 +44,25 @@ namespace Avalonia.Animation
}
}
AvaloniaProperty ITransition.Property
{
get => Property ?? throw new InvalidOperationException("Transition has no property specified.");
set => Property = value;
}
/// <summary>
/// Apply interpolation to the property.
/// </summary>
public abstract IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue);
/// <inheritdocs/>
public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue)
public virtual IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue)
{
var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue);
if (Property is null)
throw new InvalidOperationException("Transition has no property specified.");
var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue!, (T)newValue!);
return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
}
}
}
}

6
src/Avalonia.Animation/TransitionInstance.cs

@ -10,11 +10,11 @@ namespace Avalonia.Animation
/// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double>, IObserver<TimeSpan>
{
private IDisposable _timerSubscription;
private IDisposable? _timerSubscription;
private TimeSpan _delay;
private TimeSpan _duration;
private readonly IClock _baseClock;
private TransitionClock _clock;
private TransitionClock? _clock;
public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
{
@ -67,7 +67,7 @@ namespace Avalonia.Animation
protected override void Unsubscribed()
{
_timerSubscription?.Dispose();
_clock.PlayState = PlayState.Stop;
_clock!.PlayState = PlayState.Stop;
}
protected override void Subscribed()

21
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@ -59,7 +59,7 @@ namespace Avalonia.Collections
}
private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>,
IWeakSubscriber<NotifyCollectionChangedEventArgs>
IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{
private WeakReference<INotifyCollectionChanged> _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);
}
}
}

10
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -23,18 +23,16 @@ namespace Avalonia.Data.Core
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
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<INotifyPropertyChanged, PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged))
inputs.Add(WeakObservable.FromEventPattern(
inpc, WeakEvents.PropertyChanged)
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}

20
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -11,6 +11,12 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
public class IndeiValidationPlugin : IDataValidationPlugin
{
private static readonly WeakEvent<INotifyDataErrorInfo, DataErrorsChangedEventArgs>
ErrorsChangedWeakEvent = WeakEvent.Register<INotifyDataErrorInfo, DataErrorsChangedEventArgs>(
(s, h) => s.ErrorsChanged += h,
(s, h) => s.ErrorsChanged -= h
);
/// <inheritdoc/>
public bool Match(WeakReference<object?> reference, string memberName)
{
@ -25,7 +31,7 @@ namespace Avalonia.Data.Core.Plugins
return new Validator(reference, name, accessor);
}
private class Validator : DataValidationBase, IWeakSubscriber<DataErrorsChangedEventArgs>
private class Validator : DataValidationBase, IWeakEventSubscriber<DataErrorsChangedEventArgs>
{
private readonly WeakReference<object?> _reference;
private readonly string _name;
@ -37,7 +43,7 @@ namespace Avalonia.Data.Core.Plugins
_name = name;
}
void IWeakSubscriber<DataErrorsChangedEventArgs>.OnEvent(object? sender, DataErrorsChangedEventArgs e)
void IWeakEventSubscriber<DataErrorsChangedEventArgs>.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();

24
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<PropertyChangedEventArgs>
private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
{
private readonly WeakReference<object?> _reference;
private readonly PropertyInfo _property;
@ -129,7 +130,8 @@ namespace Avalonia.Data.Core.Plugins
return false;
}
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object? sender, PropertyChangedEventArgs e)
void IWeakEventSubscriber<PropertyChangedEventArgs>.
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);
}
}
}

2
src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs

@ -30,7 +30,7 @@ namespace Avalonia.Threading
/// <inheritdoc/>
public override void Post(SendOrPostCallback d, object? state)
{
Dispatcher.UIThread.Post(() => d(state), DispatcherPriority.Send);
Dispatcher.UIThread.Post(() => d(state), DispatcherPriority.Background);
}
/// <inheritdoc/>

12
src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs

@ -0,0 +1,12 @@
using System;
namespace Avalonia.Utilities;
/// <summary>
/// Defines a listener to a event subscribed vis the <see cref="WeakEvent{TTarget, TEventArgs}"/>.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
public interface IWeakEventSubscriber<in TEventArgs> where TEventArgs : EventArgs
{
void OnEvent(object? sender, WeakEvent ev, TEventArgs e);
}

187
src/Avalonia.Base/Utilities/WeakEvent.cs

@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Threading;
namespace Avalonia.Utilities;
/// <summary>
/// Manages subscriptions to events using weak listeners.
/// </summary>
public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : EventArgs where TSender : class
{
private readonly Func<TSender, EventHandler<TEventArgs>, Action> _subscribe;
readonly ConditionalWeakTable<object, Subscription> _subscriptions = new();
internal WeakEvent(
Action<TSender, EventHandler<TEventArgs>> subscribe,
Action<TSender, EventHandler<TEventArgs>> unsubscribe)
{
_subscribe = (t, s) =>
{
subscribe(t, s);
return () => unsubscribe(t, s);
};
}
internal WeakEvent(Func<TSender, EventHandler<TEventArgs>, Action> subscribe)
{
_subscribe = subscribe;
}
public void Subscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (!_subscriptions.TryGetValue(target, out var subscription))
_subscriptions.Add(target, subscription = new Subscription(this, target));
subscription.Add(new WeakReference<IWeakEventSubscriber<TEventArgs>>(subscriber));
}
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (_subscriptions.TryGetValue(target, out var subscription))
subscription.Remove(subscriber);
}
private class Subscription
{
private readonly WeakEvent<TSender, TEventArgs> _ev;
private readonly TSender _target;
private readonly Action _compact;
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data =
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16];
private int _count;
private readonly Action _unsubscribe;
private bool _compactScheduled;
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target)
{
_ev = ev;
_target = target;
_compact = Compact;
_unsubscribe = ev._subscribe(target, OnEvent);
}
void Destroy()
{
_unsubscribe();
_ev._subscriptions.Remove(_target);
}
public void Add(WeakReference<IWeakEventSubscriber<TEventArgs>> s)
{
if (_count == _data.Length)
{
//Extend capacity
var extendedData = new WeakReference<IWeakEventSubscriber<TEventArgs>>?[_data.Length * 2];
Array.Copy(_data, extendedData, _data.Length);
_data = extendedData;
}
_data[_count] = s;
_count++;
}
public void Remove(IWeakEventSubscriber<TEventArgs> 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)
{
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++)
{
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)
ScheduleCompact();
}
}
}
public class WeakEvent
{
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Action<TSender, EventHandler<TEventArgs>> subscribe,
Action<TSender, EventHandler<TEventArgs>> unsubscribe) where TSender : class where TEventArgs : EventArgs
{
return new WeakEvent<TSender, TEventArgs>(subscribe, unsubscribe);
}
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Func<TSender, EventHandler<TEventArgs>, Action> subscribe) where TSender : class where TEventArgs : EventArgs
{
return new WeakEvent<TSender, TEventArgs>(subscribe);
}
public static WeakEvent<TSender, EventArgs> Register<TSender>(
Action<TSender, EventHandler> subscribe,
Action<TSender, EventHandler> unsubscribe) where TSender : class
{
return Register<TSender, EventArgs>((s, h) =>
{
EventHandler handler = (_, e) => h(s, e);
subscribe(s, handler);
return () => unsubscribe(s, handler);
});
}
}

40
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
{
/// <summary>
/// Represents CollectionChanged event from <see cref="INotifyCollectionChanged"/>
/// </summary>
public static readonly WeakEvent<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>
CollectionChanged = WeakEvent.Register<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
(c, s) =>
{
NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e);
c.CollectionChanged += handler;
return () => c.CollectionChanged -= handler;
});
/// <summary>
/// Represents PropertyChanged event from <see cref="INotifyPropertyChanged"/>
/// </summary>
public static readonly WeakEvent<INotifyPropertyChanged, PropertyChangedEventArgs>
PropertyChanged = WeakEvent.Register<INotifyPropertyChanged, PropertyChangedEventArgs>(
(s, h) =>
{
PropertyChangedEventHandler handler = (_, e) => h(s, e);
s.PropertyChanged += handler;
return () => s.PropertyChanged -= handler;
});
/// <summary>
/// Represents CanExecuteChanged event from <see cref="ICommand"/>
/// </summary>
public static readonly WeakEvent<ICommand, EventArgs> CommandCanExecuteChanged =
WeakEvent.Register<ICommand>((s, h) => s.CanExecuteChanged += h,
(s, h) => s.CanExecuteChanged -= h);
}

35
src/Avalonia.Base/Utilities/WeakObservable.cs

@ -18,6 +18,7 @@ namespace Avalonia.Utilities
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="eventName">Name of the event to convert.</param>
/// <returns></returns>
[Obsolete("Use WeakEvent-based overload")]
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target,
string eventName)
@ -34,7 +35,9 @@ namespace Avalonia.Utilities
}).Publish().RefCount();
}
private class Handler<TEventArgs> : IWeakSubscriber<TEventArgs> where TEventArgs : EventArgs
private class Handler<TEventArgs>
: IWeakSubscriber<TEventArgs>,
IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
{
private IObserver<EventPattern<object, TEventArgs>> _observer;
@ -47,6 +50,36 @@ namespace Avalonia.Utilities
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
public void OnEvent(object? sender, WeakEvent ev, TEventArgs e)
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
}
/// <summary>
/// Converts a WeakEvent conforming to the standard .NET event pattern into an observable
/// sequence, subscribing weakly.
/// </summary>
/// <typeparam name="TTarget">The type of target.</typeparam>
/// <typeparam name="TEventArgs">The type of the event args.</typeparam>
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="ev">The weak event to convert.</param>
/// <returns></returns>
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target, WeakEvent<TTarget, TEventArgs> ev)
where TEventArgs : EventArgs where TTarget : class
{
_ = target ?? throw new ArgumentNullException(nameof(target));
_ = ev ?? throw new ArgumentNullException(nameof(ev));
return Observable.Create<EventPattern<object, TEventArgs>>(observer =>
{
var handler = new Handler<TEventArgs>(observer);
ev.Subscribe(target, handler);
return () => ev.Unsubscribe(target, handler);
}).Publish().RefCount();
}
}
}

1
src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs

@ -19,6 +19,7 @@ namespace Avalonia.Utilities
/// <param name="target">The event source.</param>
/// <param name="eventName">The name of the event.</param>
/// <param name="subscriber">The subscriber.</param>
[Obsolete("Use WeakEvent")]
public static void Subscribe<TTarget, TEventArgs>(TTarget target, string eventName, IWeakSubscriber<TEventArgs> subscriber)
where TEventArgs : EventArgs
{

1
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);

2
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -448,7 +448,7 @@ namespace Avalonia.Controls
internal set;
}
public bool IsReadOnly
public virtual bool IsReadOnly
{
get
{

72
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@ -1,4 +1,4 @@
// (c) Copyright Microsoft Corporation.
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
@ -15,7 +15,7 @@ namespace Avalonia.Controls
{
public class DataGridTemplateColumn : DataGridColumn
{
IDataTemplate _cellTemplate;
private IDataTemplate _cellTemplate;
public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellTemplateProperty =
AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>(
@ -30,17 +30,38 @@ namespace Avalonia.Controls
set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); }
}
private IDataTemplate _cellEditingCellTemplate;
/// <summary>
/// Defines the <see cref="CellEditingTemplate"/> property.
/// </summary>
public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellEditingTemplateProperty =
AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>(
nameof(CellEditingTemplate),
o => o.CellEditingTemplate,
(o, v) => o.CellEditingTemplate = v);
/// <summary>
/// Gets or sets the <see cref="IDataTemplate"/> which is used for the editing mode of the current <see cref="DataGridCell"/>
/// </summary>
/// <value>
/// An <see cref="IDataTemplate"/> for the editing mode of the current <see cref="DataGridCell"/>
/// </value>
/// <remarks>
/// If this property is <see langword="null"/> the column is read-only.
/// </remarks>
public IDataTemplate CellEditingTemplate
{
get => _cellEditingCellTemplate;
set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value);
}
private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldValue = (IDataTemplate)e.OldValue;
var value = (IDataTemplate)e.NewValue;
}
public DataGridTemplateColumn()
{
IsReadOnly = true;
}
protected override IControl GenerateElement(DataGridCell cell, object dataItem)
{
if(CellTemplate != null)
@ -60,7 +81,22 @@ 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);
}
else if (CellTemplate != null)
{
return CellTemplate.Build(dataItem);
}
if (Design.IsDesignMode)
{
return null;
}
else
{
throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn));
}
}
protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
@ -70,12 +106,30 @@ 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);
}
base.RefreshCellContent(element, propertyName);
}
public override bool IsReadOnly
{
get
{
if (CellEditingTemplate is null)
{
return true;
}
return base.IsReadOnly;
}
set
{
base.IsReadOnly = value;
}
}
}
}

7
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<System.Boolean> Avalonia.StyledProperty<System.Boolean> 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<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> 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<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> 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<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public System.Action<Avalonia.Size> 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<Avalonia.Size>)' 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<Avalonia.Controls.ResourcesChangedEventArgs>' 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.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> 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

11
src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@ -185,7 +185,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
nameof(SelectedDate),
o => o.SelectedDate,
(o, v) => o.SelectedDate = v);
(o, v) => o.SelectedDate = v,
enableDataValidation: true);
public static readonly StyledProperty<CalendarDatePickerFormat> SelectedDateFormatProperty =
AvaloniaProperty.Register<CalendarDatePicker, CalendarDatePickerFormat>(
@ -533,13 +534,11 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
base.OnPropertyChanged(change);
if (change.Property == SelectedDateProperty)
if (property == SelectedDateProperty)
{
DataValidationErrors.SetError(this, change.NewValue.Error);
DataValidationErrors.SetError(this, value.Error);
}
}

10
src/Avalonia.Controls/NativeMenuItem.cs

@ -33,7 +33,7 @@ namespace Avalonia.Controls
}
class CanExecuteChangedSubscriber : IWeakSubscriber<EventArgs>
class CanExecuteChangedSubscriber : IWeakEventSubscriber<EventArgs>
{
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();
}

2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -101,7 +101,7 @@ namespace Avalonia.Controls.Platform
root.Deactivated -= WindowDeactivated;
}
if (_root is TopLevel tl)
if (_root is TopLevel tl && tl.PlatformImpl != null)
tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus;
_inputManagerSubscription?.Dispose();

2
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -44,7 +44,7 @@ namespace Avalonia.Controls.Primitives
/// The dependency resolver to use. If null the default dependency resolver will be used.
/// </param>
public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver)
: base(impl, dependencyResolver)
: base(ValidatingPopupImpl.Wrap(impl), dependencyResolver)
{
_parent = parent;
}

32
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.
/// </summary>
public class ItemsRepeater : Panel, IChildIndexProvider
public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber<EventArgs>
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
@ -723,14 +723,8 @@ namespace Avalonia.Controls
{
oldValue.UninitializeForContext(LayoutContext);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
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<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
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<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent)
InvalidateArrange();
else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent)
InvalidateMeasure();
}
private VirtualizingLayoutContext GetLayoutContext()
{

24
src/Avalonia.Controls/TopLevel.cs

@ -34,7 +34,7 @@ namespace Avalonia.Controls
IStyleHost,
ILogicalRoot,
ITextInputMethodRoot,
IWeakSubscriber<ResourcesChangedEventArgs>
IWeakEventSubscriber<ResourcesChangedEventArgs>
{
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
@ -74,6 +74,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White);
private static readonly WeakEvent<IResourceHost, ResourcesChangedEventArgs>
ResourcesChangedWeakEvent = WeakEvent.Register<IResourceHost, ResourcesChangedEventArgs>(
(s, h) => s.ResourcesChanged += h,
(s, h) => s.ResourcesChanged -= h
);
private readonly IInputManager _inputManager;
private readonly IAccessKeyHandler _accessKeyHandler;
private readonly IKeyboardNavigationHandler _keyboardNavigationHandler;
@ -128,6 +134,8 @@ namespace Avalonia.Controls
"Could not create window implementation: maybe no windowing subsystem was initialized?");
}
impl = ValidatingToplevelImpl.Wrap(impl);
PlatformImpl = impl;
_actualTransparencyLevel = PlatformImpl.TransparencyLevel;
@ -178,10 +186,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 +291,7 @@ namespace Avalonia.Controls
/// <inheritdoc/>
IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice;
void IWeakSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, ResourcesChangedEventArgs e)
void IWeakEventSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e)
{
((ILogical)this).NotifyResourcesChanged(e);
}
@ -364,14 +369,15 @@ namespace Avalonia.Controls
Renderer?.Dispose();
Renderer = null;
(this as IInputRoot).MouseDevice?.TopLevelClosed(this);
PlatformImpl = null;
var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null);
((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
var visualArgs = new VisualTreeAttachmentEventArgs(this, this);
OnDetachedFromVisualTreeCore(visualArgs);
(this as IInputRoot).MouseDevice?.TopLevelClosed(this);
PlatformImpl = null;
OnClosed(EventArgs.Empty);
LayoutManager?.Dispose();

15
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<NotifyCollectionChangedEventArgs>, IDisposable
private class Entry : IWeakEventSubscriber<NotifyCollectionChangedEventArgs>, IDisposable
{
private INotifyCollectionChanged _collection;
@ -91,23 +91,18 @@ namespace Avalonia.Controls.Utils
{
_collection = collection;
Listeners = new List<WeakReference<ICollectionChangedListener>>();
WeakSubscriptionManager.Subscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
WeakEvents.CollectionChanged.Subscribe(_collection, this);
}
public List<WeakReference<ICollectionChangedListener>> Listeners { get; }
public void Dispose()
{
WeakSubscriptionManager.Unsubscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
WeakEvents.CollectionChanged.Unsubscribe(_collection, this);
}
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object? sender, NotifyCollectionChangedEventArgs e)
void IWeakEventSubscriber<NotifyCollectionChangedEventArgs>.
OnEvent(object? notifyCollectionChanged, WeakEvent ev, NotifyCollectionChangedEventArgs e)
{
static void Notify(
INotifyCollectionChanged incc,

326
src/Avalonia.Controls/ValidatingToplevel.cs

@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform;
using Avalonia.Rendering;
namespace Avalonia.Controls;
internal class ValidatingToplevelImpl : ITopLevelImpl, ITopLevelImplWithNativeControlHost,
ITopLevelImplWithNativeMenuExporter, ITopLevelImplWithTextInputMethod
{
private readonly ITopLevelImpl _impl;
private bool _disposed;
public ValidatingToplevelImpl(ITopLevelImpl impl)
{
_impl = impl ?? throw new InvalidOperationException(
"Could not create TopLevel implementation: maybe no windowing subsystem was initialized?");
}
public void Dispose()
{
_disposed = true;
_impl.Dispose();
}
protected void CheckDisposed()
{
if (_disposed)
throw new ObjectDisposedException(_impl.GetType().FullName);
}
protected ITopLevelImpl Inner
{
get
{
CheckDisposed();
return _impl;
}
}
public static ITopLevelImpl Wrap(ITopLevelImpl impl)
{
#if DEBUG
if (impl is ValidatingToplevelImpl)
return impl;
return new ValidatingToplevelImpl(impl);
#else
return impl;
#endif
}
public Size ClientSize => Inner.ClientSize;
public Size? FrameSize => Inner.FrameSize;
public double RenderScaling => Inner.RenderScaling;
public IEnumerable<object> Surfaces => Inner.Surfaces;
public Action<RawInputEventArgs> Input
{
get => Inner.Input;
set => Inner.Input = value;
}
public Action<Rect> Paint
{
get => Inner.Paint;
set => Inner.Paint = value;
}
public Action<Size, PlatformResizeReason> Resized
{
get => Inner.Resized;
set => Inner.Resized = value;
}
public Action<double> ScalingChanged
{
get => Inner.ScalingChanged;
set => Inner.ScalingChanged = value;
}
public Action<WindowTransparencyLevel> TransparencyLevelChanged
{
get => Inner.TransparencyLevelChanged;
set => Inner.TransparencyLevelChanged = value;
}
public IRenderer CreateRenderer(IRenderRoot root) => Inner.CreateRenderer(root);
public void Invalidate(Rect rect) => Inner.Invalidate(rect);
public void SetInputRoot(IInputRoot inputRoot) => Inner.SetInputRoot(inputRoot);
public Point PointToClient(PixelPoint point) => Inner.PointToClient(point);
public PixelPoint PointToScreen(Point point) => Inner.PointToScreen(point);
public void SetCursor(ICursorImpl cursor) => Inner.SetCursor(cursor);
public Action Closed
{
get => Inner.Closed;
set => Inner.Closed = value;
}
public Action LostFocus
{
get => Inner.LostFocus;
set => Inner.LostFocus = value;
}
// Exception: for some reason we are notifying platform mouse device from TopLevel.cs
public IMouseDevice MouseDevice => _impl.MouseDevice;
public IPopupImpl CreatePopup() => Inner.CreatePopup();
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) =>
Inner.SetTransparencyLevelHint(transparencyLevel);
public WindowTransparencyLevel TransparencyLevel => Inner.TransparencyLevel;
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => Inner.AcrylicCompensationLevels;
public INativeControlHostImpl NativeControlHost => (Inner as ITopLevelImplWithNativeControlHost)?.NativeControlHost;
public ITopLevelNativeMenuExporter NativeMenuExporter =>
(Inner as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter;
public ITextInputMethodImpl TextInputMethod => (Inner as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
}
internal class ValidatingWindowBaseImpl : ValidatingToplevelImpl, IWindowBaseImpl
{
private readonly IWindowBaseImpl _impl;
public ValidatingWindowBaseImpl(IWindowBaseImpl impl) : base(impl)
{
_impl = impl;
}
protected new IWindowBaseImpl Inner
{
get
{
CheckDisposed();
return _impl;
}
}
public static IWindowBaseImpl Wrap(IWindowBaseImpl impl)
{
#if DEBUG
if (impl is ValidatingToplevelImpl)
return impl;
return new ValidatingWindowBaseImpl(impl);
#else
return impl;
#endif
}
public void Show(bool activate, bool isDialog) => Inner.Show(activate, isDialog);
public void Hide() => Inner.Hide();
public double DesktopScaling => Inner.DesktopScaling;
public PixelPoint Position => Inner.Position;
public Action<PixelPoint> PositionChanged
{
get => Inner.PositionChanged;
set => Inner.PositionChanged = value;
}
public void Activate() => Inner.Activate();
public Action Deactivated
{
get => Inner.Deactivated;
set => Inner.Deactivated = value;
}
public Action Activated
{
get => Inner.Activated;
set => Inner.Deactivated = value;
}
public IPlatformHandle Handle => Inner.Handle;
public Size MaxAutoSizeHint => Inner.MaxAutoSizeHint;
public void SetTopmost(bool value) => Inner.SetTopmost(value);
public IScreenImpl Screen => Inner.Screen;
}
internal class ValidatingWindowImpl : ValidatingWindowBaseImpl, IWindowImpl
{
private readonly IWindowImpl _impl;
public ValidatingWindowImpl(IWindowImpl impl) : base(impl)
{
_impl = impl;
}
protected new IWindowImpl Inner
{
get
{
CheckDisposed();
return _impl;
}
}
public static IWindowImpl Wrap(IWindowImpl impl)
{
#if DEBUG
if (impl is ValidatingToplevelImpl)
return impl;
return new ValidatingWindowImpl(impl);
#else
return impl;
#endif
}
public WindowState WindowState
{
get => Inner.WindowState;
set => Inner.WindowState = value;
}
public Action<WindowState> WindowStateChanged
{
get => Inner.WindowStateChanged;
set => Inner.WindowStateChanged = value;
}
public void SetTitle(string title) => Inner.SetTitle(title);
public void SetParent(IWindowImpl parent) => Inner.SetParent(parent);
public void SetEnabled(bool enable) => Inner.SetEnabled(enable);
public Action GotInputWhenDisabled
{
get => Inner.GotInputWhenDisabled;
set => Inner.GotInputWhenDisabled = value;
}
public void SetSystemDecorations(SystemDecorations enabled) => Inner.SetSystemDecorations(enabled);
public void SetIcon(IWindowIconImpl icon) => Inner.SetIcon(icon);
public void ShowTaskbarIcon(bool value) => Inner.ShowTaskbarIcon(value);
public void CanResize(bool value) => Inner.CanResize(value);
public Func<bool> Closing
{
get => Inner.Closing;
set => Inner.Closing = value;
}
public bool IsClientAreaExtendedToDecorations => Inner.IsClientAreaExtendedToDecorations;
public Action<bool> ExtendClientAreaToDecorationsChanged
{
get => Inner.ExtendClientAreaToDecorationsChanged;
set => Inner.ExtendClientAreaToDecorationsChanged = value;
}
public bool NeedsManagedDecorations => Inner.NeedsManagedDecorations;
public Thickness ExtendedMargins => Inner.ExtendedMargins;
public Thickness OffScreenMargin => Inner.OffScreenMargin;
public void BeginMoveDrag(PointerPressedEventArgs e) => Inner.BeginMoveDrag(e);
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => Inner.BeginResizeDrag(edge, e);
public void Resize(Size clientSize, PlatformResizeReason reason) =>
Inner.Resize(clientSize, reason);
public void Move(PixelPoint point) => Inner.Move(point);
public void SetMinMaxSize(Size minSize, Size maxSize) => Inner.SetMinMaxSize(minSize, maxSize);
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) =>
Inner.SetExtendClientAreaToDecorationsHint(extendIntoClientAreaHint);
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) =>
Inner.SetExtendClientAreaChromeHints(hints);
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) =>
Inner.SetExtendClientAreaTitleBarHeightHint(titleBarHeight);
}
internal class ValidatingPopupImpl : ValidatingWindowBaseImpl, IPopupImpl
{
private readonly IPopupImpl _impl;
public ValidatingPopupImpl(IPopupImpl impl) : base(impl)
{
_impl = impl;
}
protected new IPopupImpl Inner
{
get
{
CheckDisposed();
return _impl;
}
}
public static IPopupImpl Wrap(IPopupImpl impl)
{
#if DEBUG
if (impl is ValidatingToplevelImpl)
return impl;
return new ValidatingPopupImpl(impl);
#else
return impl;
#endif
}
public IPopupPositioner PopupPositioner => Inner.PopupPositioner;
public void SetWindowManagerAddShadowHint(bool enabled) => Inner.SetWindowManagerAddShadowHint(enabled);
}

11
src/Avalonia.Controls/Window.cs

@ -237,13 +237,14 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="impl">The window implementation.</param>
public Window(IWindowImpl impl)
: base(impl)
: base(ValidatingWindowImpl.Wrap(impl))
{
impl.Closing = HandleClosing;
impl.GotInputWhenDisabled = OnGotInputWhenDisabled;
impl.WindowStateChanged = HandleWindowStateChanged;
var wrapped = (IWindowImpl)base.PlatformImpl!;
wrapped.Closing = HandleClosing;
wrapped.GotInputWhenDisabled = OnGotInputWhenDisabled;
wrapped.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
wrapped.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application));
PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);

9
src/Avalonia.Controls/WindowBase.cs

@ -57,12 +57,13 @@ namespace Avalonia.Controls
{
}
public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyResolver) : base(impl, dependencyResolver)
public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyResolver) : base(ValidatingWindowBaseImpl.Wrap(impl), dependencyResolver)
{
Screens = new Screens(PlatformImpl?.Screen);
impl.Activated = HandleActivated;
impl.Deactivated = HandleDeactivated;
impl.PositionChanged = HandlePositionChanged;
var wrapped = PlatformImpl!;
wrapped.Activated = HandleActivated;
wrapped.Deactivated = HandleDeactivated;
wrapped.PositionChanged = HandlePositionChanged;
}
/// <summary>

2
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml

@ -95,7 +95,7 @@
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >
<TextBlock Text="Chat Room | " />
<Button Classes="Hyperlink" CommandParameter="https://gitter.im/AvaloniaUI/Avalonia/" />
<Button Classes="Hyperlink" CommandParameter="https://t.me/Avalonia" />
</StackPanel>
</StackPanel>
<StackPanel VerticalAlignment="Bottom" Margin="10">

3
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<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
Total Issues: 1

1
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -5,6 +5,7 @@
<ItemGroup>
<AvaloniaResource Include="Assets\*" />
<AvaloniaResource Include="**/*.xaml" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />

6
src/Avalonia.Input/KeyBinding.cs

@ -35,9 +35,11 @@ namespace Avalonia.Input
{
if (Gesture?.Matches(args) == true)
{
args.Handled = true;
if (Command?.CanExecute(CommandParameter) == true)
if (Command?.CanExecute(CommandParameter) == true)
{
args.Handled = true;
Command.Execute(CommandParameter);
}
}
}
}

17
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
/// </summary>
public event EventHandler? MeasureInvalidated;
/// <summary>
/// Occurs when the measurement state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> MeasureInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.MeasureInvalidated += h,
(s, h) => s.MeasureInvalidated -= h);
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public event EventHandler? ArrangeInvalidated;
/// <summary>
/// Occurs when the arrange state (layout) has been invalidated.
/// </summary>
public static readonly WeakEvent<AttachedLayout, EventArgs> ArrangeInvalidatedWeakEvent =
WeakEvent.Register<AttachedLayout>(
(s, h) => s.ArrangeInvalidated += h,
(s, h) => s.ArrangeInvalidated -= h);
/// <summary>
/// Initializes any per-container state the layout requires when it is attached to an

115
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -70,81 +70,83 @@ namespace Avalonia.Native
var result = new NativeMenu();
var aboutItem = new NativeMenuItem("About Avalonia");
aboutItem.Click += async (sender, e) =>
aboutItem.Click += async (_, _) =>
{
var dialog = new AboutAvaloniaDialog();
var mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
await dialog.ShowDialog(mainWindow);
if (Application.Current is
{ ApplicationLifetime: IClassicDesktopStyleApplicationLifetime { MainWindow: { } mainWindow } })
{
await dialog.ShowDialog(mainWindow);
}
};
result.Add(aboutItem);
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
if (macOpts == null || !macOpts.DisableDefaultApplicationMenuItems)
{
result.Add(new NativeMenuItemSeparator());
return result;
}
var servicesMenu = new NativeMenuItem("Services");
servicesMenu.Menu = new NativeMenu
{
[MacOSNativeMenuCommands.IsServicesSubmenuProperty] = true
};
result.Add(servicesMenu);
private void PopulateStandardOSXMenuItems(NativeMenu appMenu)
{
appMenu.Add(new NativeMenuItemSeparator());
result.Add(new NativeMenuItemSeparator());
var servicesMenu = new NativeMenuItem("Services");
servicesMenu.Menu = new NativeMenu { [MacOSNativeMenuCommands.IsServicesSubmenuProperty] = true };
var hideItem = new NativeMenuItem("Hide " + Application.Current.Name)
{
Gesture = new KeyGesture(Key.H, KeyModifiers.Meta)
};
hideItem.Click += (sender, args) =>
{
_applicationCommands.HideApp();
};
result.Add(hideItem);
appMenu.Add(servicesMenu);
appMenu.Add(new NativeMenuItemSeparator());
var hideOthersItem = new NativeMenuItem("Hide Others")
{
Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta | KeyModifiers.Alt)
};
hideOthersItem.Click += (sender, args) =>
{
_applicationCommands.HideOthers();
};
result.Add(hideOthersItem);
var hideItem = new NativeMenuItem("Hide " + (Application.Current?.Name ?? "Application"))
{
Gesture = new KeyGesture(Key.H, KeyModifiers.Meta)
};
hideItem.Click += (_, _) =>
{
_applicationCommands.HideApp();
};
var showAllItem = new NativeMenuItem("Show All");
showAllItem.Click += (sender, args) =>
{
_applicationCommands.ShowAll();
};
result.Add(showAllItem);
appMenu.Add(hideItem);
result.Add(new NativeMenuItemSeparator());
var hideOthersItem = new NativeMenuItem("Hide Others")
{
Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta | KeyModifiers.Alt)
};
hideOthersItem.Click += (_, _) =>
{
_applicationCommands.HideOthers();
};
appMenu.Add(hideOthersItem);
var quitItem = new NativeMenuItem("Quit")
{
Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta)
};
quitItem.Click += (sender, args) =>
{
_applicationCommands.ShowAll();
};
result.Add(quitItem);
}
var showAllItem = new NativeMenuItem("Show All");
showAllItem.Click += (_, _) =>
{
_applicationCommands.ShowAll();
};
appMenu.Add(showAllItem);
return result;
appMenu.Add(new NativeMenuItemSeparator());
var quitItem = new NativeMenuItem("Quit") { Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta) };
quitItem.Click += (_, _) =>
{
if (Application.Current is { ApplicationLifetime: IControlledApplicationLifetime lifetime })
{
lifetime.Shutdown();
}
};
appMenu.Add(quitItem);
}
private void DoLayoutReset(bool forceUpdate = false)
{
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions();
if (macOpts != null && macOpts.DisableNativeMenus)
if (macOpts.DisableNativeMenus)
{
return;
}
@ -220,6 +222,13 @@ namespace Avalonia.Native
_nativeMenu.Initialize(this, appMenuHolder, "");
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
if (macOpts == null || !macOpts.DisableDefaultApplicationMenuItems)
{
PopulateStandardOSXMenuItems(menu);
}
setMenu = true;
}

4
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<MacOSPlatformOptions>();
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions();
_factory.MacOptions.SetShowInDock(macOpts?.ShowInDock != false ? 1 : 0);
_factory.MacOptions.SetShowInDock(macOpts.ShowInDock ? 1 : 0);
}
AvaloniaLocator.CurrentMutable

28
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)

2
src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml

@ -11,7 +11,7 @@
<Setter Property="MinWidth" Value="40"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="Margin" Value="1"/>
<Setter Property="Padding" Value="0,0,0,4"/>
<Setter Property="Padding" Value="0,0,0,0"/>
<!--These are actually set on the CalendarView in WinUI-->
<Setter Property="Foreground" Value="{DynamicResource CalendarViewCalendarItemForeground}"/>
<Setter Property="Background" Value="{DynamicResource CalendarViewCalendarItemRevealBackground}"/>

6
src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml

@ -33,6 +33,7 @@
<Setter Property="Template">
<ControlTemplate>
<DataValidationErrors>
<Grid ColumnDefinitions="*,Auto">
<Grid.Styles>
@ -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}" />
</Popup>
</Grid>
</DataValidationErrors>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="CalendarDatePicker:error TextBox /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlErrorTextForegroundBrush}"/>
</Style>
</Styles>

2
src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml

@ -11,7 +11,7 @@
<Setter Property="MinWidth" Value="40"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="Margin" Value="1"/>
<Setter Property="Padding" Value="0,0,0,4"/>
<Setter Property="Padding" Value="0,0,0,0"/>
<!--These are actually set on the CalendarView in WinUI-->
<Setter Property="Foreground" Value="{DynamicResource CalendarViewCalendarItemForeground}"/>
<Setter Property="Background" Value="{DynamicResource CalendarViewCalendarItemRevealBackground}"/>

1
src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml

@ -32,6 +32,7 @@
<Style Selector="Button.CalendarHeader">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="20" />
<Setter Property="Background" Value="{DynamicResource CalendarViewNavigationButtonBackground}"/>
<Setter Property="Template">

1
src/Avalonia.Themes.Fluent/Controls/ListBox.xaml

@ -20,6 +20,7 @@
<Setter Property="Template">
<ControlTemplate>
<Border Name="border"
ClipToBounds="{TemplateBinding ClipToBounds}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"

4
src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml

@ -38,7 +38,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="0"
MinWidth="{TemplateBinding MinWidth}"
MinWidth="0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
AllowSpin="{TemplateBinding AllowSpin}"
@ -50,7 +50,7 @@
BorderBrush="Transparent"
Margin="-1"
Padding="{TemplateBinding Padding}"
MinWidth="{TemplateBinding MinWidth}"
MinWidth="0"
Foreground="{TemplateBinding Foreground}"
FontSize="{TemplateBinding FontSize}"
Watermark="{TemplateBinding Watermark}"

7
src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs

@ -38,8 +38,8 @@ namespace Avalonia.Animation.Animators
}
/// <inheritdoc/>
public override IDisposable Apply(Animation animation, Animatable control, IClock clock,
IObservable<bool> match, Action onComplete)
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
IObservable<bool> match, Action? onComplete)
{
if (TryCreateCustomRegisteredAnimator(out var animator)
|| TryCreateGradientAnimator(out animator)
@ -135,9 +135,8 @@ namespace Avalonia.Animation.Animators
private bool TryCreateCustomRegisteredAnimator([NotNullWhen(true)] out IAnimator? animator)
{
if (_brushAnimators.Count > 0)
if (_brushAnimators.Count > 0 && this[0].Value?.GetType() is Type firstKeyType)
{
var firstKeyType = this[0].Value.GetType();
foreach (var (match, animatorType) in _brushAnimators)
{
if (!match(firstKeyType))

5
src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs

@ -58,6 +58,11 @@ namespace Avalonia.Animation.Animators
public override IDisposable BindAnimation(Animatable control, IObservable<IGradientBrush?> instance)
{
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation);
}

5
src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs

@ -24,6 +24,11 @@ namespace Avalonia.Animation.Animators
public override IDisposable BindAnimation(Animatable control, IObservable<ISolidColorBrush?> instance)
{
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation);
}
}

7
src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs

@ -14,10 +14,15 @@ namespace Avalonia.Animation.Animators
DoubleAnimator? _doubleAnimator;
/// <inheritdoc/>
public override IDisposable? Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> obsMatch, Action onComplete)
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> obsMatch, Action? onComplete)
{
var ctrl = (Visual)control;
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
// Check if the Target Property is Transform derived.
if (typeof(Transform).IsAssignableFrom(Property.OwnerType))
{

5
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<T>(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

118
src/Avalonia.Visuals/Media/Pen.cs

@ -7,7 +7,7 @@ namespace Avalonia.Media
/// <summary>
/// Describes how a stroke is drawn.
/// </summary>
public class Pen : AvaloniaObject, IPen
public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber<EventArgs>
{
/// <summary>
/// Defines the <see cref="Brush"/> property.
@ -45,6 +45,10 @@ namespace Avalonia.Media
public static readonly StyledProperty<double> MiterLimitProperty =
AvaloniaProperty.Register<Pen, double>(nameof(MiterLimit), 10.0);
private EventHandler? _invalidated;
private IAffectsRender? _subscribedToBrush;
private IAffectsRender? _subscribedToDashes;
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
@ -96,17 +100,6 @@ namespace Avalonia.Media
DashStyle = dashStyle;
}
static Pen()
{
AffectsRender<Pen>(
BrushProperty,
ThicknessProperty,
DashStyleProperty,
LineCapProperty,
LineJoinProperty,
MiterLimitProperty);
}
/// <summary>
/// Gets or sets the brush used to draw the stroke.
/// </summary>
@ -116,6 +109,11 @@ namespace Avalonia.Media
set => SetValue(BrushProperty, value);
}
private static readonly WeakEvent<IAffectsRender, EventArgs> InvalidatedWeakEvent =
WeakEvent.Register<IAffectsRender>(
(s, h) => s.Invalidated += h,
(s, h) => s.Invalidated -= h);
/// <summary>
/// Gets or sets the stroke thickness.
/// </summary>
@ -165,7 +163,19 @@ namespace Avalonia.Media
/// <summary>
/// Raised when the pen changes.
/// </summary>
public event EventHandler? Invalidated;
public event EventHandler? Invalidated
{
add
{
_invalidated += value;
UpdateSubscriptions();
}
remove
{
_invalidated -= value;
UpdateSubscriptions();
}
}
/// <summary>
/// Creates an immutable clone of the brush.
@ -182,68 +192,42 @@ namespace Avalonia.Media
MiterLimit);
}
/// <summary>
/// Marks a property as affecting the pen's visual representation.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a pen's static constructor, any change to the
/// property will cause the <see cref="Invalidated"/> event to be raised on the pen.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : Pen
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> 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)
UpdateSubscription(ref _subscribedToBrush, Brush);
if(change.Property == DashStyleProperty)
UpdateSubscription(ref _subscribedToDashes, DashStyle);
base.OnPropertyChanged(change);
}
static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e)
void UpdateSubscription(ref IAffectsRender? field, object? value)
{
if ((_invalidated == null || field != value) && field != null)
{
if (e.Sender is T sender)
{
if (e.OldValue is IAffectsRender oldValue)
{
WeakEventHandlerManager.Unsubscribe<EventArgs, T>(
oldValue,
nameof(oldValue.Invalidated),
sender.AffectsRenderInvalidated);
}
if (e.NewValue is IAffectsRender newValue)
{
WeakEventHandlerManager.Subscribe<IAffectsRender, EventArgs, T>(
newValue,
nameof(newValue.Invalidated),
sender.AffectsRenderInvalidated);
}
sender.RaiseInvalidated(EventArgs.Empty);
}
InvalidatedWeakEvent.Unsubscribe(field, this);
field = null;
}
foreach (var property in properties)
if (_invalidated != null && field != value && value is IAffectsRender affectsRender)
{
if (property.CanValueAffectRender())
{
property.Changed.Subscribe(e => InvalidateAndSubscribe(e));
}
else
{
property.Changed.Subscribe(e => Invalidate(e));
}
InvalidatedWeakEvent.Subscribe(affectsRender, this);
field = affectsRender;
}
}
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
private void AffectsRenderInvalidated(object? sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty);
void UpdateSubscriptions()
{
UpdateSubscription(ref _subscribedToBrush, Brush);
UpdateSubscription(ref _subscribedToDashes, DashStyle);
}
void IWeakEventSubscriber<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
if (ev == InvalidatedWeakEvent)
_invalidated?.Invoke(this, EventArgs.Empty);
}
}
}

7
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);
}

4
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<X11PlatformOptions>()
?.GlxRendererBlacklist;
var opts = AvaloniaLocator.Current.GetService<X11PlatformOptions>() ?? new X11PlatformOptions();
var blacklist = opts.GlxRendererBlacklist;
if (blacklist != null)
foreach (var item in blacklist)
if (glInterface.Renderer.Contains(item))

13
src/Avalonia.X11/X11Window.cs

@ -341,9 +341,13 @@ namespace Avalonia.X11
public Action<WindowTransparencyLevel> TransparencyLevelChanged
{
get => _transparencyHelper.TransparencyLevelChanged;
set => _transparencyHelper.TransparencyLevelChanged = value;
}
get => _transparencyHelper?.TransparencyLevelChanged;
set
{
if (_transparencyHelper != null)
_transparencyHelper.TransparencyLevelChanged = value;
}
}
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
@ -1163,7 +1167,8 @@ namespace Avalonia.X11
{
}
public WindowTransparencyLevel TransparencyLevel => _transparencyHelper.CurrentLevel;
public WindowTransparencyLevel TransparencyLevel =>
_transparencyHelper?.CurrentLevel ?? WindowTransparencyLevel.None;
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8);

4
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -38,11 +38,11 @@ namespace Avalonia.LinuxFramebuffer
if (_fb is IGlOutputBackend gl)
AvaloniaLocator.CurrentMutable.Bind<IPlatformOpenGlInterface>().ToConstant(gl.PlatformOpenGlInterface);
var opts = AvaloniaLocator.Current.GetService<LinuxFramebufferPlatformOptions>();
var opts = AvaloniaLocator.Current.GetService<LinuxFramebufferPlatformOptions>() ?? new LinuxFramebufferPlatformOptions();
AvaloniaLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>().ToConstant(Threading)
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(opts?.Fps ?? 60))
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(opts.Fps))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())

36
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<PropertyChangedEventArgs>
{
protected readonly WeakReference<object> _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<PropertyChangedEventArgs, InpcPropertyAccessor>(
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<INotifyPropertyChanged, PropertyChangedEventArgs, InpcPropertyAccessor>(
inpc,
nameof(INotifyPropertyChanged.PropertyChanged),
OnNotifyPropertyChanged);
}
WeakEvents.PropertyChanged.Subscribe(inpc, this);
}
}
internal class IndexerAccessor : InpcPropertyAccessor
internal class IndexerAccessor : InpcPropertyAccessor, IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{
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<INotifyCollectionChanged, NotifyCollectionChangedEventArgs, IndexerAccessor>(
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<NotifyCollectionChangedEventArgs, IndexerAccessor>(
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))
{

13
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -459,8 +459,6 @@ namespace Avalonia.Skia
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
ConfigureTextRendering(paintWrapper);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)glyphRun.BaselineOrigin.X,
(float)glyphRun.BaselineOrigin.Y, paintWrapper.Paint);
}
@ -482,7 +480,7 @@ namespace Avalonia.Skia
public void PushClip(RoundedRect clip)
{
Canvas.Save();
Canvas.ClipRoundRect(clip.ToSKRoundRect());
Canvas.ClipRoundRect(clip.ToSKRoundRect(), antialias:true);
}
/// <inheritdoc />
@ -606,15 +604,6 @@ namespace Avalonia.Skia
}
}
internal void ConfigureTextRendering(PaintWrapper wrapper)
{
var paint = wrapper.Paint;
paint.IsEmbeddedBitmapText = true;
paint.SubpixelText = true;
paint.LcdRenderText = _canTextUseLcdRendering;
}
/// <summary>
/// Configure paint wrapper for using gradient brush.
/// </summary>

2
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -291,7 +291,7 @@ namespace Avalonia.Skia
{
AvaloniaFormattedTextLine line = _skiaLines[c];
float x = TransformX(origin.X, 0, paint.TextAlign);
float x = TransformX(origin.X, line.Width, paint.TextAlign);
if (!hasCusomFGBrushes)
{

2
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -198,7 +198,7 @@ namespace Avalonia.Skia
private static readonly SKFont s_font = new SKFont
{
Subpixel = true,
Edging = SKFontEdging.Antialias,
Edging = SKFontEdging.SubpixelAntialias,
Hinting = SKFontHinting.Full,
LinearMetrics = true
};

6
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);

8
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;
}
}

9
src/Windows/Avalonia.Win32/Win32GlManager.cs

@ -13,19 +13,18 @@ namespace Avalonia.Win32
{
AvaloniaLocator.CurrentMutable.Bind<IPlatformOpenGlInterface>().ToLazy<IPlatformOpenGlInterface>(() =>
{
var opts = AvaloniaLocator.Current.GetService<Win32PlatformOptions>();
if (opts?.UseWgl == true)
var opts = AvaloniaLocator.Current.GetService<Win32PlatformOptions>() ?? 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);
}

25
tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

@ -36,7 +36,7 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Transition_Is_Not_Applied_To_Initial_Style()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
using (Start())
{
var target = CreateTarget();
var control = new Control
@ -74,6 +74,7 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Transition_Is_Applied_When_Local_Value_Changes()
{
using var app = Start();
var target = CreateTarget();
var control = CreateControl(target.Object);
@ -170,6 +171,7 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present()
{
using var app = Start();
var target = CreateTarget();
var control = CreateControl(target.Object);
@ -195,6 +197,7 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Transition_Is_Disposed_When_Local_Value_Changes()
{
using var app = Start();
var target = CreateTarget();
var control = CreateControl(target.Object);
var sub = new Mock<IDisposable>();
@ -211,6 +214,7 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void New_Transition_Is_Applied_When_Local_Value_Changes()
{
using var app = Start();
var target = CreateTarget();
var control = CreateControl(target.Object);
@ -239,6 +243,7 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree()
{
using var app = Start();
var target = CreateTarget();
var control = CreateControl(target.Object);
@ -266,6 +271,7 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Animation_Is_Cancelled_When_Transition_Removed()
{
using var app = Start();
var target = CreateTarget();
var control = CreateControl(target.Object);
var sub = new Mock<IDisposable>();
@ -285,7 +291,7 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Animation_Is_Cancelled_When_New_Style_Activates()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
using (Start())
{
var target = CreateTarget();
var control = CreateStyledControl(target.Object);
@ -301,7 +307,7 @@ namespace Avalonia.Animation.UnitTests
target.Verify(x => x.Apply(
control,
It.IsAny<Clock>(),
It.IsAny<IClock>(),
1.0,
0.5),
Times.Once);
@ -315,7 +321,7 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Transition_From_Style_Trigger_Is_Applied()
{
using (UnitTestApplication.Start(TestServices.RealStyler))
using (Start())
{
var target = CreateTransition(Control.WidthProperty);
var control = CreateStyledControl(transition2: target.Object);
@ -326,7 +332,7 @@ namespace Avalonia.Animation.UnitTests
target.Verify(x => x.Apply(
control,
It.IsAny<Clock>(),
It.IsAny<IClock>(),
double.NaN,
100.0),
Times.Once);
@ -337,7 +343,7 @@ namespace Avalonia.Animation.UnitTests
public void Replacing_Transitions_During_Animation_Does_Not_Throw_KeyNotFound()
{
// Issue #4059
using (UnitTestApplication.Start(TestServices.RealStyler))
using (Start())
{
Border target;
var clock = new TestClock();
@ -428,6 +434,13 @@ namespace Avalonia.Animation.UnitTests
control.EndBatchUpdate();
}
private static IDisposable Start()
{
var clock = new MockGlobalClock();
var services = TestServices.RealStyler.With(globalClock: clock);
return UnitTestApplication.Start(services);
}
private static Mock<ITransition> CreateTarget()
{
return CreateTransition(Visual.OpacityProperty);

3
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);
}

12
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);

11
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);

27
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);

3
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);
}

75
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 WeakEventTests
{
class EventSource
{
public event EventHandler Event;
public void Fire()
{
Event?.Invoke(this, new EventArgs());
}
public static readonly WeakEvent<EventSource, EventArgs> WeakEv = WeakEvent.Register<EventSource>(
(t, s) => t.Event += s,
(t, s) => t.Event -= s);
}
class Subscriber : IWeakEventSubscriber<EventArgs>
{
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.WeakEv.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.WeakEv.Subscribe(source, new Subscriber(func));
}
}
}

5
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);
}

7
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);
}

15
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);

Loading…
Cancel
Save