Browse Source

Merge branch 'master' into fixes/2821-remove-contentcontrolmixin

pull/2926/head
danwalmsley 7 years ago
committed by GitHub
parent
commit
cfba46c509
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      build/SharedVersion.props
  2. 1
      scripts/ReplaceNugetCache.sh
  3. 17
      src/Avalonia.Animation/Animatable.cs
  4. 37
      src/Avalonia.Base/AvaloniaObject.cs
  5. 44
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  6. 28
      src/Avalonia.Base/BoxedValue.cs
  7. 16
      src/Avalonia.Base/PriorityBindingEntry.cs
  8. 14
      src/Avalonia.Base/PriorityValue.cs
  9. 11
      src/Avalonia.Base/StyledPropertyBase.cs
  10. 14
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  11. 30
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  12. 7
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  13. 32
      src/Avalonia.Input/Gestures.cs
  14. 4
      src/Avalonia.Interactivity/EventSubscription.cs
  15. 101
      src/Avalonia.Interactivity/Interactive.cs
  16. 50
      src/Avalonia.Interactivity/RoutedEvent.cs
  17. 23
      src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs
  18. 19
      src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs
  19. 30
      src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
  20. 160
      src/Avalonia.Visuals/Vector.cs
  21. 9
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs
  22. 46
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs
  23. 112
      tests/Avalonia.Visuals.UnitTests/VectorTests.cs

9
build/SharedVersion.props

@ -4,11 +4,16 @@
<Product>Avalonia</Product> <Product>Avalonia</Product>
<Version>0.8.999</Version> <Version>0.8.999</Version>
<Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright> <Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
<PackageLicenseUrl>https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md</PackageLicenseUrl> <PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<PackageProjectUrl>https://github.com/AvaloniaUI/Avalonia/</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl> <RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591</NoWarn> <NoWarn>CS1591</NoWarn>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIconUrl>https://avatars2.githubusercontent.com/u/14075148?s=200</PackageIconUrl>
<PackageDescription>Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS.</PackageDescription>
<PackageTags>avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin</PackageTags>
<PackageReleaseNotes>https://github.com/AvaloniaUI/Avalonia/releases</PackageReleaseNotes>
<RepositoryType>git</RepositoryType>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

1
scripts/ReplaceNugetCache.sh

@ -2,7 +2,6 @@
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.gtk3/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/

17
src/Avalonia.Animation/Animatable.cs

@ -45,16 +45,17 @@ namespace Avalonia.Animation
{ {
get get
{ {
if (_transitions == null) if (_transitions is null)
_transitions = new Transitions(); _transitions = new Transitions();
if (_previousTransitions == null) if (_previousTransitions is null)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>(); _previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>();
return _transitions; return _transitions;
} }
set set
{ {
SetAndRaise(TransitionsProperty, ref _transitions, value); SetAndRaise(TransitionsProperty, ref _transitions, value);
} }
} }
@ -66,18 +67,20 @@ namespace Avalonia.Animation
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{ {
if (e.Priority != BindingPriority.Animation && Transitions != null && _previousTransitions != null) if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return;
{
var match = Transitions.FirstOrDefault(x => x.Property == e.Property);
if (match != null) // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations).
foreach (var transition in Transitions)
{
if (transition.Property == e.Property)
{ {
if (_previousTransitions.TryGetValue(e.Property, out var dispose)) if (_previousTransitions.TryGetValue(e.Property, out var dispose))
dispose.Dispose(); dispose.Dispose();
var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
_previousTransitions[e.Property] = instance; _previousTransitions[e.Property] = instance;
return;
} }
} }
} }

37
src/Avalonia.Base/AvaloniaObject.cs

@ -82,6 +82,7 @@ namespace Avalonia
set set
{ {
VerifyAccess();
if (_inheritanceParent != value) if (_inheritanceParent != value)
{ {
if (_inheritanceParent != null) if (_inheritanceParent != null)
@ -89,25 +90,33 @@ namespace Avalonia
_inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged;
} }
var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this) var oldInheritanceParent = _inheritanceParent;
.Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType()));
var inherited = (from property in properties
where property.Inherits
select new
{
Property = property,
Value = GetValue(property),
}).ToList();
_inheritanceParent = value; _inheritanceParent = value;
var valuestore = _values;
foreach (var i in inherited) foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()))
{ {
object newValue = GetValue(i.Property); if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue)
{
// if local value set there can be no change
continue;
}
// get the value as it would have been with the previous InheritanceParent
object oldValue;
if (oldInheritanceParent is AvaloniaObject aobj)
{
oldValue = aobj.GetValueOrDefaultUnchecked(property);
}
else
{
oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
}
object newValue = GetDefaultValue(property);
if (!Equals(i.Value, newValue)) if (!Equals(oldValue, newValue))
{ {
RaisePropertyChanged(i.Property, i.Value, newValue, BindingPriority.LocalValue); RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue);
} }
} }

44
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -26,6 +26,8 @@ namespace Avalonia
new Dictionary<Type, List<AvaloniaProperty>>(); new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache = private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
new Dictionary<Type, List<PropertyInitializationData>>(); new Dictionary<Type, List<PropertyInitializationData>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
new Dictionary<Type, List<AvaloniaProperty>>();
/// <summary> /// <summary>
/// Gets the <see cref="AvaloniaPropertyRegistry"/> instance /// Gets the <see cref="AvaloniaPropertyRegistry"/> instance
@ -103,6 +105,46 @@ namespace Avalonia
return result; return result;
} }
/// <summary>
/// Gets all inherited <see cref="AvaloniaProperty"/>s registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredInherited(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
if (_inheritedCache.TryGetValue(type, out var result))
{
return result;
}
result = new List<AvaloniaProperty>();
var visited = new HashSet<AvaloniaProperty>();
foreach (var property in GetRegistered(type))
{
if (property.Inherits)
{
result.Add(property);
visited.Add(property);
}
}
foreach (var property in GetRegisteredAttached(type))
{
if (property.Inherits)
{
if (!visited.Contains(property))
{
result.Add(property);
}
}
}
_inheritedCache.Add(type, result);
return result;
}
/// <summary> /// <summary>
/// Gets all <see cref="AvaloniaProperty"/>s registered on a object. /// Gets all <see cref="AvaloniaProperty"/>s registered on a object.
/// </summary> /// </summary>
@ -230,6 +272,7 @@ namespace Avalonia
_registeredCache.Clear(); _registeredCache.Clear();
_initializedCache.Clear(); _initializedCache.Clear();
_inheritedCache.Clear();
} }
/// <summary> /// <summary>
@ -266,6 +309,7 @@ namespace Avalonia
_attachedCache.Clear(); _attachedCache.Clear();
_initializedCache.Clear(); _initializedCache.Clear();
_inheritedCache.Clear();
} }
internal void NotifyInitialized(AvaloniaObject o) internal void NotifyInitialized(AvaloniaObject o)

28
src/Avalonia.Base/BoxedValue.cs

@ -0,0 +1,28 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia
{
/// <summary>
/// Represents boxed value of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Type of stored value.</typeparam>
internal readonly struct BoxedValue<T>
{
public BoxedValue(T value)
{
Boxed = value;
Typed = value;
}
/// <summary>
/// Boxed value.
/// </summary>
public object Boxed { get; }
/// <summary>
/// Typed value.
/// </summary>
public T Typed { get; }
}
}

16
src/Avalonia.Base/PriorityBindingEntry.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Runtime.ExceptionServices;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Threading; using Avalonia.Threading;
@ -10,9 +11,9 @@ namespace Avalonia
/// <summary> /// <summary>
/// A registered binding in a <see cref="PriorityValue"/>. /// A registered binding in a <see cref="PriorityValue"/>.
/// </summary> /// </summary>
internal class PriorityBindingEntry : IDisposable internal class PriorityBindingEntry : IDisposable, IObserver<object>
{ {
private PriorityLevel _owner; private readonly PriorityLevel _owner;
private IDisposable _subscription; private IDisposable _subscription;
/// <summary> /// <summary>
@ -85,7 +86,7 @@ namespace Avalonia
Description = ((IDescription)binding).Description; Description = ((IDescription)binding).Description;
} }
_subscription = binding.Subscribe(ValueChanged, Completed); _subscription = binding.Subscribe(this);
} }
/// <summary> /// <summary>
@ -96,7 +97,7 @@ namespace Avalonia
_subscription?.Dispose(); _subscription?.Dispose();
} }
private void ValueChanged(object value) void IObserver<object>.OnNext(object value)
{ {
void Signal() void Signal()
{ {
@ -132,7 +133,7 @@ namespace Avalonia
} }
} }
private void Completed() void IObserver<object>.OnCompleted()
{ {
HasCompleted = true; HasCompleted = true;
@ -145,5 +146,10 @@ namespace Avalonia
Dispatcher.UIThread.Post(() => _owner.Completed(this)); Dispatcher.UIThread.Post(() => _owner.Completed(this));
} }
} }
void IObserver<object>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
} }
} }

14
src/Avalonia.Base/PriorityValue.cs

@ -24,13 +24,11 @@ namespace Avalonia
/// <see cref="IPriorityValueOwner.Changed"/> method on the /// <see cref="IPriorityValueOwner.Changed"/> method on the
/// owner object is fired with the old and new values. /// owner object is fired with the old and new values.
/// </remarks> /// </remarks>
internal class PriorityValue internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)>
{ {
private readonly Type _valueType; private readonly Type _valueType;
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>(); private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate; private readonly Func<object, object> _validate;
private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
private (object value, int priority) _value; private (object value, int priority) _value;
private DeferredSetter<object> _setter; private DeferredSetter<object> _setter;
@ -52,7 +50,6 @@ namespace Avalonia
_valueType = valueType; _valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue); _value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate; _validate = validate;
_setAndNotifyCallback = SetAndNotify;
} }
/// <summary> /// <summary>
@ -257,10 +254,15 @@ namespace Avalonia
_setter = Owner.GetNonDirectDeferredSetter(Property); _setter = Owner.GetNonDirectDeferredSetter(Property);
} }
_setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue); _setter.SetAndNotifyCallback(Property, this, ref _value, newValue);
}
void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value)
{
SetAndNotify(ref backing, value);
} }
private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update) private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update)
{ {
var val = update.value; var val = update.value;
var notification = val as BindingNotification; var notification = val as BindingNotification;

11
src/Avalonia.Base/StyledPropertyBase.cs

@ -68,7 +68,7 @@ namespace Avalonia
{ {
Contract.Requires<ArgumentNullException>(type != null); Contract.Requires<ArgumentNullException>(type != null);
return GetMetadata(type).DefaultValue; return GetMetadata(type).DefaultValue.Typed;
} }
/// <summary> /// <summary>
@ -164,7 +164,14 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultValue(type); object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
private object GetDefaultBoxedValue(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
return GetMetadata(type).DefaultValue.Boxed;
}
[DebuggerHidden] [DebuggerHidden]
private Func<IAvaloniaObject, TValue, TValue> Cast<THost>(Func<THost, TValue, TValue> validate) private Func<IAvaloniaObject, TValue, TValue> Cast<THost>(Func<THost, TValue, TValue> validate)

14
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -19,26 +19,26 @@ namespace Avalonia
/// <param name="validate">A validation function.</param> /// <param name="validate">A validation function.</param>
/// <param name="defaultBindingMode">The default binding mode.</param> /// <param name="defaultBindingMode">The default binding mode.</param>
public StyledPropertyMetadata( public StyledPropertyMetadata(
TValue defaultValue = default(TValue), TValue defaultValue = default,
Func<IAvaloniaObject, TValue, TValue> validate = null, Func<IAvaloniaObject, TValue, TValue> validate = null,
BindingMode defaultBindingMode = BindingMode.Default) BindingMode defaultBindingMode = BindingMode.Default)
: base(defaultBindingMode) : base(defaultBindingMode)
{ {
DefaultValue = defaultValue; DefaultValue = new BoxedValue<TValue>(defaultValue);
Validate = validate; Validate = validate;
} }
/// <summary> /// <summary>
/// Gets the default value for the property. /// Gets the default value for the property.
/// </summary> /// </summary>
public TValue DefaultValue { get; private set; } internal BoxedValue<TValue> DefaultValue { get; private set; }
/// <summary> /// <summary>
/// Gets the validation callback. /// Gets the validation callback.
/// </summary> /// </summary>
public Func<IAvaloniaObject, TValue, TValue> Validate { get; private set; } public Func<IAvaloniaObject, TValue, TValue> Validate { get; private set; }
object IStyledPropertyMetadata.DefaultValue => DefaultValue; object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
Func<IAvaloniaObject, object, object> IStyledPropertyMetadata.Validate => Cast(Validate); Func<IAvaloniaObject, object, object> IStyledPropertyMetadata.Validate => Cast(Validate);
@ -47,11 +47,9 @@ namespace Avalonia
{ {
base.Merge(baseMetadata, property); base.Merge(baseMetadata, property);
var src = baseMetadata as StyledPropertyMetadata<TValue>; if (baseMetadata is StyledPropertyMetadata<TValue> src)
if (src != null)
{ {
if (DefaultValue == null) if (DefaultValue.Boxed == null)
{ {
DefaultValue = src.DefaultValue; DefaultValue = src.DefaultValue;
} }

30
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -5,15 +5,6 @@ using System;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
/// <summary>
/// Callback invoked when deferred setter wants to set a value.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value);
/// <summary> /// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent. /// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855. /// Used to fix #855.
@ -70,14 +61,14 @@ namespace Avalonia.Utilities
return false; return false;
} }
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value) public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, ISetAndNotifyHandler<TValue> setAndNotifyHandler, ref TValue backing, TValue value)
where TValue : TSetRecord where TValue : TSetRecord
{ {
if (!_isNotifying) if (!_isNotifying)
{ {
using (new NotifyDisposable(this)) using (new NotifyDisposable(this))
{ {
setAndNotifyCallback(property, ref backing, value); setAndNotifyHandler.HandleSetAndNotify(property, ref backing, value);
} }
if (!_pendingValues.Empty) if (!_pendingValues.Empty)
@ -86,7 +77,7 @@ namespace Avalonia.Utilities
{ {
while (!_pendingValues.Empty) while (!_pendingValues.Empty)
{ {
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); setAndNotifyHandler.HandleSetAndNotify(property, ref backing, (TValue)_pendingValues.Dequeue());
} }
} }
} }
@ -119,4 +110,19 @@ namespace Avalonia.Utilities
} }
} }
} }
/// <summary>
/// Handler for set and notify requests.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
internal interface ISetAndNotifyHandler<TValue>
{
/// <summary>
/// Handles deferred setter requests to set a value.
/// </summary>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
void HandleSetAndNotify(AvaloniaProperty property, ref TValue backing, TValue value);
}
} }

7
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -28,6 +28,11 @@ namespace Avalonia
{ {
Diagnostics.DevTools.Attach(control, gesture); Diagnostics.DevTools.Attach(control, gesture);
} }
public static void OpenDevTools(this TopLevel control)
{
Diagnostics.DevTools.OpenDevTools(control);
}
} }
} }
@ -73,7 +78,7 @@ namespace Avalonia.Diagnostics
RoutingStrategies.Tunnel); RoutingStrategies.Tunnel);
} }
private static void OpenDevTools(TopLevel control) internal static void OpenDevTools(TopLevel control)
{ {
if (s_open.TryGetValue(control, out var devToolsWindow)) if (s_open.TryGetValue(control, out var devToolsWindow))
{ {

32
src/Avalonia.Input/Gestures.cs

@ -31,7 +31,7 @@ namespace Avalonia.Input
RoutedEvent.Register<ScrollGestureEventArgs>( RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
private static WeakReference<IInteractive> s_lastPress; private static WeakReference<IInteractive> s_lastPress = new WeakReference<IInteractive>(null);
static Gestures() static Gestures()
{ {
@ -39,6 +39,36 @@ namespace Avalonia.Input
InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased);
} }
public static void AddTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(TappedEvent, handler);
}
public static void AddDoubleTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(DoubleTappedEvent, handler);
}
public static void AddRightTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.AddHandler(RightTappedEvent, handler);
}
public static void RemoveTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.RemoveHandler(TappedEvent, handler);
}
public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.RemoveHandler(DoubleTappedEvent, handler);
}
public static void RemoveRightTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
{
element.RemoveHandler(RightTappedEvent, handler);
}
private static void PointerPressed(RoutedEventArgs ev) private static void PointerPressed(RoutedEventArgs ev)
{ {
if (ev.Route == RoutingStrategies.Bubble) if (ev.Route == RoutingStrategies.Bubble)

4
src/Avalonia.Interactivity/EventSubscription.cs

@ -5,8 +5,12 @@ using System;
namespace Avalonia.Interactivity namespace Avalonia.Interactivity
{ {
internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args);
internal class EventSubscription internal class EventSubscription
{ {
public HandlerInvokeSignature InvokeAdapter { get; set; }
public Delegate Handler { get; set; } public Delegate Handler { get; set; }
public RoutingStrategies Routes { get; set; } public RoutingStrategies Routes { get; set; }

101
src/Avalonia.Interactivity/Interactive.cs

@ -4,8 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -18,15 +16,14 @@ namespace Avalonia.Interactivity
{ {
private Dictionary<RoutedEvent, List<EventSubscription>> _eventHandlers; private Dictionary<RoutedEvent, List<EventSubscription>> _eventHandlers;
private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
/// <summary> /// <summary>
/// Gets the interactive parent of the object for bubbling and tunneling events. /// Gets the interactive parent of the object for bubbling and tunneling events.
/// </summary> /// </summary>
IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>());
{
get { return _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>()); }
}
/// <summary> /// <summary>
/// Adds a handler for the specified routed event. /// Adds a handler for the specified routed event.
@ -45,24 +42,14 @@ namespace Avalonia.Interactivity
Contract.Requires<ArgumentNullException>(routedEvent != null); Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null); Contract.Requires<ArgumentNullException>(handler != null);
List<EventSubscription> subscriptions; var subscription = new EventSubscription
if (!EventHandlers.TryGetValue(routedEvent, out subscriptions))
{
subscriptions = new List<EventSubscription>();
EventHandlers.Add(routedEvent, subscriptions);
}
var sub = new EventSubscription
{ {
Handler = handler, Handler = handler,
Routes = routes, Routes = routes,
AlsoIfHandled = handledEventsToo, AlsoIfHandled = handledEventsToo,
}; };
subscriptions.Add(sub); return AddEventSubscription(routedEvent, subscription);
return Disposable.Create(() => subscriptions.Remove(sub));
} }
/// <summary> /// <summary>
@ -80,7 +67,37 @@ namespace Avalonia.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) where TEventArgs : RoutedEventArgs bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
{ {
return AddHandler(routedEvent, (Delegate)handler, routes, handledEventsToo); Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
// EventHandler delegate is not covariant, this forces us to create small wrapper
// that will cast our type erased instance and invoke it.
Type eventArgsType = routedEvent.EventArgsType;
if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter))
{
void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args)
{
var typedHandler = (EventHandler<TEventArgs>)baseHandler;
var typedArgs = (TEventArgs)args;
typedHandler(sender, typedArgs);
}
invokeAdapter = InvokeAdapter;
s_invokeHandlerCache.Add(eventArgsType, invokeAdapter);
}
var subscription = new EventSubscription
{
InvokeAdapter = invokeAdapter,
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
return AddEventSubscription(routedEvent, subscription);
} }
/// <summary> /// <summary>
@ -196,10 +213,54 @@ namespace Avalonia.Interactivity
if (correctRoute && notFinished) if (correctRoute && notFinished)
{ {
sub.Handler.DynamicInvoke(this, e); if (sub.InvokeAdapter != null)
{
sub.InvokeAdapter(sub.Handler, this, e);
}
else
{
sub.Handler.DynamicInvoke(this, e);
}
} }
} }
} }
} }
private List<EventSubscription> GetEventSubscriptions(RoutedEvent routedEvent)
{
if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions))
{
subscriptions = new List<EventSubscription>();
EventHandlers.Add(routedEvent, subscriptions);
}
return subscriptions;
}
private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
{
List<EventSubscription> subscriptions = GetEventSubscriptions(routedEvent);
subscriptions.Add(subscription);
return new UnsubscribeDisposable(subscriptions, subscription);
}
private sealed class UnsubscribeDisposable : IDisposable
{
private readonly List<EventSubscription> _subscriptions;
private readonly EventSubscription _subscription;
public UnsubscribeDisposable(List<EventSubscription> subscriptions, EventSubscription subscription)
{
_subscriptions = subscriptions;
_subscription = subscription;
}
public void Dispose()
{
_subscriptions.Remove(_subscription);
}
}
} }
} }

50
src/Avalonia.Interactivity/RoutedEvent.cs

@ -4,7 +4,6 @@
using System; using System;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Reflection; using System.Reflection;
using System.Runtime.ExceptionServices;
namespace Avalonia.Interactivity namespace Avalonia.Interactivity
{ {
@ -18,8 +17,8 @@ namespace Avalonia.Interactivity
public class RoutedEvent public class RoutedEvent
{ {
private Subject<Tuple<object, RoutedEventArgs>> _raised = new Subject<Tuple<object, RoutedEventArgs>>(); private readonly Subject<(object, RoutedEventArgs)> _raised = new Subject<(object, RoutedEventArgs)>();
private Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>(); private readonly Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
public RoutedEvent( public RoutedEvent(
string name, string name,
@ -38,31 +37,15 @@ namespace Avalonia.Interactivity
RoutingStrategies = routingStrategies; RoutingStrategies = routingStrategies;
} }
public Type EventArgsType public Type EventArgsType { get; }
{
get;
private set;
}
public string Name public string Name { get; }
{
get;
private set;
}
public Type OwnerType public Type OwnerType { get; }
{
get;
private set;
}
public RoutingStrategies RoutingStrategies public RoutingStrategies RoutingStrategies { get; }
{
get;
private set;
}
public IObservable<Tuple<object, RoutedEventArgs>> Raised => _raised; public IObservable<(object, RoutedEventArgs)> Raised => _raised;
public IObservable<RoutedEventArgs> RouteFinished => _routeFinished; public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
public static RoutedEvent<TEventArgs> Register<TOwner, TEventArgs>( public static RoutedEvent<TEventArgs> Register<TOwner, TEventArgs>(
@ -98,29 +81,20 @@ namespace Avalonia.Interactivity
{ {
return Raised.Subscribe(args => return Raised.Subscribe(args =>
{ {
var sender = args.Item1; (object sender, RoutedEventArgs e) = args;
var e = args.Item2;
if (targetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) && if (targetType.IsInstanceOfType(sender) &&
((e.Route == RoutingStrategies.Direct) || (e.Route & routes) != 0) && (e.Route == RoutingStrategies.Direct || (e.Route & routes) != 0) &&
(!e.Handled || handledEventsToo)) (!e.Handled || handledEventsToo))
{ {
try handler(sender, e);
{
handler.DynamicInvoke(sender, e);
}
catch (TargetInvocationException ex)
{
// Unwrap the inner exception.
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}
} }
}); });
} }
internal void InvokeRaised(object sender, RoutedEventArgs e) internal void InvokeRaised(object sender, RoutedEventArgs e)
{ {
_raised.OnNext(Tuple.Create(sender, e)); _raised.OnNext((sender, e));
} }
internal void InvokeRouteFinished(RoutedEventArgs e) internal void InvokeRouteFinished(RoutedEventArgs e)

23
src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs

@ -1,5 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Disposables; using System.Threading;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using Avalonia.Rendering; using Avalonia.Rendering;
@ -13,11 +16,27 @@ namespace Avalonia.Native
{ {
_window = window; _window = window;
} }
public IDisposable TryLock() public IDisposable TryLock()
{ {
if (_window.TryLock()) if (_window.TryLock())
return Disposable.Create(() => _window.Unlock()); return new UnlockDisposable(_window);
return null; return null;
} }
private sealed class UnlockDisposable : IDisposable
{
private IAvnWindowBase _window;
public UnlockDisposable(IAvnWindowBase window)
{
_window = window;
}
public void Dispose()
{
Interlocked.Exchange(ref _window, null)?.Unlock();
}
}
} }
} }

19
src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs

@ -8,16 +8,16 @@ using Avalonia.Controls.Platform;
namespace Avalonia.Native namespace Avalonia.Native
{ {
internal class WindowsMountedVolumeInfoListener : IDisposable internal class MacOSMountedVolumeInfoListener : IDisposable
{ {
private readonly CompositeDisposable _disposables; private readonly CompositeDisposable _disposables;
private readonly ObservableCollection<MountedVolumeInfo> _targetObs;
private bool _beenDisposed = false; private bool _beenDisposed = false;
private ObservableCollection<MountedVolumeInfo> mountedDrives; private ObservableCollection<MountedVolumeInfo> mountedDrives;
public WindowsMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives) public MacOSMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
{ {
this.mountedDrives = mountedDrives; this.mountedDrives = mountedDrives;
_disposables = new CompositeDisposable(); _disposables = new CompositeDisposable();
var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1))
@ -30,7 +30,8 @@ namespace Avalonia.Native
private void Poll(long _) private void Poll(long _)
{ {
var mountVolInfos = Directory.GetDirectories("/Volumes") var mountVolInfos = Directory.GetDirectories("/Volumes/")
.Where(p=> p != null)
.Select(p => new MountedVolumeInfo() .Select(p => new MountedVolumeInfo()
{ {
VolumeLabel = Path.GetFileName(p), VolumeLabel = Path.GetFileName(p),
@ -38,15 +39,15 @@ namespace Avalonia.Native
VolumeSizeBytes = 0 VolumeSizeBytes = 0
}) })
.ToArray(); .ToArray();
if (_targetObs.SequenceEqual(mountVolInfos)) if (mountedDrives.SequenceEqual(mountVolInfos))
return; return;
else else
{ {
_targetObs.Clear(); mountedDrives.Clear();
foreach (var i in mountVolInfos) foreach (var i in mountVolInfos)
_targetObs.Add(i); mountedDrives.Add(i);
} }
} }
@ -72,7 +73,7 @@ namespace Avalonia.Native
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives) public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{ {
Contract.Requires<ArgumentNullException>(mountedDrives != null); Contract.Requires<ArgumentNullException>(mountedDrives != null);
return new WindowsMountedVolumeInfoListener(mountedDrives); return new MacOSMountedVolumeInfoListener(mountedDrives);
} }
} }
} }

30
src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs

@ -1,5 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Disposables;
using System.Threading; using System.Threading;
namespace Avalonia.Rendering namespace Avalonia.Rendering
@ -7,7 +9,7 @@ namespace Avalonia.Rendering
public class ManagedDeferredRendererLock : IDeferredRendererLock public class ManagedDeferredRendererLock : IDeferredRendererLock
{ {
private readonly object _lock = new object(); private readonly object _lock = new object();
/// <summary> /// <summary>
/// Tries to lock the target surface or window /// Tries to lock the target surface or window
/// </summary> /// </summary>
@ -15,7 +17,7 @@ namespace Avalonia.Rendering
public IDisposable TryLock() public IDisposable TryLock()
{ {
if (Monitor.TryEnter(_lock)) if (Monitor.TryEnter(_lock))
return Disposable.Create(() => Monitor.Exit(_lock)); return new UnlockDisposable(_lock);
return null; return null;
} }
@ -25,7 +27,27 @@ namespace Avalonia.Rendering
public IDisposable Lock() public IDisposable Lock()
{ {
Monitor.Enter(_lock); Monitor.Enter(_lock);
return Disposable.Create(() => Monitor.Exit(_lock)); return new UnlockDisposable(_lock);
}
private sealed class UnlockDisposable : IDisposable
{
private object _lock;
public UnlockDisposable(object @lock)
{
_lock = @lock;
}
public void Dispose()
{
object @lock = Interlocked.Exchange(ref _lock, null);
if (@lock != null)
{
Monitor.Exit(@lock);
}
}
} }
} }
} }

160
src/Avalonia.Visuals/Vector.cs

@ -65,9 +65,7 @@ namespace Avalonia
/// <param name="b">Second vector</param> /// <param name="b">Second vector</param>
/// <returns>The dot product</returns> /// <returns>The dot product</returns>
public static double operator *(Vector a, Vector b) public static double operator *(Vector a, Vector b)
{ => Dot(a, b);
return a.X * b.X + a.Y * b.Y;
}
/// <summary> /// <summary>
/// Scales a vector. /// Scales a vector.
@ -76,9 +74,7 @@ namespace Avalonia
/// <param name="scale">The scaling factor.</param> /// <param name="scale">The scaling factor.</param>
/// <returns>The scaled vector.</returns> /// <returns>The scaled vector.</returns>
public static Vector operator *(Vector vector, double scale) public static Vector operator *(Vector vector, double scale)
{ => Multiply(vector, scale);
return new Vector(vector._x * scale, vector._y * scale);
}
/// <summary> /// <summary>
/// Scales a vector. /// Scales a vector.
@ -87,14 +83,17 @@ namespace Avalonia
/// <param name="scale">The divisor.</param> /// <param name="scale">The divisor.</param>
/// <returns>The scaled vector.</returns> /// <returns>The scaled vector.</returns>
public static Vector operator /(Vector vector, double scale) public static Vector operator /(Vector vector, double scale)
{ => Divide(vector, scale);
return new Vector(vector._x / scale, vector._y / scale);
}
/// <summary> /// <summary>
/// Length of the vector /// Length of the vector
/// </summary> /// </summary>
public double Length => Math.Sqrt(X * X + Y * Y); public double Length => Math.Sqrt(SquaredLength);
/// <summary>
/// Squared Length of the vector
/// </summary>
public double SquaredLength => _x * _x + _y * _y;
/// <summary> /// <summary>
/// Negates a vector. /// Negates a vector.
@ -102,9 +101,7 @@ namespace Avalonia
/// <param name="a">The vector.</param> /// <param name="a">The vector.</param>
/// <returns>The negated vector.</returns> /// <returns>The negated vector.</returns>
public static Vector operator -(Vector a) public static Vector operator -(Vector a)
{ => Negate(a);
return new Vector(-a._x, -a._y);
}
/// <summary> /// <summary>
/// Adds two vectors. /// Adds two vectors.
@ -113,9 +110,7 @@ namespace Avalonia
/// <param name="b">The second vector.</param> /// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the addition.</returns> /// <returns>A vector that is the result of the addition.</returns>
public static Vector operator +(Vector a, Vector b) public static Vector operator +(Vector a, Vector b)
{ => Add(a, b);
return new Vector(a._x + b._x, a._y + b._y);
}
/// <summary> /// <summary>
/// Subtracts two vectors. /// Subtracts two vectors.
@ -124,9 +119,7 @@ namespace Avalonia
/// <param name="b">The second vector.</param> /// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the subtraction.</returns> /// <returns>A vector that is the result of the subtraction.</returns>
public static Vector operator -(Vector a, Vector b) public static Vector operator -(Vector a, Vector b)
{ => Subtract(a, b);
return new Vector(a._x - b._x, a._y - b._y);
}
/// <summary> /// <summary>
/// Check if two vectors are equal (bitwise). /// Check if two vectors are equal (bitwise).
@ -155,7 +148,8 @@ namespace Avalonia
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(null, obj))
return false;
return obj is Vector vector && Equals(vector); return obj is Vector vector && Equals(vector);
} }
@ -206,5 +200,131 @@ namespace Avalonia
{ {
return new Vector(_x, y); return new Vector(_x, y);
} }
/// <summary>
/// Returns a normalized version of this vector.
/// </summary>
/// <returns>The normalized vector.</returns>
public Vector Normalize()
=> Normalize(this);
/// <summary>
/// Returns a negated version of this vector.
/// </summary>
/// <returns>The negated vector.</returns>
public Vector Negate()
=> Negate(this);
/// <summary>
/// Returns the dot product of two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The dot product.</returns>
public static double Dot(Vector a, Vector b)
=> a._x * b._x + a._y * b._y;
/// <summary>
/// Returns the cross product of two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The cross product.</returns>
public static double Cross(Vector a, Vector b)
=> a._x * b._y - a._y * b._x;
/// <summary>
/// Normalizes the given vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <returns>The normalized vector.</returns>
public static Vector Normalize(Vector vector)
=> Divide(vector, vector.Length);
/// <summary>
/// Divides the first vector by the second.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The scaled vector.</returns>
public static Vector Divide(Vector a, Vector b)
=> new Vector(a._x / b._x, a._y / b._y);
/// <summary>
/// Divides the vector by the given scalar.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="scalar">The scalar value</param>
/// <returns>The scaled vector.</returns>
public static Vector Divide(Vector vector, double scalar)
=> new Vector(vector._x / scalar, vector._y / scalar);
/// <summary>
/// Multiplies the first vector by the second.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The scaled vector.</returns>
public static Vector Multiply(Vector a, Vector b)
=> new Vector(a._x * b._x, a._y * b._y);
/// <summary>
/// Multiplies the vector by the given scalar.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="scalar">The scalar value</param>
/// <returns>The scaled vector.</returns>
public static Vector Multiply(Vector vector, double scalar)
=> new Vector(vector._x * scalar, vector._y * scalar);
/// <summary>
/// Adds the second to the first vector
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The summed vector.</returns>
public static Vector Add(Vector a, Vector b)
=> new Vector(a._x + b._x, a._y + b._y);
/// <summary>
/// Subtracts the second from the first vector
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The difference vector.</returns>
public static Vector Subtract(Vector a, Vector b)
=> new Vector(a._x - b._x, a._y - b._y);
/// <summary>
/// Negates the vector
/// </summary>
/// <param name="vector">The vector to negate.</param>
/// <returns>The scaled vector.</returns>
public static Vector Negate(Vector vector)
=> new Vector(-vector._x, -vector._y);
/// <summary>
/// Returnes the vector (0.0, 0.0)
/// </summary>
public static Vector Zero
=> new Vector(0, 0);
/// <summary>
/// Returnes the vector (1.0, 1.0)
/// </summary>
public static Vector One
=> new Vector(1, 1);
/// <summary>
/// Returnes the vector (1.0, 0.0)
/// </summary>
public static Vector UnitX
=> new Vector(1, 0);
/// <summary>
/// Returnes the vector (0.0, 1.0)
/// </summary>
public static Vector UnitY
=> new Vector(0, 1);
} }
} }

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

@ -10,8 +10,7 @@ namespace Avalonia.Win32
{ {
internal class WindowsMountedVolumeInfoListener : IDisposable internal class WindowsMountedVolumeInfoListener : IDisposable
{ {
private readonly CompositeDisposable _disposables; private readonly CompositeDisposable _disposables;
private readonly ObservableCollection<MountedVolumeInfo> _targetObs = new ObservableCollection<MountedVolumeInfo>();
private bool _beenDisposed = false; private bool _beenDisposed = false;
private ObservableCollection<MountedVolumeInfo> mountedDrives; private ObservableCollection<MountedVolumeInfo> mountedDrives;
@ -41,14 +40,14 @@ namespace Avalonia.Win32
}) })
.ToArray(); .ToArray();
if (_targetObs.SequenceEqual(mountVolInfos)) if (mountedDrives.SequenceEqual(mountVolInfos))
return; return;
else else
{ {
_targetObs.Clear(); mountedDrives.Clear();
foreach (var i in mountVolInfos) foreach (var i in mountVolInfos)
_targetObs.Add(i); mountedDrives.Add(i);
} }
} }

46
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs

@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -12,45 +11,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
public class EventTests : XamlTestBase public class EventTests : XamlTestBase
{ {
[Fact] [Fact]
public void Event_Is_Attached() public void Event_Is_Assigned()
{ {
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='OnClick'/>"; var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='OnClick'/>";
var loader = new AvaloniaXamlLoader(); var loader = new AvaloniaXamlLoader();
var target = new MyButton(); var target = new MyButton();
loader.Load(xaml, rootInstance: target); loader.Load(xaml, rootInstance: target);
RaiseClick(target);
Assert.True(target.Clicked); target.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = Button.ClickEvent,
});
Assert.True(target.WasClicked);
} }
[Fact] [Fact]
public void Exception_Is_Thrown_If_Event_Not_Found() public void Attached_Event_Is_Assigned()
{ {
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='NotFound'/>"; var xaml = @"<Button xmlns='https://github.com/avaloniaui' Gestures.Tapped='OnTapped'/>";
var loader = new AvaloniaXamlLoader(); var loader = new AvaloniaXamlLoader();
var target = new MyButton(); var target = new MyButton();
XamlTestHelpers.AssertThrowsXamlException(() => loader.Load(xaml, rootInstance: target)); loader.Load(xaml, rootInstance: target);
}
private void RaiseClick(MyButton target) target.RaiseEvent(new RoutedEventArgs
{
target.RaiseEvent(new KeyEventArgs
{ {
RoutedEvent = Button.KeyDownEvent, RoutedEvent = Gestures.TappedEvent,
Key = Key.Enter,
}); });
Assert.True(target.WasTapped);
}
[Fact]
public void Exception_Is_Thrown_If_Event_Not_Found()
{
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='NotFound'/>";
var loader = new AvaloniaXamlLoader();
var target = new MyButton();
XamlTestHelpers.AssertThrowsXamlException(() => loader.Load(xaml, rootInstance: target));
} }
public class MyButton : Button public class MyButton : Button
{ {
public bool Clicked { get; private set; } public bool WasClicked { get; private set; }
public bool WasTapped { get; private set; }
public void OnClick(object sender, RoutedEventArgs e) public void OnClick(object sender, RoutedEventArgs e) => WasClicked = true;
{ public void OnTapped(object sender, RoutedEventArgs e) => WasTapped = true;
Clicked = true;
}
} }
} }
} }

112
tests/Avalonia.Visuals.UnitTests/VectorTests.cs

@ -0,0 +1,112 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Xunit;
using Avalonia;
using System;
namespace Avalonia.Visuals.UnitTests
{
public class VectorTests
{
[Fact]
public void Length_Should_Return_Correct_Length_Of_Vector()
{
var vector = new Vector(2, 4);
var length = Math.Sqrt(2 * 2 + 4 * 4);
Assert.Equal(length, vector.Length);
}
[Fact]
public void Length_Squared_Should_Return_Correct_Length_Of_Vector()
{
var vectorA = new Vector(2, 4);
var squaredLengthA = 2 * 2 + 4 * 4;
Assert.Equal(squaredLengthA, vectorA.SquaredLength);
}
[Fact]
public void Normalize_Should_Return_Normalized_Vector()
{
// the length of a normalized vector must be 1
var vectorA = new Vector(13, 84);
var vectorB = new Vector(-34, 345);
var vectorC = new Vector(-34, -84);
Assert.Equal(1.0, vectorA.Normalize().Length);
Assert.Equal(1.0, vectorB.Normalize().Length);
Assert.Equal(1.0, vectorC.Normalize().Length);
}
[Fact]
public void Negate_Should_Return_Negated_Vector()
{
var vector = new Vector(2, 4);
var negated = new Vector(-2, -4);
Assert.Equal(negated, vector.Negate());
}
[Fact]
public void Dot_Should_Return_Correct_Value()
{
var a = new Vector(-6, 8.0);
var b = new Vector(5, 12.0);
Assert.Equal(66.0, Vector.Dot(a, b));
}
[Fact]
public void Cross_Should_Return_Correct_Value()
{
var a = new Vector(-6, 8.0);
var b = new Vector(5, 12.0);
Assert.Equal(-112.0, Vector.Cross(a, b));
}
[Fact]
public void Divied_By_Vector_Should_Return_Correct_Value()
{
var a = new Vector(10, 2);
var b = new Vector(5, 2);
var expected = new Vector(2, 1);
Assert.Equal(expected, Vector.Divide(a, b));
}
[Fact]
public void Divied_Should_Return_Correct_Value()
{
var vector = new Vector(10, 2);
var expected = new Vector(5, 1);
Assert.Equal(expected, Vector.Divide(vector, 2));
}
[Fact]
public void Multiply_By_Vector_Should_Return_Correct_Value()
{
var a = new Vector(10, 2);
var b = new Vector(2, 2);
var expected = new Vector(20, 4);
Assert.Equal(expected, Vector.Multiply(a, b));
}
[Fact]
public void Multiply_Should_Return_Correct_Value()
{
var vector = new Vector(10, 2);
var expected = new Vector(20, 4);
Assert.Equal(expected, Vector.Multiply(vector, 2));
}
}
}
Loading…
Cancel
Save