From 048cf388bcdb570f3fb176b5ea77e15919057948 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Dec 2021 17:59:08 +0000 Subject: [PATCH 1/5] allow disabling of osx menu export. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 7 +++++++ src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index b9d8fd3711..bda1c91750 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -142,6 +142,13 @@ namespace Avalonia.Native private void DoLayoutReset(bool forceUpdate = false) { + var macOpts = AvaloniaLocator.Current.GetService(); + + if (macOpts != null && macOpts.DisableNativeMenus) + { + return; + } + if (_resetQueued || forceUpdate) { _resetQueued = false; diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index eef765e7ec..809c063b47 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/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. /// public bool DisableDefaultApplicationMenuItems { get; set; } + + /// + /// + /// + public bool DisableNativeMenus { get; set; } } } From 18115363f8445e3ea9894a098497ddd48ab0f745 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 17 Dec 2021 10:45:35 +0800 Subject: [PATCH 2/5] Update src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs Co-authored-by: Max Katz --- src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 809c063b47..10619d675b 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -75,7 +75,7 @@ namespace Avalonia public bool DisableDefaultApplicationMenuItems { get; set; } /// - /// + /// Gets or sets a value indicating whether the native macOS menu bar will be enabled for the application. /// public bool DisableNativeMenus { get; set; } } From daee30758e478bdd1d222b242de2012f280863b6 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Wed, 22 Dec 2021 11:50:25 +0200 Subject: [PATCH 3/5] Update readme --- readme.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 3efc3211ee..b38552a010 100644 --- a/readme.md +++ b/readme.md @@ -3,9 +3,6 @@
[![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)] From 328199b2d22aafcb194f961508ac8b59d394b820 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Dec 2021 14:02:28 +0100 Subject: [PATCH 4/5] Added nullable annotations to Avalonia.Styling. --- src/Avalonia.Styling/Avalonia.Styling.csproj | 1 + .../Controls/ChildNameScope.cs | 12 ++--- src/Avalonia.Styling/Controls/INameScope.cs | 4 +- src/Avalonia.Styling/Controls/NameScope.cs | 30 ++++++----- .../Controls/NameScopeExtensions.cs | 28 +++++------ .../Controls/NameScopeLocator.cs | 8 +-- .../Controls/PseudoClassesExtensions.cs | 2 +- .../Controls/ResourceDictionary.cs | 2 +- .../Controls/ResourceNodeExtensions.cs | 6 +-- src/Avalonia.Styling/INamed.cs | 2 +- .../LogicalTree/ControlLocator.cs | 16 +++--- src/Avalonia.Styling/LogicalTree/ILogical.cs | 6 +-- .../LogicalTree/LogicalExtensions.cs | 26 +++++----- .../LogicalTreeAttachmentEventArgs.cs | 7 +-- src/Avalonia.Styling/StyledElement.cs | 18 +++---- .../Styling/Activators/NthChildActivator.cs | 2 +- .../Styling/Activators/StyleClassActivator.cs | 2 +- src/Avalonia.Styling/Styling/ChildSelector.cs | 6 +-- src/Avalonia.Styling/Styling/OrSelector.cs | 2 +- .../Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Styling/Styling/Selectors.cs | 50 +++++++++++-------- src/Avalonia.Styling/Styling/Styles.cs | 14 +++--- .../Styling/TemplateSelector.cs | 6 +-- src/Avalonia.Visuals/Visual.cs | 2 +- .../Markup/Parsers/SelectorParser.cs | 6 +-- 25 files changed, 132 insertions(+), 128 deletions(-) diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index 20b3183b00..3548749846 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -9,4 +9,5 @@ + diff --git a/src/Avalonia.Styling/Controls/ChildNameScope.cs b/src/Avalonia.Styling/Controls/ChildNameScope.cs index e6707e71db..58114a57fd 100644 --- a/src/Avalonia.Styling/Controls/ChildNameScope.cs +++ b/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 FindAsync(string name) + public SynchronousCompletionAsyncResult FindAsync(string name) { var found = Find(name); if (found != null) - return new SynchronousCompletionAsyncResult(found); + return new SynchronousCompletionAsyncResult(found); // Not found and both current and parent scope are in completed state if(IsCompleted) - return new SynchronousCompletionAsyncResult(null); + return new SynchronousCompletionAsyncResult(null); return DoFindAsync(name); } - public SynchronousCompletionAsyncResult DoFindAsync(string name) + public SynchronousCompletionAsyncResult DoFindAsync(string name) { - var src = new SynchronousCompletionAsyncResultSource(); + var src = new SynchronousCompletionAsyncResultSource(); 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) diff --git a/src/Avalonia.Styling/Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs index 2d5295fe45..1ca7db2f37 100644 --- a/src/Avalonia.Styling/Controls/INameScope.cs +++ b/src/Avalonia.Styling/Controls/INameScope.cs @@ -22,14 +22,14 @@ namespace Avalonia.Controls /// /// The name. /// The element, or null if the name was not found. - SynchronousCompletionAsyncResult FindAsync(string name); + SynchronousCompletionAsyncResult FindAsync(string name); /// /// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack /// /// The name. /// The element, or null if the name was not found. - object Find(string name); + object? Find(string name); /// /// Marks the name scope as completed, no further registrations will be allowed diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs index 62a04eac8b..77f98f85c4 100644 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ b/src/Avalonia.Styling/Controls/NameScope.cs @@ -22,8 +22,8 @@ namespace Avalonia.Controls private readonly Dictionary _inner = new Dictionary(); - private readonly Dictionary> _pendingSearches = - new Dictionary>(); + private readonly Dictionary> _pendingSearches = + new Dictionary>(); /// /// Gets the value of the attached on a styled element. @@ -32,7 +32,7 @@ namespace Avalonia.Controls /// The value of the NameScope attached property. public static INameScope GetNameScope(StyledElement styled) { - Contract.Requires(styled != null); + _ = styled ?? throw new ArgumentNullException(nameof(styled)); return styled.GetValue(NameScopeProperty); } @@ -44,7 +44,7 @@ namespace Avalonia.Controls /// The value to set. public static void SetNameScope(StyledElement styled, INameScope value) { - Contract.Requires(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(name != null); - Contract.Requires(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 FindAsync(string name) + public SynchronousCompletionAsyncResult FindAsync(string name) { var found = Find(name); if (found != null) - return new SynchronousCompletionAsyncResult(found); + return new SynchronousCompletionAsyncResult(found); if (IsCompleted) - return new SynchronousCompletionAsyncResult((object)null); + return new SynchronousCompletionAsyncResult(null); if (!_pendingSearches.TryGetValue(name, out var tcs)) // We are intentionally running continuations synchronously here - _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource(); + _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource(); return tcs.AsyncResult; } /// - public object Find(string name) + public object? Find(string name) { - Contract.Requires(name != null); + _ = name ?? throw new ArgumentNullException(nameof(name)); - object result; - _inner.TryGetValue(name, out result); + _inner.TryGetValue(name, out var result); return result; } diff --git a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs index 75630711b8..3895b6ceb9 100644 --- a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs +++ b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs @@ -17,11 +17,11 @@ namespace Avalonia.Controls /// The name scope. /// The name. /// The named element or null if not found. - public static T Find(this INameScope nameScope, string name) + public static T? Find(this INameScope nameScope, string name) where T : class { - Contract.Requires(nameScope != null); - Contract.Requires(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; } /// @@ -41,11 +41,11 @@ namespace Avalonia.Controls /// The control to take the name scope from. /// The name. /// The named element or null if not found. - public static T Find(this ILogical anchor, string name) + public static T? Find(this ILogical anchor, string name) where T : class { - Contract.Requires(anchor != null); - Contract.Requires(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(this INameScope nameScope, string name) where T : class { - Contract.Requires(nameScope != null); - Contract.Requires(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(this ILogical anchor, string name) where T : class { - Contract.Requires(anchor != null); - Contract.Requires(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(name); } - public static INameScope FindNameScope(this ILogical control) + public static INameScope? FindNameScope(this ILogical control) { - Contract.Requires(control != null); + _ = control ?? throw new ArgumentNullException(nameof(control)); var scope = control.GetSelfAndLogicalAncestors() .OfType() diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs index 51f4c5c4eb..f0ce7f8a5b 100644 --- a/src/Avalonia.Styling/Controls/NameScopeLocator.cs +++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs @@ -11,9 +11,9 @@ namespace Avalonia.Controls /// /// The scope relative from which the object should be resolved. /// The name of the object to find. - public static IObservable Track(INameScope scope, string name) + public static IObservable Track(INameScope scope, string name) { - return new NeverEndingSynchronousCompletionAsyncResultObservable(scope.FindAsync(name)); + return new NeverEndingSynchronousCompletionAsyncResultObservable(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 : IObservable { - private T _value; + private T? _value; private SynchronousCompletionAsyncResult? _asyncResult; public NeverEndingSynchronousCompletionAsyncResultObservable(SynchronousCompletionAsyncResult task) @@ -47,7 +47,7 @@ namespace Avalonia.Controls observer.OnNext(_asyncResult.Value.GetResult()); }); else - observer.OnNext(_value); + observer.OnNext(_value!); return Disposable.Empty; } diff --git a/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs b/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs index 7b28d1a911..4cddc8c8fb 100644 --- a/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs +++ b/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls /// True to add the pseudoclass or false to remove. public static void Set(this IPseudoClasses classes, string name, bool value) { - Contract.Requires(classes != null); + _ = classes ?? throw new ArgumentNullException(nameof(classes)); if (value) { diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index e797f8cf8b..3af14daa83 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/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); } diff --git a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs index be1069a4da..513b3f2424 100644 --- a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs +++ b/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(); } diff --git a/src/Avalonia.Styling/INamed.cs b/src/Avalonia.Styling/INamed.cs index 1f8d269b07..df83f3b6ef 100644 --- a/src/Avalonia.Styling/INamed.cs +++ b/src/Avalonia.Styling/INamed.cs @@ -8,6 +8,6 @@ namespace Avalonia /// /// Gets the element name. /// - string Name { get; } + string? Name { get; } } } diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 6d0302ace4..f60ed5aab8 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -9,19 +9,19 @@ namespace Avalonia.LogicalTree /// public static class ControlLocator { - public static IObservable Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null) + public static IObservable Track(ILogical relativeTo, int ancestorLevel, Type? ancestorType = null) { return new ControlTracker(relativeTo, ancestorLevel, ancestorType); } - private class ControlTracker : LightweightObservableBase + private class ControlTracker : LightweightObservableBase { 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 observer, bool first) + protected override void Subscribed(IObserver 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); diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Styling/LogicalTree/ILogical.cs index 9c4223618f..caff3d8150 100644 --- a/src/Avalonia.Styling/LogicalTree/ILogical.cs +++ b/src/Avalonia.Styling/LogicalTree/ILogical.cs @@ -12,12 +12,12 @@ namespace Avalonia.LogicalTree /// /// Raised when the control is attached to a rooted logical tree. /// - event EventHandler AttachedToLogicalTree; + event EventHandler? AttachedToLogicalTree; /// /// Raised when the control is detached from a rooted logical tree. /// - event EventHandler DetachedFromLogicalTree; + event EventHandler? DetachedFromLogicalTree; /// /// Gets a value indicating whether the element is attached to a rooted logical tree. @@ -27,7 +27,7 @@ namespace Avalonia.LogicalTree /// /// Gets the logical parent. /// - ILogical LogicalParent { get; } + ILogical? LogicalParent { get; } /// /// Gets the logical children. diff --git a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs index 458ab0fce2..74720c0a77 100644 --- a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs +++ b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs @@ -15,14 +15,14 @@ namespace Avalonia.LogicalTree /// The logical's ancestors. public static IEnumerable GetLogicalAncestors(this ILogical logical) { - Contract.Requires(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 /// The logical. /// If given logical should be included in search. /// First ancestor of given type. - public static T FindLogicalAncestorOfType(this ILogical logical, bool includeSelf = false) where T : class + public static T? FindLogicalAncestorOfType(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 /// The logical. /// If given logical should be included in search. /// First descendant of given type. - public static T FindLogicalDescendantOfType(this ILogical logical, bool includeSelf = false) where T : class + public static T? FindLogicalDescendantOfType(this ILogical logical, bool includeSelf = false) where T : class { if (logical is null) { @@ -140,7 +140,7 @@ namespace Avalonia.LogicalTree /// /// The logical. /// The parent, or null if the logical is unparented. - public static ILogical GetLogicalParent(this ILogical logical) + public static ILogical? GetLogicalParent(this ILogical logical) { return logical.LogicalParent; } @@ -153,7 +153,7 @@ namespace Avalonia.LogicalTree /// /// The parent, or null if the logical is unparented or its parent is not of type . /// - public static T GetLogicalParent(this ILogical logical) where T : class + public static T? GetLogicalParent(this ILogical logical) where T : class { return logical.LogicalParent as T; } @@ -165,7 +165,7 @@ namespace Avalonia.LogicalTree /// The logical siblings. public static IEnumerable GetLogicalSiblings(this ILogical logical) { - ILogical parent = logical.LogicalParent; + var parent = logical.LogicalParent; if (parent != null) { @@ -187,7 +187,7 @@ namespace Avalonia.LogicalTree /// 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(ILogical logical) where T : class + private static T? FindDescendantOfTypeCore(ILogical logical) where T : class { var logicalChildren = logical.LogicalChildren; var logicalChildrenCount = logicalChildren.Count; diff --git a/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs b/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs index 891724599c..826fc5b2a4 100644 --- a/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs +++ b/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs @@ -19,11 +19,8 @@ namespace Avalonia.LogicalTree ILogical source, ILogical parent) { - Contract.Requires(root != null); - Contract.Requires(source != null); - - Root = root; - Source = source; + Root = root ?? throw new ArgumentNullException(nameof(root)); + Source = source ?? throw new ArgumentNullException(nameof(source)); Parent = parent; } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 2292f5c518..5f498623e1 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/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) { diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs index 803809a8ce..6f54cd5904 100644 --- a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs +++ b/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. diff --git a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs index 7906a29cb5..98d3f16a0a 100644 --- a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs +++ b/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) { diff --git a/src/Avalonia.Styling/Styling/ChildSelector.cs b/src/Avalonia.Styling/Styling/ChildSelector.cs index 85e7aaabde..5c92182b80 100644 --- a/src/Avalonia.Styling/Styling/ChildSelector.cs +++ b/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; /// - 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; } } diff --git a/src/Avalonia.Styling/Styling/OrSelector.cs b/src/Avalonia.Styling/Styling/OrSelector.cs index 8251915504..3d6db9b01e 100644 --- a/src/Avalonia.Styling/Styling/OrSelector.cs +++ b/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; } diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs index ebdfff222a..1cd1a650ef 100644 --- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs +++ b/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; diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index 64d0a0e96b..7c66469cf1 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -25,10 +25,14 @@ namespace Avalonia.Styling /// The previous selector. /// The name of the style class. /// The selector. - public static Selector Class(this Selector previous, string name) + public static Selector Class(this Selector? previous, string name) { - Contract.Requires(name != null); - Contract.Requires(!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 /// /// The previous selector. /// The selector. - 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 /// The previous selector. /// The type. /// The selector. - public static Selector Is(this Selector previous, Type type) + public static Selector Is(this Selector? previous, Type type) { - Contract.Requires(type != null); + _ = type ?? throw new ArgumentNullException(nameof(type)); return TypeNameAndClassSelector.Is(previous, type); } @@ -72,7 +76,7 @@ namespace Avalonia.Styling /// The type. /// The previous selector. /// The selector. - public static Selector Is(this Selector previous) where T : IStyleable + public static Selector Is(this Selector? previous) where T : IStyleable { return previous.Is(typeof(T)); } @@ -83,10 +87,14 @@ namespace Avalonia.Styling /// The previous selector. /// The name. /// The selector. - public static Selector Name(this Selector previous, string name) + public static Selector Name(this Selector? previous, string name) { - Contract.Requires(name != null); - Contract.Requires(!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 /// The previous selector. /// The selector to be not-ed. /// The selector. - public static Selector Not(this Selector previous, Func argument) + public static Selector Not(this Selector? previous, Func argument) { return new NotSelector(previous, argument(null)); } @@ -118,7 +126,7 @@ namespace Avalonia.Styling /// The previous selector. /// The selector to be not-ed. /// The selector. - 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 /// /// /// The selector. - 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 /// /// /// The selector. - 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 /// The previous selector. /// The type. /// The selector. - public static Selector OfType(this Selector previous, Type type) + public static Selector OfType(this Selector? previous, Type type) { - Contract.Requires(type != null); + _ = type ?? throw new ArgumentNullException(nameof(type)); return TypeNameAndClassSelector.OfType(previous, type); } @@ -158,7 +166,7 @@ namespace Avalonia.Styling /// The type. /// The previous selector. /// The selector. - public static Selector OfType(this Selector previous) where T : IStyleable + public static Selector OfType(this Selector? previous) where T : IStyleable { return previous.OfType(typeof(T)); } @@ -191,9 +199,9 @@ namespace Avalonia.Styling /// The property. /// The property value. /// The selector. - public static Selector PropertyEquals(this Selector previous, AvaloniaProperty property, object value) + public static Selector PropertyEquals(this Selector? previous, AvaloniaProperty property, object? value) { - Contract.Requires(property != null); + _ = property ?? throw new ArgumentNullException(nameof(property)); return new PropertyEqualsSelector(previous, property, value); } @@ -205,9 +213,9 @@ namespace Avalonia.Styling /// The property. /// The property value. /// The selector. - public static Selector PropertyEquals(this Selector previous, AvaloniaProperty property, object value) + public static Selector PropertyEquals(this Selector? previous, AvaloniaProperty property, object? value) { - Contract.Requires(property != null); + _ = property ?? throw new ArgumentNullException(nameof(property)); return new PropertyEqualsSelector(previous, property, value); } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index c752bdfeb8..81502f1570 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/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 ToReadOnlyList(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."); diff --git a/src/Avalonia.Styling/Styling/TemplateSelector.cs b/src/Avalonia.Styling/Styling/TemplateSelector.cs index 5ea8defeda..e8051efa6d 100644 --- a/src/Avalonia.Styling/Styling/TemplateSelector.cs +++ b/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; /// - 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; } } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 78c6d9c057..324b253a0f 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/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); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index 1b7ee9025e..5b6522064a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/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); From 1fbcd61e6f83f6d827af66148e8e8a34d06bb818 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Wed, 22 Dec 2021 16:19:10 +0200 Subject: [PATCH 5/5] Fix DoubleTapped on touch (#7213) DoubleTapped now can be triggered by touch Fixed DoubleTapped triggering logic, previously it could be triggered only when you click twice, didn't work when you click 4 times,6, etc. Fixed Tapped to be triggered only once when you double-click, now it triggers Tapped once and DoubleTapped once instead of triggering Tapped two times and DoubleTapped once.Matches UWP behaviour. --- src/Avalonia.Input/Gestures.cs | 18 +- src/Avalonia.Input/PointerEventArgs.cs | 9 +- src/Avalonia.Input/TouchDevice.cs | 44 ++- .../TouchDeviceTests.cs | 272 ++++++++++++++++++ 4 files changed, 323 insertions(+), 20 deletions(-) create mode 100644 tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 639b4ef117..a5ee558f47 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/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 TappedEvent = RoutedEvent.Register( "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)); } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index ba39f7ca8e..8c86cd4637 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/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(); diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index d6ad836f37..0f832d9add 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/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 _pointers = new Dictionary(); 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(); + 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(); } - + } } diff --git a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs new file mode 100644 index 0000000000..6c4416be47 --- /dev/null +++ b/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(); + platformSettingsMock.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().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(); + platformSettingsMock.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().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(); + platformSettingsMock.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(0, 0, 0, 0, 20)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().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(); + tmp.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().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(); + tmp.Setup(x => x.DoubleClickTime).Returns(new TimeSpan(200)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().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)); + } + } +}