Browse Source

Merge branch 'master' into deferred-setter-notifyhandler

pull/2933/head
Dariusz Komosiński 7 years ago
committed by GitHub
parent
commit
b81e18ca72
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. 51
      src/Avalonia.Base/AvaloniaObject.cs
  5. 28
      src/Avalonia.Base/BoxedValue.cs
  6. 16
      src/Avalonia.Base/PriorityBindingEntry.cs
  7. 11
      src/Avalonia.Base/StyledPropertyBase.cs
  8. 14
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  9. 4
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  10. 7
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  11. 32
      src/Avalonia.Input/Gestures.cs
  12. 4
      src/Avalonia.Interactivity/EventSubscription.cs
  13. 101
      src/Avalonia.Interactivity/Interactive.cs
  14. 50
      src/Avalonia.Interactivity/RoutedEvent.cs
  15. 23
      src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs
  16. 19
      src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs
  17. 30
      src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
  18. 160
      src/Avalonia.Visuals/Vector.cs
  19. 12
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  20. 9
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs
  21. 76
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.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>
<Version>0.8.999</Version>
<Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
<PackageLicenseUrl>https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/AvaloniaUI/Avalonia/</PackageProjectUrl>
<PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591</NoWarn>
<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>
</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/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.native/$1/lib/netstandard2.0/

17
src/Avalonia.Animation/Animatable.cs

@ -45,16 +45,17 @@ namespace Avalonia.Animation
{
get
{
if (_transitions == null)
if (_transitions is null)
_transitions = new Transitions();
if (_previousTransitions == null)
if (_previousTransitions is null)
_previousTransitions = new Dictionary<AvaloniaProperty, IDisposable>();
return _transitions;
}
set
{
SetAndRaise(TransitionsProperty, ref _transitions, value);
}
}
@ -66,18 +67,20 @@ namespace Avalonia.Animation
/// <param name="e">The event args.</param>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Priority != BindingPriority.Animation && Transitions != null && _previousTransitions != null)
{
var match = Transitions.FirstOrDefault(x => x.Property == e.Property);
if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return;
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))
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;
return;
}
}
}

51
src/Avalonia.Base/AvaloniaObject.cs

@ -208,20 +208,9 @@ namespace Avalonia
{
return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this);
}
else if (_values != null)
{
var result = Values.GetValue(property);
if (result == AvaloniaProperty.UnsetValue)
{
result = GetDefaultValue(property);
}
return result;
}
else
{
return GetDefaultValue(property);
return GetValueOrDefaultUnchecked(property);
}
}
@ -598,10 +587,46 @@ namespace Avalonia
private object GetDefaultValue(AvaloniaProperty property)
{
if (property.Inherits && InheritanceParent is AvaloniaObject aobj)
return aobj.GetValue(property);
return aobj.GetValueOrDefaultUnchecked(property);
return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType());
}
/// <summary>
/// Gets the value or default value for a property.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>The default value.</returns>
private object GetValueOrDefaultUnchecked(AvaloniaProperty property)
{
var aobj = this;
var valuestore = aobj._values;
if (valuestore != null)
{
var result = valuestore.GetValue(property);
if (result != AvaloniaProperty.UnsetValue)
{
return result;
}
}
if (property.Inherits)
{
while (aobj.InheritanceParent is AvaloniaObject parent)
{
aobj = parent;
valuestore = aobj._values;
if (valuestore != null)
{
var result = valuestore.GetValue(property);
if (result != AvaloniaProperty.UnsetValue)
{
return result;
}
}
}
}
return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType());
}
/// <summary>
/// Sets the value of a direct property.
/// </summary>

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

11
src/Avalonia.Base/StyledPropertyBase.cs

@ -68,7 +68,7 @@ namespace Avalonia
{
Contract.Requires<ArgumentNullException>(type != null);
return GetMetadata(type).DefaultValue;
return GetMetadata(type).DefaultValue.Typed;
}
/// <summary>
@ -164,7 +164,14 @@ namespace Avalonia
}
/// <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]
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="defaultBindingMode">The default binding mode.</param>
public StyledPropertyMetadata(
TValue defaultValue = default(TValue),
TValue defaultValue = default,
Func<IAvaloniaObject, TValue, TValue> validate = null,
BindingMode defaultBindingMode = BindingMode.Default)
: base(defaultBindingMode)
{
DefaultValue = defaultValue;
DefaultValue = new BoxedValue<TValue>(defaultValue);
Validate = validate;
}
/// <summary>
/// Gets the default value for the property.
/// </summary>
public TValue DefaultValue { get; private set; }
internal BoxedValue<TValue> DefaultValue { get; private set; }
/// <summary>
/// Gets the validation callback.
/// </summary>
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);
@ -47,11 +47,9 @@ namespace Avalonia
{
base.Merge(baseMetadata, property);
var src = baseMetadata as StyledPropertyMetadata<TValue>;
if (src != null)
if (baseMetadata is StyledPropertyMetadata<TValue> src)
{
if (DefaultValue == null)
if (DefaultValue.Boxed == null)
{
DefaultValue = src.DefaultValue;
}

4
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -217,12 +217,12 @@
<DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
<Rectangle Name="BottomRightCorner" Fill="#FFE9EEF4" Grid.Column="2" Grid.Row="2" />
<Rectangle Name="BottomLeftCorner" Fill="#FFE9EEF4" Grid.Row="2" Grid.ColumnSpan="2" />
<ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="18" Margin="0,-1,-1,-1"/>
<ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}" Margin="0,-1,-1,-1"/>
<Grid Grid.Column="1" Grid.Row="2"
ColumnDefinitions="Auto,*">
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
<ScrollBar Name="PART_HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="18" Margin="-1,0,-1,-1"/>
<ScrollBar Name="PART_HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="{DynamicResource ScrollBarThickness}" Margin="-1,0,-1,-1"/>
</Grid>
</Grid>
</Border>

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

@ -28,6 +28,11 @@ namespace Avalonia
{
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);
}
private static void OpenDevTools(TopLevel control)
internal static void OpenDevTools(TopLevel control)
{
if (s_open.TryGetValue(control, out var devToolsWindow))
{

32
src/Avalonia.Input/Gestures.cs

@ -31,7 +31,7 @@ namespace Avalonia.Input
RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
private static WeakReference<IInteractive> s_lastPress;
private static WeakReference<IInteractive> s_lastPress = new WeakReference<IInteractive>(null);
static Gestures()
{
@ -39,6 +39,36 @@ namespace Avalonia.Input
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)
{
if (ev.Route == RoutingStrategies.Bubble)

4
src/Avalonia.Interactivity/EventSubscription.cs

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

101
src/Avalonia.Interactivity/Interactive.cs

@ -4,8 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Layout;
using Avalonia.VisualTree;
@ -18,15 +16,14 @@ namespace Avalonia.Interactivity
{
private Dictionary<RoutedEvent, List<EventSubscription>> _eventHandlers;
private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
/// <summary>
/// Gets the interactive parent of the object for bubbling and tunneling events.
/// </summary>
IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers
{
get { return _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>()); }
}
private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>());
/// <summary>
/// Adds a handler for the specified routed event.
@ -45,24 +42,14 @@ namespace Avalonia.Interactivity
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
List<EventSubscription> subscriptions;
if (!EventHandlers.TryGetValue(routedEvent, out subscriptions))
{
subscriptions = new List<EventSubscription>();
EventHandlers.Add(routedEvent, subscriptions);
}
var sub = new EventSubscription
var subscription = new EventSubscription
{
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
subscriptions.Add(sub);
return Disposable.Create(() => subscriptions.Remove(sub));
return AddEventSubscription(routedEvent, subscription);
}
/// <summary>
@ -80,7 +67,37 @@ namespace Avalonia.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
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>
@ -196,10 +213,54 @@ namespace Avalonia.Interactivity
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.Reactive.Subjects;
using System.Reflection;
using System.Runtime.ExceptionServices;
namespace Avalonia.Interactivity
{
@ -18,8 +17,8 @@ namespace Avalonia.Interactivity
public class RoutedEvent
{
private Subject<Tuple<object, RoutedEventArgs>> _raised = new Subject<Tuple<object, RoutedEventArgs>>();
private Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
private readonly Subject<(object, RoutedEventArgs)> _raised = new Subject<(object, RoutedEventArgs)>();
private readonly Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
public RoutedEvent(
string name,
@ -38,31 +37,15 @@ namespace Avalonia.Interactivity
RoutingStrategies = routingStrategies;
}
public Type EventArgsType
{
get;
private set;
}
public Type EventArgsType { get; }
public string Name
{
get;
private set;
}
public string Name { get; }
public Type OwnerType
{
get;
private set;
}
public Type OwnerType { get; }
public RoutingStrategies RoutingStrategies
{
get;
private set;
}
public RoutingStrategies RoutingStrategies { get; }
public IObservable<Tuple<object, RoutedEventArgs>> Raised => _raised;
public IObservable<(object, RoutedEventArgs)> Raised => _raised;
public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
public static RoutedEvent<TEventArgs> Register<TOwner, TEventArgs>(
@ -98,29 +81,20 @@ namespace Avalonia.Interactivity
{
return Raised.Subscribe(args =>
{
var sender = args.Item1;
var e = args.Item2;
(object sender, RoutedEventArgs e) = args;
if (targetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) &&
((e.Route == RoutingStrategies.Direct) || (e.Route & routes) != 0) &&
if (targetType.IsInstanceOfType(sender) &&
(e.Route == RoutingStrategies.Direct || (e.Route & routes) != 0) &&
(!e.Handled || handledEventsToo))
{
try
{
handler.DynamicInvoke(sender, e);
}
catch (TargetInvocationException ex)
{
// Unwrap the inner exception.
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}
handler(sender, e);
}
});
}
internal void InvokeRaised(object sender, RoutedEventArgs e)
{
_raised.OnNext(Tuple.Create(sender, e));
_raised.OnNext((sender, 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.Reactive.Disposables;
using System.Threading;
using Avalonia.Native.Interop;
using Avalonia.Rendering;
@ -13,11 +16,27 @@ namespace Avalonia.Native
{
_window = window;
}
public IDisposable TryLock()
{
if (_window.TryLock())
return Disposable.Create(() => _window.Unlock());
return new UnlockDisposable(_window);
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
{
internal class WindowsMountedVolumeInfoListener : IDisposable
internal class MacOSMountedVolumeInfoListener : IDisposable
{
private readonly CompositeDisposable _disposables;
private readonly ObservableCollection<MountedVolumeInfo> _targetObs;
private bool _beenDisposed = false;
private ObservableCollection<MountedVolumeInfo> mountedDrives;
public WindowsMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
public MacOSMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
{
this.mountedDrives = mountedDrives;
_disposables = new CompositeDisposable();
var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1))
@ -30,7 +30,8 @@ namespace Avalonia.Native
private void Poll(long _)
{
var mountVolInfos = Directory.GetDirectories("/Volumes")
var mountVolInfos = Directory.GetDirectories("/Volumes/")
.Where(p=> p != null)
.Select(p => new MountedVolumeInfo()
{
VolumeLabel = Path.GetFileName(p),
@ -38,15 +39,15 @@ namespace Avalonia.Native
VolumeSizeBytes = 0
})
.ToArray();
if (_targetObs.SequenceEqual(mountVolInfos))
if (mountedDrives.SequenceEqual(mountVolInfos))
return;
else
{
_targetObs.Clear();
mountedDrives.Clear();
foreach (var i in mountVolInfos)
_targetObs.Add(i);
mountedDrives.Add(i);
}
}
@ -72,7 +73,7 @@ namespace Avalonia.Native
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{
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.Reactive.Disposables;
using System.Threading;
namespace Avalonia.Rendering
@ -7,7 +9,7 @@ namespace Avalonia.Rendering
public class ManagedDeferredRendererLock : IDeferredRendererLock
{
private readonly object _lock = new object();
/// <summary>
/// Tries to lock the target surface or window
/// </summary>
@ -15,7 +17,7 @@ namespace Avalonia.Rendering
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return Disposable.Create(() => Monitor.Exit(_lock));
return new UnlockDisposable(_lock);
return null;
}
@ -25,7 +27,27 @@ namespace Avalonia.Rendering
public IDisposable 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>
/// <returns>The dot product</returns>
public static double operator *(Vector a, Vector b)
{
return a.X * b.X + a.Y * b.Y;
}
=> Dot(a, b);
/// <summary>
/// Scales a vector.
@ -76,9 +74,7 @@ namespace Avalonia
/// <param name="scale">The scaling factor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator *(Vector vector, double scale)
{
return new Vector(vector._x * scale, vector._y * scale);
}
=> Multiply(vector, scale);
/// <summary>
/// Scales a vector.
@ -87,14 +83,17 @@ namespace Avalonia
/// <param name="scale">The divisor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator /(Vector vector, double scale)
{
return new Vector(vector._x / scale, vector._y / scale);
}
=> Divide(vector, scale);
/// <summary>
/// Length of the vector
/// </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>
/// Negates a vector.
@ -102,9 +101,7 @@ namespace Avalonia
/// <param name="a">The vector.</param>
/// <returns>The negated vector.</returns>
public static Vector operator -(Vector a)
{
return new Vector(-a._x, -a._y);
}
=> Negate(a);
/// <summary>
/// Adds two vectors.
@ -113,9 +110,7 @@ namespace Avalonia
/// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the addition.</returns>
public static Vector operator +(Vector a, Vector b)
{
return new Vector(a._x + b._x, a._y + b._y);
}
=> Add(a, b);
/// <summary>
/// Subtracts two vectors.
@ -124,9 +119,7 @@ namespace Avalonia
/// <param name="b">The second vector.</param>
/// <returns>A vector that is the result of the subtraction.</returns>
public static Vector operator -(Vector a, Vector b)
{
return new Vector(a._x - b._x, a._y - b._y);
}
=> Subtract(a, b);
/// <summary>
/// Check if two vectors are equal (bitwise).
@ -155,7 +148,8 @@ namespace Avalonia
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);
}
@ -206,5 +200,131 @@ namespace Avalonia
{
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);
}
}

12
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -76,7 +76,12 @@ namespace Avalonia.Data
}
var children = Bindings.Select(x => x.Initiate(target, null));
var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter));
var input = children.Select(x => x.Observable)
.CombineLatest()
.Select(x => ConvertValue(x, targetType, converter))
.Where(x => x != BindingOperations.DoNothing);
var mode = Mode == BindingMode.Default ?
targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode;
@ -97,11 +102,6 @@ namespace Avalonia.Data
var culture = CultureInfo.CurrentCulture;
var converted = converter.Convert(values, targetType, ConverterParameter, culture);
if (converted == BindingOperations.DoNothing)
{
return converted;
}
if (converted == AvaloniaProperty.UnsetValue)
{
converted = FallbackValue;

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

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

76
tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Converters
{
public class MultiValueConverterTests : XamlTestBase
{
[Fact]
public void MultiValueConverter_Special_Values_Work()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:c='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Converters;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Name='textBlock'>
<TextBlock.Text>
<MultiBinding Converter='{x:Static c:TestMultiValueConverter.Instance}' FallbackValue='bar'>
<Binding Path='Item1' />
<Binding Path='Item2' />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
window.ApplyTemplate();
window.DataContext = Tuple.Create(2, 2);
Assert.Equal("foo", textBlock.Text);
window.DataContext = Tuple.Create(-3, 3);
Assert.Equal("foo", textBlock.Text);
window.DataContext = Tuple.Create(0, 2);
Assert.Equal("bar", textBlock.Text);
}
}
}
public class TestMultiValueConverter : IMultiValueConverter
{
public static readonly TestMultiValueConverter Instance = new TestMultiValueConverter();
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] is int i && values[1] is int j)
{
var p = i * j;
if (p > 0)
{
return "foo";
}
if (p == 0)
{
return AvaloniaProperty.UnsetValue;
}
return BindingOperations.DoNothing;
}
return "(default)";
}
}
}

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

@ -1,7 +1,6 @@
// 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 Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -12,45 +11,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
public class EventTests : XamlTestBase
{
[Fact]
public void Event_Is_Attached()
public void Event_Is_Assigned()
{
var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='OnClick'/>";
var loader = new AvaloniaXamlLoader();
var target = new MyButton();
loader.Load(xaml, rootInstance: target);
RaiseClick(target);
Assert.True(target.Clicked);
target.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = Button.ClickEvent,
});
Assert.True(target.WasClicked);
}
[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 target = new MyButton();
XamlTestHelpers.AssertThrowsXamlException(() => loader.Load(xaml, rootInstance: target));
}
loader.Load(xaml, rootInstance: target);
private void RaiseClick(MyButton target)
{
target.RaiseEvent(new KeyEventArgs
target.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = Button.KeyDownEvent,
Key = Key.Enter,
RoutedEvent = Gestures.TappedEvent,
});
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 bool Clicked { get; private set; }
public bool WasClicked { get; private set; }
public bool WasTapped { get; private set; }
public void OnClick(object sender, RoutedEventArgs e)
{
Clicked = true;
}
public void OnClick(object sender, RoutedEventArgs e) => WasClicked = true;
public void OnTapped(object sender, RoutedEventArgs e) => WasTapped = 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