From 6ce1d94a090b76d29aec46f2b0eab09e16b66404 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 28 Jun 2020 12:59:53 +0200 Subject: [PATCH 1/8] Remove a few warnings. --- src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs | 7 +------ src/Avalonia.Styling/IStyledElement.cs | 5 ----- src/Avalonia.Styling/StyledElement.cs | 1 - src/Avalonia.Styling/Styling/Styles.cs | 1 - 4 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs index 6ed6c2ef52..4d82381323 100644 --- a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -1,7 +1,4 @@ -using System; -using Avalonia.Data; - -#nullable enable +#nullable enable namespace Avalonia.PropertyStore { @@ -10,8 +7,6 @@ namespace Avalonia.PropertyStore /// internal interface IPriorityValueEntry : IValue { - BindingPriority Priority { get; } - void Reparent(IValueSink sink); } diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs index 610c743a3e..37e6ed6fbb 100644 --- a/src/Avalonia.Styling/IStyledElement.cs +++ b/src/Avalonia.Styling/IStyledElement.cs @@ -17,11 +17,6 @@ namespace Avalonia /// event EventHandler Initialized; - /// - /// Raised when resources on the element are changed. - /// - event EventHandler ResourcesChanged; - /// /// Gets a value that indicates whether the element has finished initialization. /// diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 05e031c9ec..65885ddebe 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -67,7 +67,6 @@ namespace Avalonia private List? _appliedStyles; private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; - private bool _notifyingResourcesChanged; /// /// Initializes static members of the class. diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 7c79060930..c752bdfeb8 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -21,7 +21,6 @@ namespace Avalonia.Styling private IResourceHost? _owner; private IResourceDictionary? _resources; private Dictionary?>? _cache; - private bool _notifyingResourcesChanged; public Styles() { From 128fbee51fc77b2b931be1cded41fd723921dc40 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 28 Jun 2020 13:51:34 +0200 Subject: [PATCH 2/8] Remove nullable and cref warnings. --- src/Avalonia.Base/AvaloniaProperty.cs | 4 +-- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 4 +-- .../Collections/Pooled/IReadOnlyPooledList.cs | 2 ++ .../Collections/Pooled/PooledList.cs | 3 +- src/Avalonia.Base/Data/BindingValue.cs | 36 +++++++++---------- src/Avalonia.Base/Data/Optional.cs | 10 +++--- src/Avalonia.Base/DirectPropertyBase.cs | 2 +- src/Avalonia.Base/IStyledPropertyMetadata.cs | 2 -- .../PropertyStore/LocalValueEntry.cs | 4 +-- src/Avalonia.Base/StyledPropertyMetadata`1.cs | 3 +- src/Avalonia.Base/ValueStore.cs | 2 +- 11 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index daa7191cc5..09480f2701 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -159,8 +159,6 @@ namespace Avalonia /// internal int Id { get; } - internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false; - /// /// Provides access to a property's binding via the /// indexer. @@ -512,7 +510,7 @@ namespace Avalonia /// /// An if setting the property can be undone, otherwise null. /// - internal abstract IDisposable? RouteSetValue( + internal abstract IDisposable RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority); diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 4a3b104f2a..4cde965400 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -362,7 +362,7 @@ namespace Avalonia /// The property. /// /// You won't usually want to call this method directly, instead use the - /// + /// /// method. /// public void Register(Type type, AvaloniaProperty property) @@ -413,7 +413,7 @@ namespace Avalonia /// The property. /// /// You won't usually want to call this method directly, instead use the - /// + /// /// method. /// public void RegisterAttached(Type type, AvaloniaProperty property) diff --git a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs index 9bc3609dc5..7a233a62ab 100644 --- a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs @@ -13,9 +13,11 @@ namespace Avalonia.Collections.Pooled public interface IReadOnlyPooledList : IReadOnlyList { +#pragma warning disable CS0419 /// /// Gets a for the items currently in the collection. /// +#pragma warning restore CS0419 ReadOnlySpan Span { get; } } } diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index f0d6b292cc..e50e100d32 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -138,7 +138,6 @@ namespace Avalonia.Collections.Pooled /// initially empty, but will have room for the given number of elements /// before any reallocations are required. /// - /// If true, Count of list equals capacity. Depending on ClearMode, rented items may or may not hold dirty values. public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool, bool sizeToCapacity) { if (capacity < 0) @@ -499,11 +498,13 @@ namespace Avalonia.Collections.Pooled public void AddRange(T[] array) => AddRange(array.AsSpan()); +#pragma warning disable CS0419 /// /// Adds the elements of the given to the end of this list. If /// required, the capacity of the list is increased to twice the previous /// capacity or the new size, whichever is larger. /// +#pragma warning restore CS0419 public void AddRange(ReadOnlySpan span) { var newSpan = InsertSpan(_size, span.Length, false); diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 9aac1bacba..56f37ff89c 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -190,7 +190,7 @@ namespace Avalonia.Data /// Gets the value of the binding value if present, otherwise the default value. /// /// The value. - public T GetValueOrDefault() => HasValue ? _value : default; + public T GetValueOrDefault() => HasValue ? _value : default!; /// /// Gets the value of the binding value if present, otherwise a default value. @@ -209,8 +209,8 @@ namespace Avalonia.Data public TResult GetValueOrDefault() { return HasValue ? - _value is TResult result ? result : default - : default; + _value is TResult result ? result : default! + : default!; } /// @@ -225,7 +225,7 @@ namespace Avalonia.Data public TResult GetValueOrDefault(TResult defaultValue) { return HasValue ? - _value is TResult result ? result : default + _value is TResult result ? result : default! : defaultValue; } @@ -242,7 +242,7 @@ namespace Avalonia.Data UnsetValueType _ => Unset, DoNothingType _ => DoNothing, BindingNotification n => n.ToBindingValue().Cast(), - _ => (T)value + _ => (T)value! }; } @@ -259,18 +259,18 @@ namespace Avalonia.Data public static implicit operator BindingValue(Optional optional) { - return optional.HasValue ? optional.Value : Unset; + return optional.HasValue ? optional.Value! : Unset; } /// /// Returns a binding value with a type of . /// - public static BindingValue Unset => new BindingValue(BindingValueType.UnsetValue, default, null); + public static BindingValue Unset => new BindingValue(BindingValueType.UnsetValue, default!, null); /// /// Returns a binding value with a type of . /// - public static BindingValue DoNothing => new BindingValue(BindingValueType.DoNothing, default, null); + public static BindingValue DoNothing => new BindingValue(BindingValueType.DoNothing, default!, null); /// /// Returns a binding value with a type of . @@ -278,9 +278,9 @@ namespace Avalonia.Data /// The binding error. public static BindingValue BindingError(Exception e) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); - return new BindingValue(BindingValueType.BindingError, default, e); + return new BindingValue(BindingValueType.BindingError, default!, e); } /// @@ -290,7 +290,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue BindingError(Exception e, T fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.BindingErrorWithFallback, fallbackValue, e); } @@ -303,13 +303,13 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue BindingError(Exception e, Optional fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue( fallbackValue.HasValue ? BindingValueType.BindingErrorWithFallback : BindingValueType.BindingError, - fallbackValue.HasValue ? fallbackValue.Value : default, + fallbackValue.HasValue ? fallbackValue.Value : default!, e); } @@ -319,9 +319,9 @@ namespace Avalonia.Data /// The data validation error. public static BindingValue DataValidationError(Exception e) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); - return new BindingValue(BindingValueType.DataValidationError, default, e); + return new BindingValue(BindingValueType.DataValidationError, default!, e); } /// @@ -331,7 +331,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue DataValidationError(Exception e, T fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e); } @@ -344,13 +344,13 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue DataValidationError(Exception e, Optional fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue( fallbackValue.HasValue ? BindingValueType.DataValidationErrorWithFallback : BindingValueType.DataValidationError, - fallbackValue.HasValue ? fallbackValue.Value : default, + fallbackValue.HasValue ? fallbackValue.Value : default!, e); } diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index dd952c895c..cc064e0faa 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -60,7 +60,7 @@ namespace Avalonia.Data /// Casts the value (if any) to an . /// /// The cast optional value. - public Optional ToObject() => HasValue ? new Optional(_value) : default; + public Optional ToObject() => HasValue ? new Optional(_value!) : default; /// public override string ToString() => HasValue ? _value?.ToString() ?? "(null)" : "(empty)"; @@ -69,7 +69,7 @@ namespace Avalonia.Data /// Gets the value if present, otherwise the default value. /// /// The value. - public T GetValueOrDefault() => HasValue ? _value : default; + public T GetValueOrDefault() => HasValue ? _value : default!; /// /// Gets the value if present, otherwise a default value. @@ -88,8 +88,8 @@ namespace Avalonia.Data public TResult GetValueOrDefault() { return HasValue ? - _value is TResult result ? result : default - : default; + _value is TResult result ? result : default! + : default!; } /// @@ -104,7 +104,7 @@ namespace Avalonia.Data public TResult GetValueOrDefault(TResult defaultValue) { return HasValue ? - _value is TResult result ? result : default + _value is TResult result ? result : default! : defaultValue; } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 0e65379abd..b59e86f8d5 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -122,7 +122,7 @@ namespace Avalonia internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) { - return o.GetValue(this); + return o.GetValue(this)!; } /// diff --git a/src/Avalonia.Base/IStyledPropertyMetadata.cs b/src/Avalonia.Base/IStyledPropertyMetadata.cs index f567cd930c..a68b65e5e0 100644 --- a/src/Avalonia.Base/IStyledPropertyMetadata.cs +++ b/src/Avalonia.Base/IStyledPropertyMetadata.cs @@ -1,5 +1,3 @@ -using System; - namespace Avalonia { /// diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 59c017bc09..5d3930469d 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -15,11 +15,11 @@ namespace Avalonia.PropertyStore public LocalValueEntry(T value) => _value = value; public BindingPriority Priority => BindingPriority.LocalValue; - Optional IValue.GetValue() => new Optional(_value); + Optional IValue.GetValue() => new Optional(_value!); public Optional GetValue(BindingPriority maxPriority) { - return BindingPriority.LocalValue >= maxPriority ? _value : Optional.Empty; + return BindingPriority.LocalValue >= maxPriority ? _value! : Optional.Empty; } public void SetValue(T value) => _value = value; diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 300548db0a..cf0a0c34ec 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using Avalonia.Data; namespace Avalonia @@ -35,7 +34,7 @@ namespace Avalonia /// /// Gets the value coercion callback, if any. /// - public Func? CoerceValue { get; private set; } + public Func CoerceValue { get; private set; } object IStyledPropertyMetadata.DefaultValue => DefaultValue; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 05e66f2e0a..0e87a1f242 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -162,7 +162,7 @@ namespace Avalonia _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( _owner, property, - old, + old!, default, BindingPriority.Unset)); } From 6f7dac83d18b2a18a0a7ab9e7cef186f70622ae1 Mon Sep 17 00:00:00 2001 From: Maksym Katsydan Date: Wed, 1 Jul 2020 19:53:34 -0400 Subject: [PATCH 3/8] Add focus-visible support with tests --- src/Avalonia.Input/InputElement.cs | 7 +- .../InputElement_Focus.cs | 79 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 407b28b665..0616c70d82 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -158,6 +158,7 @@ namespace Avalonia.Input private bool _isEffectivelyEnabled = true; private bool _isFocused; + private bool _isFocusVisible; private bool _isPointerOver; private GestureRecognizerCollection _gestureRecognizers; @@ -427,7 +428,9 @@ namespace Avalonia.Input /// The event args. protected virtual void OnGotFocus(GotFocusEventArgs e) { - IsFocused = e.Source == this; + var isFocused = e.Source == this; + _isFocusVisible = isFocused && (e.NavigationMethod == NavigationMethod.Directional || e.NavigationMethod == NavigationMethod.Tab); + IsFocused = isFocused; } /// @@ -436,6 +439,7 @@ namespace Avalonia.Input /// The event args. protected virtual void OnLostFocus(RoutedEventArgs e) { + _isFocusVisible = false; IsFocused = false; } @@ -602,6 +606,7 @@ namespace Avalonia.Input if (isFocused.HasValue) { PseudoClasses.Set(":focus", isFocused.Value); + PseudoClasses.Set(":focus-visible", _isFocusVisible); } if (isPointerOver.HasValue) diff --git a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs index d396119555..09fae7207f 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs @@ -42,5 +42,84 @@ namespace Avalonia.Input.UnitTests Assert.Null(FocusManager.Instance.Current); } } + + [Fact] + public void Focus_Pseudoclass_Should_Be_Applied_On_Focus() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target1 = new Decorator(); + var target2 = new Decorator(); + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + target1, + target2 + } + } + }; + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + + FocusManager.Instance?.Focus(target1); + Assert.True(target1.IsFocused); + Assert.True(target1.Classes.Contains(":focus")); + Assert.False(target2.IsFocused); + Assert.False(target2.Classes.Contains(":focus")); + + FocusManager.Instance?.Focus(target2, NavigationMethod.Tab); + Assert.False(target1.IsFocused); + Assert.False(target1.Classes.Contains(":focus")); + Assert.True(target2.IsFocused); + Assert.True(target2.Classes.Contains(":focus")); + } + } + + [Fact] + public void Control_FocusVsisible_Pseudoclass_Should_Be_Applied_On_Tab_And_DirectionalFocus() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target1 = new Decorator(); + var target2 = new Decorator(); + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + target1, + target2 + } + } + }; + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + FocusManager.Instance?.Focus(target1); + Assert.True(target1.IsFocused); + Assert.False(target1.Classes.Contains(":focus-visible")); + Assert.False(target2.IsFocused); + Assert.False(target2.Classes.Contains(":focus-visible")); + + FocusManager.Instance?.Focus(target2, NavigationMethod.Tab); + Assert.False(target1.IsFocused); + Assert.False(target1.Classes.Contains(":focus-visible")); + Assert.True(target2.IsFocused); + Assert.True(target2.Classes.Contains(":focus-visible")); + + FocusManager.Instance?.Focus(target1, NavigationMethod.Directional); + Assert.True(target1.IsFocused); + Assert.True(target1.Classes.Contains(":focus-visible")); + Assert.False(target2.IsFocused); + Assert.False(target2.Classes.Contains(":focus-visible")); + } + } } } From de6553a0dc52917c84198f10f420ea903fe884ed Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jul 2020 11:15:56 +0200 Subject: [PATCH 4/8] Added nullable attributes and use them instead of `!`. --- src/Avalonia.Base/Data/BindingValue.cs | 44 +++--- src/Avalonia.Base/Data/Optional.cs | 26 ++-- src/Avalonia.Base/DirectPropertyBase.cs | 4 +- .../Metadata/NullableAttributes.cs | 140 ++++++++++++++++++ .../PropertyStore/LocalValueEntry.cs | 11 +- src/Avalonia.Base/ValueStore.cs | 2 +- 6 files changed, 188 insertions(+), 39 deletions(-) create mode 100644 src/Avalonia.Base/Metadata/NullableAttributes.cs diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 56f37ff89c..6e3c9ae67b 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Utilities; #nullable enable @@ -81,14 +82,14 @@ namespace Avalonia.Data /// public readonly struct BindingValue { - private readonly T _value; + [AllowNull] private readonly T _value; /// /// Initializes a new instance of the struct with a type of /// /// /// The value. - public BindingValue(T value) + public BindingValue([AllowNull] T value) { ValidateValue(value); _value = value; @@ -96,7 +97,7 @@ namespace Avalonia.Data Error = null; } - private BindingValue(BindingValueType type, T value, Exception? error) + private BindingValue(BindingValueType type, [AllowNull] T value, Exception? error) { _value = value; Type = type; @@ -154,7 +155,7 @@ namespace Avalonia.Data BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue, BindingValueType.DoNothing => BindingOperations.DoNothing, BindingValueType.Value => _value, - BindingValueType.BindingError => + BindingValueType.BindingError => new BindingNotification(Error, BindingErrorType.Error), BindingValueType.BindingErrorWithFallback => new BindingNotification(Error, BindingErrorType.Error, Value), @@ -175,7 +176,7 @@ namespace Avalonia.Data /// The binding type is or /// . /// - public BindingValue WithValue(T value) + public BindingValue WithValue([AllowNull] T value) { if (Type == BindingValueType.DoNothing) { @@ -190,7 +191,8 @@ namespace Avalonia.Data /// Gets the value of the binding value if present, otherwise the default value. /// /// The value. - public T GetValueOrDefault() => HasValue ? _value : default!; + [return: MaybeNull] + public T GetValueOrDefault() => HasValue ? _value : default; /// /// Gets the value of the binding value if present, otherwise a default value. @@ -206,11 +208,12 @@ namespace Avalonia.Data /// The value if present and of the correct type, `default(TResult)` if the value is /// not present or of an incorrect type. /// + [return: MaybeNull] public TResult GetValueOrDefault() { return HasValue ? - _value is TResult result ? result : default! - : default!; + _value is TResult result ? result : default + : default; } /// @@ -222,10 +225,11 @@ namespace Avalonia.Data /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult GetValueOrDefault(TResult defaultValue) + [return: MaybeNull] + public TResult GetValueOrDefault([AllowNull] TResult defaultValue) { return HasValue ? - _value is TResult result ? result : default! + _value is TResult result ? result : default : defaultValue; } @@ -242,7 +246,7 @@ namespace Avalonia.Data UnsetValueType _ => Unset, DoNothingType _ => DoNothing, BindingNotification n => n.ToBindingValue().Cast(), - _ => (T)value! + _ => new BindingValue((T)value) }; } @@ -250,7 +254,7 @@ namespace Avalonia.Data /// Creates a binding value from an instance of the underlying value type. /// /// The value. - public static implicit operator BindingValue(T value) => new BindingValue(value); + public static implicit operator BindingValue([AllowNull] T value) => new BindingValue(value); /// /// Creates a binding value from an . @@ -259,18 +263,18 @@ namespace Avalonia.Data public static implicit operator BindingValue(Optional optional) { - return optional.HasValue ? optional.Value! : Unset; + return optional.HasValue ? optional.Value : Unset; } /// /// Returns a binding value with a type of . /// - public static BindingValue Unset => new BindingValue(BindingValueType.UnsetValue, default!, null); + public static BindingValue Unset => new BindingValue(BindingValueType.UnsetValue, default, null); /// /// Returns a binding value with a type of . /// - public static BindingValue DoNothing => new BindingValue(BindingValueType.DoNothing, default!, null); + public static BindingValue DoNothing => new BindingValue(BindingValueType.DoNothing, default, null); /// /// Returns a binding value with a type of . @@ -280,7 +284,7 @@ namespace Avalonia.Data { e = e ?? throw new ArgumentNullException(nameof(e)); - return new BindingValue(BindingValueType.BindingError, default!, e); + return new BindingValue(BindingValueType.BindingError, default, e); } /// @@ -309,7 +313,7 @@ namespace Avalonia.Data fallbackValue.HasValue ? BindingValueType.BindingErrorWithFallback : BindingValueType.BindingError, - fallbackValue.HasValue ? fallbackValue.Value : default!, + fallbackValue.HasValue ? fallbackValue.Value : default, e); } @@ -321,7 +325,7 @@ namespace Avalonia.Data { e = e ?? throw new ArgumentNullException(nameof(e)); - return new BindingValue(BindingValueType.DataValidationError, default!, e); + return new BindingValue(BindingValueType.DataValidationError, default, e); } /// @@ -350,11 +354,11 @@ namespace Avalonia.Data fallbackValue.HasValue ? BindingValueType.DataValidationErrorWithFallback : BindingValueType.DataValidationError, - fallbackValue.HasValue ? fallbackValue.Value : default!, + fallbackValue.HasValue ? fallbackValue.Value : default, e); } - private static void ValidateValue(T value) + private static void ValidateValue([AllowNull] T value) { if (value is UnsetValueType) { diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index cc064e0faa..8e044d7896 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; #nullable enable @@ -22,13 +23,13 @@ namespace Avalonia.Data /// public readonly struct Optional : IEquatable> { - private readonly T _value; + [AllowNull] private readonly T _value; /// /// Initializes a new instance of the struct with value. /// /// The value. - public Optional(T value) + public Optional([AllowNull] T value) { _value = value; HasValue = true; @@ -48,7 +49,7 @@ namespace Avalonia.Data public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value."); /// - public override bool Equals(object obj) => obj is Optional o && this == o; + public override bool Equals(object? obj) => obj is Optional o && this == o; /// public bool Equals(Optional other) => this == other; @@ -60,7 +61,7 @@ namespace Avalonia.Data /// Casts the value (if any) to an . /// /// The cast optional value. - public Optional ToObject() => HasValue ? new Optional(_value!) : default; + public Optional ToObject() => HasValue ? new Optional(_value) : default; /// public override string ToString() => HasValue ? _value?.ToString() ?? "(null)" : "(empty)"; @@ -69,7 +70,8 @@ namespace Avalonia.Data /// Gets the value if present, otherwise the default value. /// /// The value. - public T GetValueOrDefault() => HasValue ? _value : default!; + [return: MaybeNull] + public T GetValueOrDefault() => HasValue ? _value : default; /// /// Gets the value if present, otherwise a default value. @@ -85,11 +87,12 @@ namespace Avalonia.Data /// The value if present and of the correct type, `default(TResult)` if the value is /// not present or of an incorrect type. /// + [return: MaybeNull] public TResult GetValueOrDefault() { return HasValue ? - _value is TResult result ? result : default! - : default!; + _value is TResult result ? result : default + : default; } /// @@ -101,10 +104,11 @@ namespace Avalonia.Data /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult GetValueOrDefault(TResult defaultValue) + [return: MaybeNull] + public TResult GetValueOrDefault([AllowNull] TResult defaultValue) { return HasValue ? - _value is TResult result ? result : default! + _value is TResult result ? result : default : defaultValue; } @@ -112,7 +116,7 @@ namespace Avalonia.Data /// Creates an from an instance of the underlying value type. /// /// The value. - public static implicit operator Optional(T value) => new Optional(value); + public static implicit operator Optional([AllowNull] T value) => new Optional(value); /// /// Compares two s for inequality. @@ -128,7 +132,7 @@ namespace Avalonia.Data /// The first value. /// The second value. /// True if the values are equal; otherwise false. - public static bool operator==(Optional x, Optional y) + public static bool operator ==(Optional x, Optional y) { if (!x.HasValue && !y.HasValue) { diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index b59e86f8d5..d42c030245 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -120,9 +120,9 @@ namespace Avalonia return o.GetValue(this); } - internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) { - return o.GetValue(this)!; + return o.GetValue(this); } /// diff --git a/src/Avalonia.Base/Metadata/NullableAttributes.cs b/src/Avalonia.Base/Metadata/NullableAttributes.cs new file mode 100644 index 0000000000..91f5e81863 --- /dev/null +++ b/src/Avalonia.Base/Metadata/NullableAttributes.cs @@ -0,0 +1,140 @@ +#pragma warning disable MA0048 // File name must match type name +#define INTERNAL_NULLABLE_ATTRIBUTES +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +} +#endif diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 5d3930469d..859e9ba81c 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -1,4 +1,5 @@ -using Avalonia.Data; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; #nullable enable @@ -11,15 +12,15 @@ namespace Avalonia.PropertyStore /// The property type. internal class LocalValueEntry : IValue { - private T _value; + [AllowNull] private T _value; - public LocalValueEntry(T value) => _value = value; + public LocalValueEntry([AllowNull] T value) => _value = value; public BindingPriority Priority => BindingPriority.LocalValue; - Optional IValue.GetValue() => new Optional(_value!); + Optional IValue.GetValue() => new Optional(_value); public Optional GetValue(BindingPriority maxPriority) { - return BindingPriority.LocalValue >= maxPriority ? _value! : Optional.Empty; + return BindingPriority.LocalValue >= maxPriority ? _value : Optional.Empty; } public void SetValue(T value) => _value = value; diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 0e87a1f242..6b89fcbdb9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -162,7 +162,7 @@ namespace Avalonia _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( _owner, property, - old!, + new Optional(old), default, BindingPriority.Unset)); } From a5f43c7ee1b07d852f275f61500bf3dd7862a7ec Mon Sep 17 00:00:00 2001 From: Mihai Stan Date: Thu, 2 Jul 2020 14:49:20 +0200 Subject: [PATCH 5/8] Add ticks property on Slider This property will allow setting custom ticks on the slider without having to create a custom template for the TickBar. --- samples/ControlCatalog/Pages/SliderPage.xaml | 20 ++++++++++++++----- .../ControlCatalog/Pages/SliderPage.xaml.cs | 11 ++++++++++ src/Avalonia.Controls/Slider.cs | 19 +++++++++++++++++- src/Avalonia.Themes.Default/Slider.xaml | 9 ++++++--- src/Avalonia.Themes.Fluent/Slider.xaml | 1 + 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index c6f5521e60..ca289e21fd 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -6,11 +6,21 @@ A control that lets the user select from a range of values by moving a Thumb control along a Track. - + + + + ("CustomTickedSlider"); + slider.Ticks = new List + { + 0d, + 5d, + 20d, + 50d, + 100d + }; } } } diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index fe1a4f5ac1..00a26a4c91 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -64,6 +65,12 @@ namespace Avalonia.Controls public static readonly StyledProperty TickPlacementProperty = AvaloniaProperty.Register(nameof(TickPlacement), 0d); + /// + /// Defines the property. + /// + public static readonly StyledProperty> TicksProperty = + TickBar.TicksProperty.AddOwner(); + // Slider required parts private bool _isDragging = false; private Track _track; @@ -83,7 +90,8 @@ namespace Avalonia.Controls PressedMixin.Attach(); OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); Thumb.DragStartedEvent.AddClassHandler((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); - Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); + Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), + RoutingStrategies.Bubble); } /// @@ -94,6 +102,15 @@ namespace Avalonia.Controls UpdatePseudoClasses(Orientation); } + /// + /// Defines the ticks to be drawn on the tick bar. + /// + public List Ticks + { + get => GetValue(TicksProperty); + set => SetValue(TicksProperty, value); + } + /// /// Gets or sets the orientation of a . /// diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index 1d48a946fc..ba4b1ee998 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -87,7 +87,10 @@ - + + diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index 539c448e0f..099c2000b8 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -182,6 +182,7 @@ From 6561cb739ad0ea3ae5292e3e17d84e75a01e0e94 Mon Sep 17 00:00:00 2001 From: Mihai Stan Date: Thu, 2 Jul 2020 14:50:17 +0200 Subject: [PATCH 6/8] Snap thumb on custom ticks when they are set --- src/Avalonia.Controls/Slider.cs | 47 +++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 00a26a4c91..b94f9bb7c9 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -257,19 +257,50 @@ namespace Avalonia.Controls /// Value that want to snap to closest Tick. private double SnapToTick(double value) { - var previous = Minimum; - var next = Maximum; - - if (TickFrequency > 0.0) + if (IsSnapToTickEnabled) { - previous = Minimum + (Math.Round((value - Minimum) / TickFrequency) * TickFrequency); - next = Math.Min(Maximum, previous + TickFrequency); + double previous = Minimum; + double next = Maximum; + + // This property is rarely set so let's try to avoid the GetValue + List ticks = Ticks; + + // If ticks collection is available, use it. + // Note that ticks may be unsorted. + if ((ticks != null) && (ticks.Count > 0)) + { + for (int i = 0; i < ticks.Count; i++) + { + double tick = ticks[i]; + if (MathUtilities.AreClose(tick, value)) + { + return value; + } + + if (MathUtilities.LessThan(tick, value) && MathUtilities.GreaterThan(tick, previous)) + { + previous = tick; + } + else if (MathUtilities.GreaterThan(tick, value) && MathUtilities.LessThan(tick, next)) + { + next = tick; + } + } + } + else if (MathUtilities.GreaterThan(TickFrequency, 0.0)) + { + previous = Minimum + (Math.Round(((value - Minimum) / TickFrequency)) * TickFrequency); + next = Math.Min(Maximum, previous + TickFrequency); + } + + // Choose the closest value between previous and next. If tie, snap to 'next'. + value = MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous; } - // Choose the closest value between previous and next. If tie, snap to 'next'. - return MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous; + return value; } + private void UpdatePseudoClasses(Orientation o) { PseudoClasses.Set(":vertical", o == Orientation.Vertical); From d8d9a1865dc4abe7466a43f14edbab7262222f7a Mon Sep 17 00:00:00 2001 From: Mihai Stan Date: Thu, 2 Jul 2020 16:34:28 +0200 Subject: [PATCH 7/8] Use AvaloniaList instead of List --- samples/ControlCatalog/Pages/SliderPage.xaml | 1 + samples/ControlCatalog/Pages/SliderPage.xaml.cs | 11 ----------- src/Avalonia.Controls/Slider.cs | 8 ++++---- src/Avalonia.Controls/TickBar.cs | 12 ++++-------- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index ca289e21fd..ea31ed0050 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -19,6 +19,7 @@ Maximum="100" TickPlacement="BottomRight" IsSnapToTickEnabled="True" + Ticks="0,20,25,40,75,100" Width="300" /> ("CustomTickedSlider"); - slider.Ticks = new List - { - 0d, - 5d, - 20d, - 50d, - 100d - }; } } } diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index b94f9bb7c9..293cbac82f 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -68,7 +68,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty> TicksProperty = + public static readonly StyledProperty> TicksProperty = TickBar.TicksProperty.AddOwner(); // Slider required parts @@ -105,7 +105,7 @@ namespace Avalonia.Controls /// /// Defines the ticks to be drawn on the tick bar. /// - public List Ticks + public AvaloniaList Ticks { get => GetValue(TicksProperty); set => SetValue(TicksProperty, value); @@ -263,7 +263,7 @@ namespace Avalonia.Controls double next = Maximum; // This property is rarely set so let's try to avoid the GetValue - List ticks = Ticks; + var ticks = Ticks; // If ticks collection is available, use it. // Note that ticks may be unsorted. diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index 16e063beb3..22145d8742 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Data.Converters; +using Avalonia.Collections; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Utilities; @@ -135,15 +131,15 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty> TicksProperty = - AvaloniaProperty.Register>(nameof(Ticks)); + public static readonly StyledProperty> TicksProperty = + AvaloniaProperty.Register>(nameof(Ticks)); /// /// The Ticks property contains collection of value of type Double which /// are the logical positions use to draw the ticks. /// The property value is a . /// - public List Ticks + public AvaloniaList Ticks { get { return GetValue(TicksProperty); } set { SetValue(TicksProperty, value); } From bd2fe20a78e43d7a985bdfdb9d6ffec2609401a7 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 3 Jul 2020 11:02:17 +0200 Subject: [PATCH 8/8] Checking for IsTabStop --- src/Avalonia.Input/Navigation/TabNavigation.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index dd50ea438a..cd377f1df6 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -219,7 +219,9 @@ namespace Avalonia.Input.Navigation if (parent != null) { - if (direction == NavigationDirection.Previous && parent.CanFocus()) + if (direction == NavigationDirection.Previous && + parent.CanFocus() && + KeyboardNavigation.GetIsTabStop((InputElement) parent)) { return parent; }