diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 431579fdac..9e4dda7256 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)); + } + } +}