Browse Source

Merge branch 'master' into feature/get-required-service

pull/7242/head
Steven Kirk 5 years ago
committed by GitHub
parent
commit
d841d76637
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      readme.md
  2. 18
      src/Avalonia.Input/Gestures.cs
  3. 9
      src/Avalonia.Input/PointerEventArgs.cs
  4. 44
      src/Avalonia.Input/TouchDevice.cs
  5. 7
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  6. 5
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  7. 1
      src/Avalonia.Styling/Avalonia.Styling.csproj
  8. 12
      src/Avalonia.Styling/Controls/ChildNameScope.cs
  9. 4
      src/Avalonia.Styling/Controls/INameScope.cs
  10. 30
      src/Avalonia.Styling/Controls/NameScope.cs
  11. 28
      src/Avalonia.Styling/Controls/NameScopeExtensions.cs
  12. 8
      src/Avalonia.Styling/Controls/NameScopeLocator.cs
  13. 2
      src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs
  14. 2
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  15. 6
      src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs
  16. 2
      src/Avalonia.Styling/INamed.cs
  17. 16
      src/Avalonia.Styling/LogicalTree/ControlLocator.cs
  18. 6
      src/Avalonia.Styling/LogicalTree/ILogical.cs
  19. 26
      src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs
  20. 7
      src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs
  21. 18
      src/Avalonia.Styling/StyledElement.cs
  22. 2
      src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs
  23. 2
      src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs
  24. 6
      src/Avalonia.Styling/Styling/ChildSelector.cs
  25. 2
      src/Avalonia.Styling/Styling/OrSelector.cs
  26. 2
      src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs
  27. 50
      src/Avalonia.Styling/Styling/Selectors.cs
  28. 14
      src/Avalonia.Styling/Styling/Styles.cs
  29. 6
      src/Avalonia.Styling/Styling/TemplateSelector.cs
  30. 2
      src/Avalonia.Visuals/Visual.cs
  31. 6
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  32. 272
      tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs

9
readme.md

@ -3,9 +3,6 @@
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
Tips: BTC: bc1q05wx78qemgy9x6ytl5ljk2xrt00yqargyjm8gx
This will be shared with the community and awarded for significant contributions.
## 📖 About AvaloniaUI
Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
@ -78,6 +75,12 @@ For more information see the [.NET Foundation Code of Conduct](https://dotnetfou
Avalonia is licenced under the [MIT licence](licence.md).
## Support Avalonia
**BTC**: bc1q05wx78qemgy9x6ytl5ljk2xrt00yqargyjm8gx
This will be shared with the community and awarded for significant contributions.
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)]

18
src/Avalonia.Input/Gestures.cs

@ -6,6 +6,7 @@ namespace Avalonia.Input
{
public static class Gestures
{
private static bool s_isDoubleTapped = false;
public static readonly RoutedEvent<TappedEventArgs> TappedEvent = RoutedEvent.Register<TappedEventArgs>(
"Tapped",
RoutingStrategies.Bubble,
@ -81,20 +82,23 @@ namespace Avalonia.Input
var e = (PointerPressedEventArgs)ev;
var visual = (IVisual)ev.Source;
#pragma warning disable CS0618 // Type or member is obsolete
var clickCount = e.ClickCount;
#pragma warning restore CS0618 // Type or member is obsolete
if (clickCount <= 1)
if (e.ClickCount <= 1)
{
s_isDoubleTapped = false;
s_lastPress.SetTarget(ev.Source);
}
else if (clickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
{
if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
{
s_isDoubleTapped = true;
e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e));
}
}
else
{
s_isDoubleTapped = false;
}
}
}
@ -112,7 +116,9 @@ namespace Avalonia.Input
{
e.Source.RaiseEvent(new TappedEventArgs(RightTappedEvent, e));
}
else
//s_isDoubleTapped needed here to prevent invoking Tapped event when DoubleTapped is called.
//This behaviour matches UWP behaviour.
else if (s_isDoubleTapped == false)
{
e.Source.RaiseEvent(new TappedEventArgs(TappedEvent, e));
}

9
src/Avalonia.Input/PointerEventArgs.cs

@ -114,7 +114,7 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs
{
private readonly int _obsoleteClickCount;
private readonly int _clickCount;
public PointerPressedEventArgs(
IInteractive source,
@ -123,15 +123,14 @@ namespace Avalonia.Input
ulong timestamp,
PointerPointProperties properties,
KeyModifiers modifiers,
int obsoleteClickCount = 1)
int clickCount = 1)
: base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
_obsoleteClickCount = obsoleteClickCount;
_clickCount = clickCount;
}
[Obsolete("Use DoubleTapped event or Gestures.DoubleRightTapped attached event")]
public int ClickCount => _obsoleteClickCount;
public int ClickCount => _clickCount;
[Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")]
public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton();

44
src/Avalonia.Input/TouchDevice.cs

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Raw;
using Avalonia.VisualTree;
using Avalonia.Platform;
namespace Avalonia.Input
{
@ -16,7 +16,9 @@ namespace Avalonia.Input
{
private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
private bool _disposed;
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) =>
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
@ -27,10 +29,10 @@ namespace Avalonia.Input
rv |= RawInputModifiers.LeftMouseButton;
return rv;
}
public void ProcessRawEvent(RawInputEventArgs ev)
{
if(_disposed)
if (_disposed)
return;
var args = (RawTouchEventArgs)ev;
if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
@ -43,16 +45,40 @@ namespace Avalonia.Input
PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit);
}
var target = pointer.Captured ?? args.Root;
if (args.Type == RawPointerEventType.TouchBegin)
{
if (_pointers.Count > 1)
{
_clickCount = 1;
_lastClickTime = 0;
_lastClickRect = new Rect();
}
else
{
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
if (settings == null)
{
throw new Exception("IPlatformSettings can not be null");
}
if (!_lastClickRect.Contains(args.Position)
|| ev.Timestamp - _lastClickTime > settings.DoubleClickTime.TotalMilliseconds)
{
_clickCount = 0;
}
++_clickCount;
_lastClickTime = ev.Timestamp;
_lastClickRect = new Rect(args.Position, new Size())
.Inflate(new Thickness(16, 16));
}
target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true),
PointerUpdateKind.LeftButtonPressed),
GetKeyModifiers(args.InputModifiers)));
GetKeyModifiers(args.InputModifiers), _clickCount));
}
if (args.Type == RawPointerEventType.TouchEnd)
@ -84,12 +110,12 @@ namespace Avalonia.Input
GetKeyModifiers(args.InputModifiers)));
}
}
public void Dispose()
{
if(_disposed)
if (_disposed)
return;
var values = _pointers.Values.ToList();
_pointers.Clear();
@ -97,6 +123,6 @@ namespace Avalonia.Input
foreach (var p in values)
p.Dispose();
}
}
}

7
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -142,6 +142,13 @@ namespace Avalonia.Native
private void DoLayoutReset(bool forceUpdate = false)
{
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
if (macOpts != null && macOpts.DisableNativeMenus)
{
return;
}
if (_resetQueued || forceUpdate)
{
_resetQueued = false;

5
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@ -73,5 +73,10 @@ namespace Avalonia
/// You can prevent Avalonia from adding those items to the OSX Application Menu with this property. The default value is false.
/// </summary>
public bool DisableDefaultApplicationMenuItems { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the native macOS menu bar will be enabled for the application.
/// </summary>
public bool DisableNativeMenus { get; set; }
}
}

1
src/Avalonia.Styling/Avalonia.Styling.csproj

@ -9,4 +9,5 @@
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
</Project>

12
src/Avalonia.Styling/Controls/ChildNameScope.cs

@ -15,20 +15,20 @@ namespace Avalonia.Controls
public void Register(string name, object element) => _inner.Register(name, element);
public SynchronousCompletionAsyncResult<object> FindAsync(string name)
public SynchronousCompletionAsyncResult<object?> FindAsync(string name)
{
var found = Find(name);
if (found != null)
return new SynchronousCompletionAsyncResult<object>(found);
return new SynchronousCompletionAsyncResult<object?>(found);
// Not found and both current and parent scope are in completed state
if(IsCompleted)
return new SynchronousCompletionAsyncResult<object>(null);
return new SynchronousCompletionAsyncResult<object?>(null);
return DoFindAsync(name);
}
public SynchronousCompletionAsyncResult<object> DoFindAsync(string name)
public SynchronousCompletionAsyncResult<object?> DoFindAsync(string name)
{
var src = new SynchronousCompletionAsyncResultSource<object>();
var src = new SynchronousCompletionAsyncResultSource<object?>();
void ParentSearch()
{
@ -56,7 +56,7 @@ namespace Avalonia.Controls
return src.AsyncResult;
}
public object Find(string name)
public object? Find(string name)
{
var found = _inner.Find(name);
if (found != null)

4
src/Avalonia.Styling/Controls/INameScope.cs

@ -22,14 +22,14 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="name">The name.</param>
/// <returns>The element, or null if the name was not found.</returns>
SynchronousCompletionAsyncResult<object> FindAsync(string name);
SynchronousCompletionAsyncResult<object?> FindAsync(string name);
/// <summary>
/// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack
/// </summary>
/// <param name="name">The name.</param>
/// <returns>The element, or null if the name was not found.</returns>
object Find(string name);
object? Find(string name);
/// <summary>
/// Marks the name scope as completed, no further registrations will be allowed

30
src/Avalonia.Styling/Controls/NameScope.cs

@ -22,8 +22,8 @@ namespace Avalonia.Controls
private readonly Dictionary<string, object> _inner = new Dictionary<string, object>();
private readonly Dictionary<string, SynchronousCompletionAsyncResultSource<object>> _pendingSearches =
new Dictionary<string, SynchronousCompletionAsyncResultSource<object>>();
private readonly Dictionary<string, SynchronousCompletionAsyncResultSource<object?>> _pendingSearches =
new Dictionary<string, SynchronousCompletionAsyncResultSource<object?>>();
/// <summary>
/// Gets the value of the attached <see cref="NameScopeProperty"/> on a styled element.
@ -32,7 +32,7 @@ namespace Avalonia.Controls
/// <returns>The value of the NameScope attached property.</returns>
public static INameScope GetNameScope(StyledElement styled)
{
Contract.Requires<ArgumentNullException>(styled != null);
_ = styled ?? throw new ArgumentNullException(nameof(styled));
return styled.GetValue(NameScopeProperty);
}
@ -44,7 +44,7 @@ namespace Avalonia.Controls
/// <param name="value">The value to set.</param>
public static void SetNameScope(StyledElement styled, INameScope value)
{
Contract.Requires<ArgumentNullException>(styled != null);
_ = styled ?? throw new ArgumentNullException(nameof(styled));
styled.SetValue(NameScopeProperty, value);
}
@ -54,12 +54,11 @@ namespace Avalonia.Controls
{
if (IsCompleted)
throw new InvalidOperationException("NameScope is completed, no further registrations are allowed");
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(element != null);
object existing;
_ = name ?? throw new ArgumentNullException(nameof(name));
_ = element ?? throw new ArgumentNullException(nameof(element));
if (_inner.TryGetValue(name, out existing))
if (_inner.TryGetValue(name, out var existing))
{
if (existing != element)
{
@ -77,27 +76,26 @@ namespace Avalonia.Controls
}
}
public SynchronousCompletionAsyncResult<object> FindAsync(string name)
public SynchronousCompletionAsyncResult<object?> FindAsync(string name)
{
var found = Find(name);
if (found != null)
return new SynchronousCompletionAsyncResult<object>(found);
return new SynchronousCompletionAsyncResult<object?>(found);
if (IsCompleted)
return new SynchronousCompletionAsyncResult<object>((object)null);
return new SynchronousCompletionAsyncResult<object?>(null);
if (!_pendingSearches.TryGetValue(name, out var tcs))
// We are intentionally running continuations synchronously here
_pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource<object>();
_pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource<object?>();
return tcs.AsyncResult;
}
/// <inheritdoc />
public object Find(string name)
public object? Find(string name)
{
Contract.Requires<ArgumentNullException>(name != null);
_ = name ?? throw new ArgumentNullException(nameof(name));
object result;
_inner.TryGetValue(name, out result);
_inner.TryGetValue(name, out var result);
return result;
}

28
src/Avalonia.Styling/Controls/NameScopeExtensions.cs

@ -17,11 +17,11 @@ namespace Avalonia.Controls
/// <param name="nameScope">The name scope.</param>
/// <param name="name">The name.</param>
/// <returns>The named element or null if not found.</returns>
public static T Find<T>(this INameScope nameScope, string name)
public static T? Find<T>(this INameScope nameScope, string name)
where T : class
{
Contract.Requires<ArgumentNullException>(nameScope != null);
Contract.Requires<ArgumentNullException>(name != null);
_ = nameScope ?? throw new ArgumentNullException(nameof(nameScope));
_ = name ?? throw new ArgumentNullException(nameof(name));
var result = nameScope.Find(name);
@ -31,7 +31,7 @@ namespace Avalonia.Controls
$"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
}
return (T)result;
return (T?)result;
}
/// <summary>
@ -41,11 +41,11 @@ namespace Avalonia.Controls
/// <param name="anchor">The control to take the name scope from.</param>
/// <param name="name">The name.</param>
/// <returns>The named element or null if not found.</returns>
public static T Find<T>(this ILogical anchor, string name)
public static T? Find<T>(this ILogical anchor, string name)
where T : class
{
Contract.Requires<ArgumentNullException>(anchor != null);
Contract.Requires<ArgumentNullException>(name != null);
_ = anchor ?? throw new ArgumentNullException(nameof(anchor));
_ = name ?? throw new ArgumentNullException(nameof(name));
var styledAnchor = anchor as StyledElement;
if (styledAnchor == null)
return null;
@ -64,8 +64,8 @@ namespace Avalonia.Controls
public static T Get<T>(this INameScope nameScope, string name)
where T : class
{
Contract.Requires<ArgumentNullException>(nameScope != null);
Contract.Requires<ArgumentNullException>(name != null);
_ = nameScope ?? throw new ArgumentNullException(nameof(nameScope));
_ = name ?? throw new ArgumentNullException(nameof(name));
var result = nameScope.Find(name);
@ -94,9 +94,9 @@ namespace Avalonia.Controls
public static T Get<T>(this ILogical anchor, string name)
where T : class
{
Contract.Requires<ArgumentNullException>(anchor != null);
Contract.Requires<ArgumentNullException>(name != null);
_ = anchor ?? throw new ArgumentNullException(nameof(anchor));
_ = name ?? throw new ArgumentNullException(nameof(name));
var nameScope = (anchor as INameScope) ?? NameScope.GetNameScope((StyledElement)anchor);
if (nameScope == null)
throw new InvalidOperationException(
@ -105,9 +105,9 @@ namespace Avalonia.Controls
return nameScope.Get<T>(name);
}
public static INameScope FindNameScope(this ILogical control)
public static INameScope? FindNameScope(this ILogical control)
{
Contract.Requires<ArgumentNullException>(control != null);
_ = control ?? throw new ArgumentNullException(nameof(control));
var scope = control.GetSelfAndLogicalAncestors()
.OfType<StyledElement>()

8
src/Avalonia.Styling/Controls/NameScopeLocator.cs

@ -11,9 +11,9 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="scope">The scope relative from which the object should be resolved.</param>
/// <param name="name">The name of the object to find.</param>
public static IObservable<object> Track(INameScope scope, string name)
public static IObservable<object?> Track(INameScope scope, string name)
{
return new NeverEndingSynchronousCompletionAsyncResultObservable<object>(scope.FindAsync(name));
return new NeverEndingSynchronousCompletionAsyncResultObservable<object?>(scope.FindAsync(name));
}
// This class is implemented in such weird way because for some reason
@ -22,7 +22,7 @@ namespace Avalonia.Controls
private class NeverEndingSynchronousCompletionAsyncResultObservable<T> : IObservable<T>
{
private T _value;
private T? _value;
private SynchronousCompletionAsyncResult<T>? _asyncResult;
public NeverEndingSynchronousCompletionAsyncResultObservable(SynchronousCompletionAsyncResult<T> task)
@ -47,7 +47,7 @@ namespace Avalonia.Controls
observer.OnNext(_asyncResult.Value.GetResult());
});
else
observer.OnNext(_value);
observer.OnNext(_value!);
return Disposable.Empty;
}

2
src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs

@ -12,7 +12,7 @@ namespace Avalonia.Controls
/// <param name="value">True to add the pseudoclass or false to remove.</param>
public static void Set(this IPseudoClasses classes, string name, bool value)
{
Contract.Requires<ArgumentNullException>(classes != null);
_ = classes ?? throw new ArgumentNullException(nameof(classes));
if (value)
{

2
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@ -177,7 +177,7 @@ namespace Avalonia.Controls
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}

6
src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs

@ -109,7 +109,7 @@ namespace Avalonia.Controls
observer.OnNext(Convert(_target.FindResource(_key)));
}
private void ResourcesChanged(object sender, ResourcesChangedEventArgs e)
private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e)
{
PublishNext(Convert(_target.FindResource(_key)));
}
@ -159,7 +159,7 @@ namespace Avalonia.Controls
}
}
private void OwnerChanged(object sender, EventArgs e)
private void OwnerChanged(object? sender, EventArgs e)
{
if (_owner is object)
{
@ -176,7 +176,7 @@ namespace Avalonia.Controls
PublishNext();
}
private void ResourcesChanged(object sender, ResourcesChangedEventArgs e)
private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e)
{
PublishNext();
}

2
src/Avalonia.Styling/INamed.cs

@ -8,6 +8,6 @@ namespace Avalonia
/// <summary>
/// Gets the element name.
/// </summary>
string Name { get; }
string? Name { get; }
}
}

16
src/Avalonia.Styling/LogicalTree/ControlLocator.cs

@ -9,19 +9,19 @@ namespace Avalonia.LogicalTree
/// </summary>
public static class ControlLocator
{
public static IObservable<ILogical> Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null)
public static IObservable<ILogical?> Track(ILogical relativeTo, int ancestorLevel, Type? ancestorType = null)
{
return new ControlTracker(relativeTo, ancestorLevel, ancestorType);
}
private class ControlTracker : LightweightObservableBase<ILogical>
private class ControlTracker : LightweightObservableBase<ILogical?>
{
private readonly ILogical _relativeTo;
private readonly int _ancestorLevel;
private readonly Type _ancestorType;
private ILogical _value;
private readonly Type? _ancestorType;
private ILogical? _value;
public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType)
public ControlTracker(ILogical relativeTo, int ancestorLevel, Type? ancestorType)
{
_relativeTo = relativeTo;
_ancestorLevel = ancestorLevel;
@ -43,18 +43,18 @@ namespace Avalonia.LogicalTree
_value = null;
}
protected override void Subscribed(IObserver<ILogical> observer, bool first)
protected override void Subscribed(IObserver<ILogical?> observer, bool first)
{
observer.OnNext(_value);
}
private void Attached(object sender, LogicalTreeAttachmentEventArgs e)
private void Attached(object? sender, LogicalTreeAttachmentEventArgs e)
{
Update();
PublishNext(_value);
}
private void Detached(object sender, LogicalTreeAttachmentEventArgs e)
private void Detached(object? sender, LogicalTreeAttachmentEventArgs e)
{
_value = null;
PublishNext(null);

6
src/Avalonia.Styling/LogicalTree/ILogical.cs

@ -12,12 +12,12 @@ namespace Avalonia.LogicalTree
/// <summary>
/// Raised when the control is attached to a rooted logical tree.
/// </summary>
event EventHandler<LogicalTreeAttachmentEventArgs> AttachedToLogicalTree;
event EventHandler<LogicalTreeAttachmentEventArgs>? AttachedToLogicalTree;
/// <summary>
/// Raised when the control is detached from a rooted logical tree.
/// </summary>
event EventHandler<LogicalTreeAttachmentEventArgs> DetachedFromLogicalTree;
event EventHandler<LogicalTreeAttachmentEventArgs>? DetachedFromLogicalTree;
/// <summary>
/// Gets a value indicating whether the element is attached to a rooted logical tree.
@ -27,7 +27,7 @@ namespace Avalonia.LogicalTree
/// <summary>
/// Gets the logical parent.
/// </summary>
ILogical LogicalParent { get; }
ILogical? LogicalParent { get; }
/// <summary>
/// Gets the logical children.

26
src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs

@ -15,14 +15,14 @@ namespace Avalonia.LogicalTree
/// <returns>The logical's ancestors.</returns>
public static IEnumerable<ILogical> GetLogicalAncestors(this ILogical logical)
{
Contract.Requires<ArgumentNullException>(logical != null);
_ = logical ?? throw new ArgumentNullException(nameof(logical));
logical = logical.LogicalParent;
ILogical? l = logical.LogicalParent;
while (logical != null)
while (l != null)
{
yield return logical;
logical = logical.LogicalParent;
yield return l;
l = l.LogicalParent;
}
}
@ -48,14 +48,14 @@ namespace Avalonia.LogicalTree
/// <param name="logical">The logical.</param>
/// <param name="includeSelf">If given logical should be included in search.</param>
/// <returns>First ancestor of given type.</returns>
public static T FindLogicalAncestorOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
public static T? FindLogicalAncestorOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
{
if (logical is null)
{
return null;
}
ILogical parent = includeSelf ? logical : logical.LogicalParent;
var parent = includeSelf ? logical : logical.LogicalParent;
while (parent != null)
{
@ -120,7 +120,7 @@ namespace Avalonia.LogicalTree
/// <param name="logical">The logical.</param>
/// <param name="includeSelf">If given logical should be included in search.</param>
/// <returns>First descendant of given type.</returns>
public static T FindLogicalDescendantOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
public static T? FindLogicalDescendantOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
{
if (logical is null)
{
@ -140,7 +140,7 @@ namespace Avalonia.LogicalTree
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The parent, or null if the logical is unparented.</returns>
public static ILogical GetLogicalParent(this ILogical logical)
public static ILogical? GetLogicalParent(this ILogical logical)
{
return logical.LogicalParent;
}
@ -153,7 +153,7 @@ namespace Avalonia.LogicalTree
/// <returns>
/// The parent, or null if the logical is unparented or its parent is not of type <typeparamref name="T"/>.
/// </returns>
public static T GetLogicalParent<T>(this ILogical logical) where T : class
public static T? GetLogicalParent<T>(this ILogical logical) where T : class
{
return logical.LogicalParent as T;
}
@ -165,7 +165,7 @@ namespace Avalonia.LogicalTree
/// <returns>The logical siblings.</returns>
public static IEnumerable<ILogical> GetLogicalSiblings(this ILogical logical)
{
ILogical parent = logical.LogicalParent;
var parent = logical.LogicalParent;
if (parent != null)
{
@ -187,7 +187,7 @@ namespace Avalonia.LogicalTree
/// </returns>
public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target)
{
ILogical current = target?.LogicalParent;
var current = target?.LogicalParent;
while (current != null)
{
@ -202,7 +202,7 @@ namespace Avalonia.LogicalTree
return false;
}
private static T FindDescendantOfTypeCore<T>(ILogical logical) where T : class
private static T? FindDescendantOfTypeCore<T>(ILogical logical) where T : class
{
var logicalChildren = logical.LogicalChildren;
var logicalChildrenCount = logicalChildren.Count;

7
src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs

@ -19,11 +19,8 @@ namespace Avalonia.LogicalTree
ILogical source,
ILogical parent)
{
Contract.Requires<ArgumentNullException>(root != null);
Contract.Requires<ArgumentNullException>(source != null);
Root = root;
Source = source;
Root = root ?? throw new ArgumentNullException(nameof(root));
Source = source ?? throw new ArgumentNullException(nameof(source));
Parent = parent;
}

18
src/Avalonia.Styling/StyledElement.cs

@ -427,7 +427,7 @@ namespace Avalonia
if (_logicalRoot != null)
{
var e = new LogicalTreeAttachmentEventArgs(_logicalRoot, this, old);
var e = new LogicalTreeAttachmentEventArgs(_logicalRoot, this, old!);
OnDetachedFromLogicalTreeCore(e);
}
@ -435,7 +435,7 @@ namespace Avalonia
if (newRoot is object)
{
var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent);
var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent!);
OnAttachedToLogicalTreeCore(e);
}
else if (parent is null)
@ -495,21 +495,21 @@ namespace Avalonia
DetachStylesFromThisAndDescendents(allStyles);
}
protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
protected virtual void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SetLogicalParent(e.NewItems);
SetLogicalParent(e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
ClearLogicalParent(e.OldItems);
ClearLogicalParent(e.OldItems!);
break;
case NotifyCollectionChangedAction.Replace:
ClearLogicalParent(e.OldItems);
SetLogicalParent(e.NewItems);
ClearLogicalParent(e.OldItems!);
SetLogicalParent(e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
@ -729,7 +729,7 @@ namespace Avalonia
for (var i = 0; i < count; i++)
{
var logical = (ILogical) children[i];
var logical = (ILogical) children[i]!;
if (logical.LogicalParent is null)
{
@ -744,7 +744,7 @@ namespace Avalonia
for (var i = 0; i < count; i++)
{
var logical = (ILogical) children[i];
var logical = (ILogical) children[i]!;
if (logical.LogicalParent == this)
{

2
src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs

@ -37,7 +37,7 @@ namespace Avalonia.Styling.Activators
_provider.ChildIndexChanged -= ChildIndexChanged;
}
private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{
// Run matching again if:
// 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.

2
src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs

@ -66,7 +66,7 @@ namespace Avalonia.Styling.Activators
_classes.CollectionChanged -= ClassesChangedHandler;
}
private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
private void ClassesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Move)
{

6
src/Avalonia.Styling/Styling/ChildSelector.cs

@ -6,7 +6,7 @@ namespace Avalonia.Styling
internal class ChildSelector : Selector
{
private readonly Selector _parent;
private string _selectorString;
private string? _selectorString;
public ChildSelector(Selector parent)
{
@ -25,7 +25,7 @@ namespace Avalonia.Styling
public override bool IsCombinator => true;
/// <inheritdoc/>
public override Type TargetType => null;
public override Type? TargetType => null;
public override string ToString()
{
@ -64,6 +64,6 @@ namespace Avalonia.Styling
}
}
protected override Selector MovePrevious() => null;
protected override Selector? MovePrevious() => null;
}
}

2
src/Avalonia.Styling/Styling/OrSelector.cs

@ -120,7 +120,7 @@ namespace Avalonia.Styling
}
else
{
while (!result.IsAssignableFrom(selector.TargetType))
while (result is not null && !result.IsAssignableFrom(selector.TargetType))
{
result = result.BaseType;
}

2
src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs

@ -109,7 +109,7 @@ namespace Avalonia.Styling
var converter = TypeDescriptor.GetConverter(propertyType);
if (converter?.CanConvertFrom(valueType) == true)
{
return Equals(propertyValue, converter.ConvertFrom(null, CultureInfo.InvariantCulture, value));
return Equals(propertyValue, converter.ConvertFrom(null, CultureInfo.InvariantCulture, value!));
}
return false;

50
src/Avalonia.Styling/Styling/Selectors.cs

@ -25,10 +25,14 @@ namespace Avalonia.Styling
/// <param name="previous">The previous selector.</param>
/// <param name="name">The name of the style class.</param>
/// <returns>The selector.</returns>
public static Selector Class(this Selector previous, string name)
public static Selector Class(this Selector? previous, string name)
{
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentException>(!string.IsNullOrWhiteSpace(name));
_ = name ?? throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Name may not be empty", nameof(name));
}
var tac = previous as TypeNameAndClassSelector;
@ -48,7 +52,7 @@ namespace Avalonia.Styling
/// </summary>
/// <param name="previous">The previous selector.</param>
/// <returns>The selector.</returns>
public static Selector Descendant(this Selector previous)
public static Selector Descendant(this Selector? previous)
{
return new DescendantSelector(previous);
}
@ -59,9 +63,9 @@ namespace Avalonia.Styling
/// <param name="previous">The previous selector.</param>
/// <param name="type">The type.</param>
/// <returns>The selector.</returns>
public static Selector Is(this Selector previous, Type type)
public static Selector Is(this Selector? previous, Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
_ = type ?? throw new ArgumentNullException(nameof(type));
return TypeNameAndClassSelector.Is(previous, type);
}
@ -72,7 +76,7 @@ namespace Avalonia.Styling
/// <typeparam name="T">The type.</typeparam>
/// <param name="previous">The previous selector.</param>
/// <returns>The selector.</returns>
public static Selector Is<T>(this Selector previous) where T : IStyleable
public static Selector Is<T>(this Selector? previous) where T : IStyleable
{
return previous.Is(typeof(T));
}
@ -83,10 +87,14 @@ namespace Avalonia.Styling
/// <param name="previous">The previous selector.</param>
/// <param name="name">The name.</param>
/// <returns>The selector.</returns>
public static Selector Name(this Selector previous, string name)
public static Selector Name(this Selector? previous, string name)
{
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentException>(!string.IsNullOrWhiteSpace(name));
_ = name ?? throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Name may not be empty", nameof(name));
}
var tac = previous as TypeNameAndClassSelector;
@ -107,7 +115,7 @@ namespace Avalonia.Styling
/// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</param>
/// <returns>The selector.</returns>
public static Selector Not(this Selector previous, Func<Selector, Selector> argument)
public static Selector Not(this Selector? previous, Func<Selector?, Selector> argument)
{
return new NotSelector(previous, argument(null));
}
@ -118,7 +126,7 @@ namespace Avalonia.Styling
/// <param name="previous">The previous selector.</param>
/// <param name="argument">The selector to be not-ed.</param>
/// <returns>The selector.</returns>
public static Selector Not(this Selector previous, Selector argument)
public static Selector Not(this Selector? previous, Selector argument)
{
return new NotSelector(previous, argument);
}
@ -126,7 +134,7 @@ namespace Avalonia.Styling
/// <inheritdoc cref="NthChildSelector"/>
/// <inheritdoc cref="NthChildSelector(Selector?, int, int)"/>
/// <returns>The selector.</returns>
public static Selector NthChild(this Selector previous, int step, int offset)
public static Selector NthChild(this Selector? previous, int step, int offset)
{
return new NthChildSelector(previous, step, offset);
}
@ -134,7 +142,7 @@ namespace Avalonia.Styling
/// <inheritdoc cref="NthLastChildSelector"/>
/// <inheritdoc cref="NthLastChildSelector(Selector?, int, int)"/>
/// <returns>The selector.</returns>
public static Selector NthLastChild(this Selector previous, int step, int offset)
public static Selector NthLastChild(this Selector? previous, int step, int offset)
{
return new NthLastChildSelector(previous, step, offset);
}
@ -145,9 +153,9 @@ namespace Avalonia.Styling
/// <param name="previous">The previous selector.</param>
/// <param name="type">The type.</param>
/// <returns>The selector.</returns>
public static Selector OfType(this Selector previous, Type type)
public static Selector OfType(this Selector? previous, Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
_ = type ?? throw new ArgumentNullException(nameof(type));
return TypeNameAndClassSelector.OfType(previous, type);
}
@ -158,7 +166,7 @@ namespace Avalonia.Styling
/// <typeparam name="T">The type.</typeparam>
/// <param name="previous">The previous selector.</param>
/// <returns>The selector.</returns>
public static Selector OfType<T>(this Selector previous) where T : IStyleable
public static Selector OfType<T>(this Selector? previous) where T : IStyleable
{
return previous.OfType(typeof(T));
}
@ -191,9 +199,9 @@ namespace Avalonia.Styling
/// <param name="property">The property.</param>
/// <param name="value">The property value.</param>
/// <returns>The selector.</returns>
public static Selector PropertyEquals<T>(this Selector previous, AvaloniaProperty<T> property, object value)
public static Selector PropertyEquals<T>(this Selector? previous, AvaloniaProperty<T> property, object? value)
{
Contract.Requires<ArgumentNullException>(property != null);
_ = property ?? throw new ArgumentNullException(nameof(property));
return new PropertyEqualsSelector(previous, property, value);
}
@ -205,9 +213,9 @@ namespace Avalonia.Styling
/// <param name="property">The property.</param>
/// <param name="value">The property value.</param>
/// <returns>The selector.</returns>
public static Selector PropertyEquals(this Selector previous, AvaloniaProperty property, object value)
public static Selector PropertyEquals(this Selector? previous, AvaloniaProperty property, object? value)
{
Contract.Requires<ArgumentNullException>(property != null);
_ = property ?? throw new ArgumentNullException(nameof(property));
return new PropertyEqualsSelector(previous, property, value);
}

14
src/Avalonia.Styling/Styling/Styles.cs

@ -262,7 +262,7 @@ namespace Avalonia.Styling
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
static IReadOnlyList<T> ToReadOnlyList<T>(IList list)
{
@ -282,7 +282,7 @@ namespace Avalonia.Styling
{
for (var i = 0; i < items.Count; ++i)
{
var style = (IStyle)items[i];
var style = (IStyle)items[i]!;
if (Owner is object && style is IResourceProvider resourceProvider)
{
@ -299,7 +299,7 @@ namespace Avalonia.Styling
{
for (var i = 0; i < items.Count; ++i)
{
var style = (IStyle)items[i];
var style = (IStyle)items[i]!;
if (Owner is object && style is IResourceProvider resourceProvider)
{
@ -315,14 +315,14 @@ namespace Avalonia.Styling
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewItems);
Add(e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
Remove(e.OldItems);
Remove(e.OldItems!);
break;
case NotifyCollectionChangedAction.Replace:
Remove(e.OldItems);
Add(e.NewItems);
Remove(e.OldItems!);
Add(e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
throw new InvalidOperationException("Reset should not be called on Styles.");

6
src/Avalonia.Styling/Styling/TemplateSelector.cs

@ -5,7 +5,7 @@ namespace Avalonia.Styling
internal class TemplateSelector : Selector
{
private readonly Selector _parent;
private string _selectorString;
private string? _selectorString;
public TemplateSelector(Selector parent)
{
@ -24,7 +24,7 @@ namespace Avalonia.Styling
public override bool IsCombinator => true;
/// <inheritdoc/>
public override Type TargetType => null;
public override Type? TargetType => null;
public override string ToString()
{
@ -48,6 +48,6 @@ namespace Avalonia.Styling
return _parent.Match(templatedParent, subscribe);
}
protected override Selector MovePrevious() => null;
protected override Selector? MovePrevious() => null;
}
}

2
src/Avalonia.Visuals/Visual.cs

@ -377,7 +377,7 @@ namespace Avalonia
}
}
protected override void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
base.LogicalChildrenCollectionChanged(sender, e);
VisualRoot?.Renderer?.RecalculateChildren(this);

6
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@ -138,16 +138,16 @@ namespace Avalonia.Markup.Parsers
break;
case SelectorGrammar.ChildSyntax child:
result = result.Child();
result = result!.Child();
break;
case SelectorGrammar.DescendantSyntax descendant:
result = result.Descendant();
break;
case SelectorGrammar.TemplateSyntax template:
result = result.Template();
result = result!.Template();
break;
case SelectorGrammar.NotSyntax not:
result = result.Not(x => Create(not.Argument));
result = result.Not(x => Create(not.Argument)!);
break;
case SelectorGrammar.NthChildSyntax nth:
result = result.NthChild(nth.Step, nth.Offset);

272
tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs

@ -0,0 +1,272 @@
using System;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class TouchDeviceTests
{
[Fact]
public void Tapped_Event_Is_Fired_With_Touch()
{
using (UnitTestApplication.Start(
new TestServices(inputManager: new InputManager())))
{
var root = new TestRoot();
var touchDevice = new TouchDevice();
var isTapped = false;
var executedTimes = 0;
root.Tapped += (a, e) =>
{
isTapped = true;
executedTimes++;
};
TapOnce(InputManager.Instance, touchDevice, root);
Assert.True(isTapped);
Assert.Equal(1, executedTimes);
}
}
[Fact]
public void DoubleTapped_Event_Is_Fired_With_Touch()
{
var platformSettingsMock = new Mock<IPlatformSettings>();
platformSettingsMock.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(platformSettingsMock.Object);
using (UnitTestApplication.Start(
new TestServices(inputManager: new InputManager())))
{
var root = new TestRoot();
var touchDevice = new TouchDevice();
var isDoubleTapped = false;
var doubleTappedExecutedTimes = 0;
var tappedExecutedTimes = 0;
root.DoubleTapped += (a, e) =>
{
isDoubleTapped = true;
doubleTappedExecutedTimes++;
};
root.Tapped += (a, e) =>
{
tappedExecutedTimes++;
};
TapOnce(InputManager.Instance, touchDevice, root);
TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 1);
Assert.Equal(1, tappedExecutedTimes);
Assert.True(isDoubleTapped);
Assert.Equal(1, doubleTappedExecutedTimes);
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
public void PointerPressed_Counts_Clicks_Correctly(int clickCount)
{
var platformSettingsMock = new Mock<IPlatformSettings>();
platformSettingsMock.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(platformSettingsMock.Object);
using (UnitTestApplication.Start(
new TestServices(inputManager: new InputManager())))
{
var root = new TestRoot();
var touchDevice = new TouchDevice();
var pointerPressedExecutedTimes = 0;
var pointerPressedClicks = 0;
root.PointerPressed += (a, e) =>
{
pointerPressedClicks = e.ClickCount;
pointerPressedExecutedTimes++;
};
for (int i = 0; i < clickCount; i++)
{
TapOnce(InputManager.Instance, touchDevice, root, touchPointId: i);
}
Assert.Equal(clickCount, pointerPressedExecutedTimes);
Assert.Equal(pointerPressedClicks, clickCount);
}
}
[Fact]
public void DoubleTapped_Not_Fired_When_Click_Too_Late()
{
var platformSettingsMock = new Mock<IPlatformSettings>();
platformSettingsMock.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(0, 0, 0, 0, 20));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(platformSettingsMock.Object);
using (UnitTestApplication.Start(
new TestServices(inputManager: new InputManager())))
{
var root = new TestRoot();
var touchDevice = new TouchDevice();
var isDoubleTapped = false;
var doubleTappedExecutedTimes = 0;
var tappedExecutedTimes = 0;
root.DoubleTapped += (a, e) =>
{
isDoubleTapped = true;
doubleTappedExecutedTimes++;
};
root.Tapped += (a, e) =>
{
tappedExecutedTimes++;
};
TapOnce(InputManager.Instance, touchDevice, root);
TapOnce(InputManager.Instance, touchDevice, root, 21, 1);
Assert.Equal(2, tappedExecutedTimes);
Assert.False(isDoubleTapped);
Assert.Equal(0, doubleTappedExecutedTimes);
}
}
[Fact]
public void DoubleTapped_Not_Fired_When_Second_Click_Is_From_Different_Touch_Contact()
{
var tmp = new Mock<IPlatformSettings>();
tmp.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(tmp.Object);
using (UnitTestApplication.Start(
new TestServices(inputManager: new InputManager())))
{
var root = new TestRoot();
var touchDevice = new TouchDevice();
var isDoubleTapped = false;
var doubleTappedExecutedTimes = 0;
var tappedExecutedTimes = 0;
root.DoubleTapped += (a, e) =>
{
isDoubleTapped = true;
doubleTappedExecutedTimes++;
};
root.Tapped += (a, e) =>
{
tappedExecutedTimes++;
};
SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchBegin, 0, 1);
SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchEnd, 0, 1);
Assert.Equal(2, tappedExecutedTimes);
Assert.False(isDoubleTapped);
Assert.Equal(0, doubleTappedExecutedTimes);
}
}
[Fact]
public void Click_Counting_Should_Work_Correctly_With_Few_Touch_Contacts()
{
var tmp = new Mock<IPlatformSettings>();
tmp.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(tmp.Object);
using (UnitTestApplication.Start(
new TestServices(inputManager: new InputManager())))
{
var root = new TestRoot();
var touchDevice = new TouchDevice();
var pointerPressedExecutedTimes = 0;
var tappedExecutedTimes = 0;
var isDoubleTapped = false;
var doubleTappedExecutedTimes = 0;
root.PointerPressed += (a, e) =>
{
pointerPressedExecutedTimes++;
switch (pointerPressedExecutedTimes)
{
case <= 2:
Assert.True(e.ClickCount == 1);
break;
case 3:
Assert.True(e.ClickCount == 2);
break;
case 4:
Assert.True(e.ClickCount == 3);
break;
case 5:
Assert.True(e.ClickCount == 4);
break;
case 6:
Assert.True(e.ClickCount == 5);
break;
case 7:
Assert.True(e.ClickCount == 1);
break;
case 8:
Assert.True(e.ClickCount == 1);
break;
case 9:
Assert.True(e.ClickCount == 2);
break;
default:
break;
}
};
root.DoubleTapped += (a, e) =>
{
isDoubleTapped = true;
doubleTappedExecutedTimes++;
};
root.Tapped += (a, e) =>
{
tappedExecutedTimes++;
};
SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchBegin, 0, 1);
SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchEnd, 0, 1);
TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 2);
TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 3);
TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 4);
SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchBegin, 5, 6, 7);
SendXTouchContactsWithIds(InputManager.Instance, touchDevice, root, RawPointerEventType.TouchEnd, 5, 6, 7);
TapOnce(InputManager.Instance, touchDevice, root, touchPointId: 8);
Assert.Equal(6, tappedExecutedTimes);
Assert.Equal(9, pointerPressedExecutedTimes);
Assert.True(isDoubleTapped);
Assert.Equal(3, doubleTappedExecutedTimes);
}
}
private static void SendXTouchContactsWithIds(IInputManager inputManager, TouchDevice device, IInputRoot root, RawPointerEventType type, params long[] touchPointIds)
{
for (int i = 0; i < touchPointIds.Length; i++)
{
inputManager.ProcessInput(new RawTouchEventArgs(device, 0,
root,
type,
new Point(0, 0),
RawInputModifiers.None,
touchPointIds[i]));
}
}
private static void TapOnce(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0)
{
inputManager.ProcessInput(new RawTouchEventArgs(device, timestamp,
root,
RawPointerEventType.TouchBegin,
new Point(0, 0),
RawInputModifiers.None,
touchPointId));
inputManager.ProcessInput(new RawTouchEventArgs(device, timestamp,
root,
RawPointerEventType.TouchEnd,
new Point(0, 0),
RawInputModifiers.None,
touchPointId));
}
}
}
Loading…
Cancel
Save