diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index cc9e6b7444..70f26288af 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -252,7 +252,6 @@ namespace Avalonia.Controls if (e.MouseButton == MouseButton.Left) { - e.Device.Capture(this); IsPressed = true; e.Handled = true; @@ -270,7 +269,6 @@ namespace Avalonia.Controls if (IsPressed && e.MouseButton == MouseButton.Left) { - e.Device.Capture(null); IsPressed = false; e.Handled = true; @@ -282,6 +280,11 @@ namespace Avalonia.Controls } } + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + IsPressed = false; + } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) { base.UpdateDataValidation(property, status); diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 30330ef9ac..e7d8018a42 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -64,6 +65,7 @@ namespace Avalonia.Controls.Presenters private Vector _offset; private IDisposable _logicalScrollSubscription; private Size _viewport; + private Dictionary _activeLogicalGestureScrolls; /// /// Initializes static members of the class. @@ -81,6 +83,7 @@ namespace Avalonia.Controls.Presenters public ScrollContentPresenter() { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); + AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } @@ -227,6 +230,72 @@ namespace Avalonia.Controls.Presenters return finalSize; } + // Arbitrary chosen value, probably need to ask ILogicalScrollable + private const int LogicalScrollItemSize = 50; + private void OnScrollGesture(object sender, ScrollGestureEventArgs e) + { + if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width) + { + var scrollable = Child as ILogicalScrollable; + bool isLogical = scrollable?.IsLogicalScrollEnabled == true; + + double x = Offset.X; + double y = Offset.Y; + + Vector delta = default; + if (isLogical) + _activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta); + delta += e.Delta; + + if (Extent.Height > Viewport.Height) + { + double dy; + if (isLogical) + { + var logicalUnits = delta.Y / LogicalScrollItemSize; + delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize); + dy = logicalUnits * scrollable.ScrollSize.Height; + } + else + dy = delta.Y; + + + y += dy; + y = Math.Max(y, 0); + y = Math.Min(y, Extent.Height - Viewport.Height); + } + + if (Extent.Width > Viewport.Width) + { + double dx; + if (isLogical) + { + var logicalUnits = delta.X / LogicalScrollItemSize; + delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize); + dx = logicalUnits * scrollable.ScrollSize.Width; + } + else + dx = delta.X; + x += dx; + x = Math.Max(x, 0); + x = Math.Min(x, Extent.Width - Viewport.Width); + } + + if (isLogical) + { + if (_activeLogicalGestureScrolls == null) + _activeLogicalGestureScrolls = new Dictionary(); + _activeLogicalGestureScrolls[e.Id] = delta; + } + + Offset = new Vector(x, y); + e.Handled = true; + } + } + + private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) + => _activeLogicalGestureScrolls?.Remove(e.Id); + /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index d6537ebbca..88b9a84111 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; @@ -8,6 +9,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -166,10 +168,23 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left) + if (e.MouseButton == MouseButton.Left && e.Pointer.Type == PointerType.Mouse) { e.Handled = UpdateSelectionFromEventSource(e.Source); } } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + if (e.MouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) + { + var container = GetContainerFromEventSource(e.Source); + if (container.GetVisualsAt(e.GetPosition(container)) + .Any(c => container == c || container.IsVisualAncestorOf(c))) + { + e.Handled = UpdateSelectionFromEventSource(e.Source); + } + } + } } } diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs new file mode 100644 index 0000000000..91b224e65a --- /dev/null +++ b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.LogicalTree; +using Avalonia.Styling; + +namespace Avalonia.Input.GestureRecognizers +{ + public class GestureRecognizerCollection : IReadOnlyCollection, IGestureRecognizerActionsDispatcher + { + private readonly IInputElement _inputElement; + private List _recognizers; + private Dictionary _pointerGrabs; + + + public GestureRecognizerCollection(IInputElement inputElement) + { + _inputElement = inputElement; + } + + public void Add(IGestureRecognizer recognizer) + { + if (_recognizers == null) + { + // We initialize the collection when the first recognizer is added + _recognizers = new List(); + _pointerGrabs = new Dictionary(); + } + + _recognizers.Add(recognizer); + recognizer.Initialize(_inputElement, this); + + // Hacks to make bindings work + + if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical) + { + logical.SetParent(logicalParent); + if (recognizer is IStyleable styleableRecognizer + && _inputElement is IStyleable styleableParent) + styleableRecognizer.Bind(StyledElement.TemplatedParentProperty, + styleableParent.GetObservable(StyledElement.TemplatedParentProperty)); + } + } + + static readonly List s_Empty = new List(); + + public IEnumerator GetEnumerator() + => _recognizers?.GetEnumerator() ?? s_Empty.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _recognizers?.Count ?? 0; + + + internal bool HandlePointerPressed(PointerPressedEventArgs e) + { + if (_recognizers == null) + return false; + foreach (var r in _recognizers) + { + if(e.Handled) + break; + r.PointerPressed(e); + } + + return e.Handled; + } + + internal bool HandlePointerReleased(PointerReleasedEventArgs e) + { + if (_recognizers == null) + return false; + if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + { + capture.PointerReleased(e); + } + else + foreach (var r in _recognizers) + { + if (e.Handled) + break; + r.PointerReleased(e); + } + return e.Handled; + } + + internal bool HandlePointerMoved(PointerEventArgs e) + { + if (_recognizers == null) + return false; + if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + { + capture.PointerMoved(e); + } + else + foreach (var r in _recognizers) + { + if (e.Handled) + break; + r.PointerMoved(e); + } + return e.Handled; + } + + internal void HandlePointerCaptureLost(PointerCaptureLostEventArgs e) + { + if (_recognizers == null) + return; + _pointerGrabs.Remove(e.Pointer); + foreach (var r in _recognizers) + { + if(e.Handled) + break; + r.PointerCaptureLost(e); + } + } + + void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) + { + pointer.Capture(_inputElement); + _pointerGrabs[pointer] = recognizer; + } + + } +} diff --git a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs new file mode 100644 index 0000000000..b8ba9e529c --- /dev/null +++ b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Input.GestureRecognizers +{ + public interface IGestureRecognizer + { + void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions); + void PointerPressed(PointerPressedEventArgs e); + void PointerReleased(PointerReleasedEventArgs e); + void PointerMoved(PointerEventArgs e); + void PointerCaptureLost(PointerCaptureLostEventArgs e); + } + + public interface IGestureRecognizerActionsDispatcher + { + void Capture(IPointer pointer, IGestureRecognizer recognizer); + } + + public enum GestureRecognizerResult + { + None, + Capture, + ReleaseCapture + } +} diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs new file mode 100644 index 0000000000..e560230bfe --- /dev/null +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -0,0 +1,129 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input.GestureRecognizers +{ + public class ScrollGestureRecognizer + : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise + IGestureRecognizer + { + private bool _scrolling; + private Point _trackedRootPoint; + private IPointer _tracking; + private IInputElement _target; + private IGestureRecognizerActionsDispatcher _actions; + private bool _canHorizontallyScroll; + private bool _canVerticallyScroll; + private int _gestureId; + + /// + /// Defines the property. + /// + public static readonly DirectProperty CanHorizontallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanHorizontallyScroll), + o => o.CanHorizontallyScroll, + (o, v) => o.CanHorizontallyScroll = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CanVerticallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanVerticallyScroll), + o => o.CanVerticallyScroll, + (o, v) => o.CanVerticallyScroll = v); + + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + public bool CanHorizontallyScroll + { + get => _canHorizontallyScroll; + set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); + } + + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + public bool CanVerticallyScroll + { + get => _canVerticallyScroll; + set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); + } + + + public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) + { + _target = target; + _actions = actions; + } + + public void PointerPressed(PointerPressedEventArgs e) + { + if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch) + { + _tracking = e.Pointer; + _scrolling = false; + _trackedRootPoint = e.GetPosition(null); + } + } + + // Arbitrary chosen value, probably need to move that to platform settings or something + private const double ScrollStartDistance = 30; + public void PointerMoved(PointerEventArgs e) + { + if (e.Pointer == _tracking) + { + var rootPoint = e.GetPosition(null); + if (!_scrolling) + { + if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance) + _scrolling = true; + if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance) + _scrolling = true; + if (_scrolling) + { + _actions.Capture(e.Pointer, this); + _gestureId = ScrollGestureEventArgs.GetNextFreeId(); + } + } + + if (_scrolling) + { + var vector = _trackedRootPoint - rootPoint; + _trackedRootPoint = rootPoint; + _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); + e.Handled = true; + } + } + } + + public void PointerCaptureLost(PointerCaptureLostEventArgs e) + { + if (e.Pointer == _tracking) EndGesture(); + } + + void EndGesture() + { + _tracking = null; + if (_scrolling) + { + _scrolling = false; + _target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); + } + + } + + + public void PointerReleased(PointerReleasedEventArgs e) + { + // TODO: handle inertia + if (e.Pointer == _tracking && _scrolling) + { + e.Handled = true; + EndGesture(); + } + } + } +} diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 23b0ad466e..65195394ab 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -18,6 +18,14 @@ namespace Avalonia.Input RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent ScrollGestureEvent = + RoutedEvent.Register( + "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent ScrollGestureEndedEvent = + RoutedEvent.Register( + "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + private static WeakReference s_lastPress; static Gestures() diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 07e04486ec..7c687f0d7e 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -127,6 +128,14 @@ namespace Avalonia.Input RoutedEvent.Register( "PointerReleased", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent PointerCaptureLostEvent = + RoutedEvent.Register( + "PointerCaptureLost", + RoutingStrategies.Direct); /// /// Defines the event. @@ -148,6 +157,7 @@ namespace Avalonia.Input private bool _isFocused; private bool _isPointerOver; + private GestureRecognizerCollection _gestureRecognizers; /// /// Initializes static members of the class. @@ -166,6 +176,7 @@ namespace Avalonia.Input PointerMovedEvent.AddClassHandler(x => x.OnPointerMoved); PointerPressedEvent.AddClassHandler(x => x.OnPointerPressed); PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased); + PointerCaptureLostEvent.AddClassHandler(x => x.OnPointerCaptureLost); PointerWheelChangedEvent.AddClassHandler(x => x.OnPointerWheelChanged); PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); @@ -263,6 +274,16 @@ namespace Avalonia.Input remove { RemoveHandler(PointerReleasedEvent, value); } } + /// + /// Occurs when the control or its child control loses the pointer capture for any reason, + /// event will not be triggered for a parent control if capture was transferred to another child of that parent control + /// + public event EventHandler PointerCaptureLost + { + add => AddHandler(PointerCaptureLostEvent, value); + remove => RemoveHandler(PointerCaptureLostEvent, value); + } + /// /// Occurs when the mouse wheen is scrolled over the control. /// @@ -370,6 +391,9 @@ namespace Avalonia.Input public List KeyBindings { get; } = new List(); + public GestureRecognizerCollection GestureRecognizers + => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this)); + /// /// Focuses the control. /// @@ -460,6 +484,8 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerMoved(PointerEventArgs e) { + if (_gestureRecognizers?.HandlePointerMoved(e) == true) + e.Handled = true; } /// @@ -468,6 +494,8 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerPressed(PointerPressedEventArgs e) { + if (_gestureRecognizers?.HandlePointerPressed(e) == true) + e.Handled = true; } /// @@ -476,6 +504,17 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerReleased(PointerReleasedEventArgs e) { + if (_gestureRecognizers?.HandlePointerReleased(e) == true) + e.Handled = true; + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _gestureRecognizers?.HandlePointerCaptureLost(e); } /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 90d9c37bd4..05840660e2 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -14,18 +14,14 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// - public class MouseDevice : IMouseDevice, IPointer + public class MouseDevice : IMouseDevice { private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; - private IInputElement _captured; - private IDisposable _capturedSubscription; - PointerType IPointer.Type => PointerType.Mouse; - bool IPointer.IsPrimary => true; - int IPointer.Id { get; } = Pointer.GetNextFreeId(); - + private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + /// /// Gets the control that is currently capturing by the mouse, if any. /// @@ -34,27 +30,9 @@ namespace Avalonia.Input /// within the control's bounds or not. To set the mouse capture, call the /// method. /// - public IInputElement Captured - { - get => _captured; - protected set - { - _capturedSubscription?.Dispose(); - _capturedSubscription = null; - - if (value != null) - { - _capturedSubscription = Observable.FromEventPattern( - x => value.DetachedFromVisualTree += x, - x => value.DetachedFromVisualTree -= x) - .Take(1) - .Subscribe(_ => Captured = null); - } + [Obsolete("Use IPointer instead")] + public IInputElement Captured => _pointer.Captured; - _captured = value; - } - } - /// /// Gets the mouse position, in screen coordinates. /// @@ -75,8 +53,7 @@ namespace Avalonia.Input /// public virtual void Capture(IInputElement control) { - // TODO: Check visibility and enabled state before setting capture. - Captured = control; + _pointer.Capture(control); } /// @@ -110,13 +87,13 @@ namespace Avalonia.Input if (rect.Contains(clientPoint)) { - if (Captured == null) + if (_pointer.Captured == null) { SetPointerOver(this, root, clientPoint, InputModifiers.None); } else { - SetPointerOver(this, root, Captured, InputModifiers.None); + SetPointerOver(this, root, _pointer.Captured, InputModifiers.None); } } } @@ -212,8 +189,8 @@ namespace Avalonia.Input if (hit != null) { - IInteractive source = GetSource(hit); - + _pointer.Capture(hit); + var source = GetSource(hit); if (source != null) { var settings = AvaloniaLocator.Current.GetService(); @@ -229,8 +206,7 @@ namespace Avalonia.Input _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); _lastMouseDownButton = properties.GetObsoleteMouseButton(); - var e = new PointerPressedEventArgs(source, this, root, p, properties, inputModifiers, _clickCount); - + var e = new PointerPressedEventArgs(source, _pointer, root, p, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; } @@ -247,17 +223,17 @@ namespace Avalonia.Input IInputElement source; - if (Captured == null) + if (_pointer.Captured == null) { source = SetPointerOver(this, root, p, inputModifiers); } else { - SetPointerOver(this, root, Captured, inputModifiers); - source = Captured; + SetPointerOver(this, root, _pointer.Captured, inputModifiers); + source = _pointer.Captured; } - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, this, root, + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, p, properties, inputModifiers); source?.RaiseEvent(e); @@ -275,9 +251,10 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerReleasedEventArgs(source, this, root, p, props, inputModifiers, _lastMouseDownButton); + var e = new PointerReleasedEventArgs(source, _pointer, root, p, props, inputModifiers, _lastMouseDownButton); source?.RaiseEvent(e); + _pointer.Capture(null); return e.Handled; } @@ -296,7 +273,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerWheelEventArgs(source, this, root, p, props, inputModifiers, delta); + var e = new PointerWheelEventArgs(source, _pointer, root, p, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -309,7 +286,7 @@ namespace Avalonia.Input { Contract.Requires(hit != null); - return Captured ?? + return _pointer.Captured ?? (hit as IInteractive) ?? hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); } @@ -318,12 +295,12 @@ namespace Avalonia.Input { Contract.Requires(root != null); - return Captured ?? root.InputHitTest(p); + return _pointer.Captured ?? root.InputHitTest(p); } PointerEventArgs CreateSimpleEvent(RoutedEvent ev, IInteractive source, InputModifiers inputModifiers) { - return new PointerEventArgs(ev, source, this, null, default, + return new PointerEventArgs(ev, source, _pointer, null, default, new PointerPointProperties(inputModifiers), inputModifiers); } diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index bdf2501b32..14703986e2 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; +using Avalonia.Interactivity; using Avalonia.VisualTree; namespace Avalonia.Input @@ -9,23 +11,40 @@ namespace Avalonia.Input private static int s_NextFreePointerId = 1000; public static int GetNextFreeId() => s_NextFreePointerId++; - public Pointer(int id, PointerType type, bool isPrimary, IInputElement implicitlyCaptured) + public Pointer(int id, PointerType type, bool isPrimary) { Id = id; Type = type; IsPrimary = isPrimary; - ImplicitlyCaptured = implicitlyCaptured; - if (ImplicitlyCaptured != null) - ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached; } public int Id { get; } + IInputElement FindCommonParent(IInputElement control1, IInputElement control2) + { + if (control1 == null || control2 == null) + return null; + var seen = new HashSet(control1.GetSelfAndVisualAncestors().OfType()); + return control2.GetSelfAndVisualAncestors().OfType().FirstOrDefault(seen.Contains); + } + public void Capture(IInputElement control) { if (Captured != null) Captured.DetachedFromVisualTree -= OnCaptureDetached; + var oldCapture = control; Captured = control; + if (oldCapture != null) + { + var commonParent = FindCommonParent(control, oldCapture); + foreach (var notifyTarget in oldCapture.GetSelfAndVisualAncestors().OfType()) + { + if (notifyTarget == commonParent) + return; + notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this)); + } + } + if (Captured != null) Captured.DetachedFromVisualTree += OnCaptureDetached; } @@ -38,26 +57,11 @@ namespace Avalonia.Input Capture(GetNextCapture(e.Parent)); } - private void OnImplicitCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) - { - ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached; - ImplicitlyCaptured = GetNextCapture(e.Parent); - if (ImplicitlyCaptured != null) - ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached; - } public IInputElement Captured { get; private set; } - public IInputElement ImplicitlyCaptured { get; private set; } - public IInputElement GetEffectiveCapture() => Captured ?? ImplicitlyCaptured; public PointerType Type { get; } public bool IsPrimary { get; } - public void Dispose() - { - if (ImplicitlyCaptured != null) - ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached; - if (Captured != null) - Captured.DetachedFromVisualTree -= OnCaptureDetached; - } + public void Dispose() => Capture(null); } } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 1d07190a81..37d9ade839 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -116,4 +116,15 @@ namespace Avalonia.Input [Obsolete()] public MouseButton MouseButton { get; private set; } } + + public class PointerCaptureLostEventArgs : RoutedEventArgs + { + public IPointer Pointer { get; } + + public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) + { + Pointer = pointer; + Source = source; + } + } } diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs index 7025965f83..3a8d358931 100644 --- a/src/Avalonia.Input/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Input/Properties/AssemblyInfo.cs @@ -5,3 +5,4 @@ using System.Reflection; using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")] diff --git a/src/Avalonia.Input/ScrollGestureEventArgs.cs b/src/Avalonia.Input/ScrollGestureEventArgs.cs new file mode 100644 index 0000000000..a682e8f0a4 --- /dev/null +++ b/src/Avalonia.Input/ScrollGestureEventArgs.cs @@ -0,0 +1,29 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class ScrollGestureEventArgs : RoutedEventArgs + { + public int Id { get; } + public Vector Delta { get; } + private static int _nextId = 1; + + public static int GetNextFreeId() => _nextId++; + + public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) + { + Id = id; + Delta = delta; + } + } + + public class ScrollGestureEndedEventArgs : RoutedEventArgs + { + public int Id { get; } + + public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) + { + Id = id; + } + } +} diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index e9715bd87c..8db2b125a6 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -35,28 +35,30 @@ namespace Avalonia.Input var hit = args.Root.InputHitTest(args.Position); _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), - PointerType.Touch, _pointers.Count == 0, hit); + PointerType.Touch, _pointers.Count == 0); + pointer.Capture(hit); } - var target = pointer.GetEffectiveCapture() ?? args.Root; + var target = pointer.Captured ?? args.Root; if (args.Type == RawPointerEventType.TouchBegin) { - var modifiers = GetModifiers(args.InputModifiers, false); target.RaiseEvent(new PointerPressedEventArgs(target, pointer, - args.Root, args.Position, new PointerPointProperties(modifiers), - modifiers)); + args.Root, args.Position, + new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)), + GetModifiers(args.InputModifiers, false))); } if (args.Type == RawPointerEventType.TouchEnd) { _pointers.Remove(args.TouchPointId); - var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); using (pointer) { target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, - args.Root, args.Position, new PointerPointProperties(modifiers), - modifiers, pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); + args.Root, args.Position, + new PointerPointProperties(GetModifiers(args.InputModifiers, false)), + GetModifiers(args.InputModifiers, pointer.IsPrimary), + pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); } } diff --git a/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml index 63440921d6..3e130cad67 100644 --- a/src/Avalonia.Themes.Default/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/ScrollViewer.xaml @@ -12,7 +12,14 @@ Extent="{TemplateBinding Extent, Mode=TwoWay}" Margin="{TemplateBinding Padding}" Offset="{TemplateBinding Offset, Mode=TwoWay}" - Viewport="{TemplateBinding Viewport, Mode=TwoWay}"/> + Viewport="{TemplateBinding Viewport, Mode=TwoWay}"> + + + + - \ No newline at end of file +