Browse Source

Merge branch 'master' into fixes/x11-transparency-crash

pull/7370/head
Max Katz 4 years ago
committed by GitHub
parent
commit
a02c2fa234
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      samples/ControlCatalog/App.xaml
  2. 15
      samples/ControlCatalog/Models/Person.cs
  3. 12
      samples/ControlCatalog/Pages/DataGridPage.xaml
  4. 22
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  5. 16
      src/Avalonia.Animation/Animatable.cs
  6. 46
      src/Avalonia.Animation/Animation.cs
  7. 33
      src/Avalonia.Animation/AnimationInstance`1.cs
  8. 20
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  9. 9
      src/Avalonia.Animation/Animators/Animator`1.cs
  10. 4
      src/Avalonia.Animation/Avalonia.Animation.csproj
  11. 2
      src/Avalonia.Animation/Clock.cs
  12. 8
      src/Avalonia.Animation/Cue.cs
  13. 8
      src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs
  14. 4
      src/Avalonia.Animation/Easing/Easing.cs
  15. 4
      src/Avalonia.Animation/Easing/EasingTypeConverter.cs
  16. 2
      src/Avalonia.Animation/IAnimation.cs
  17. 4
      src/Avalonia.Animation/IAnimationSetter.cs
  18. 4
      src/Avalonia.Animation/IAnimator.cs
  19. 2
      src/Avalonia.Animation/ITransition.cs
  20. 2
      src/Avalonia.Animation/IterationCount.cs
  21. 4
      src/Avalonia.Animation/IterationCountTypeConverter.cs
  22. 4
      src/Avalonia.Animation/KeyFrame.cs
  23. 4
      src/Avalonia.Animation/KeySplineTypeConverter.cs
  24. 21
      src/Avalonia.Animation/Transition.cs
  25. 6
      src/Avalonia.Animation/TransitionInstance.cs
  26. 2
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  27. 1
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  28. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  29. 72
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  30. 2
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
  31. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  32. 6
      src/Avalonia.Input/KeyBinding.cs
  33. 111
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  34. 2
      src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml
  35. 2
      src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml
  36. 1
      src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
  37. 1
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  38. 7
      src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs
  39. 5
      src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs
  40. 5
      src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
  41. 7
      src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
  42. 5
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  43. 118
      src/Avalonia.Visuals/Media/Pen.cs
  44. 2
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  45. 25
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

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)

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

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

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/>

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

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">

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

111
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -70,74 +70,76 @@ 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>() ?? new MacOSPlatformOptions();
if (!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);
var hideOthersItem = new NativeMenuItem("Hide Others")
{
Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta | KeyModifiers.Alt)
};
hideOthersItem.Click += (_, _) =>
{
_applicationCommands.HideOthers();
};
appMenu.Add(hideOthersItem);
result.Add(new NativeMenuItemSeparator());
var showAllItem = new NativeMenuItem("Show All");
showAllItem.Click += (_, _) =>
{
_applicationCommands.ShowAll();
};
var quitItem = new NativeMenuItem("Quit")
{
Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta)
};
quitItem.Click += (sender, args) =>
{
(Application.Current.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown();
};
result.Add(quitItem);
}
appMenu.Add(showAllItem);
appMenu.Add(new NativeMenuItemSeparator());
return result;
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)
@ -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;
}

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}"/>

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}"

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

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

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

Loading…
Cancel
Save