diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 565e6afc1a..d15abfbd3f 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -178,6 +178,14 @@ namespace Avalonia.Input nameof(PointerReleased), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + /// + /// Defines the routed event. + /// + internal static readonly RoutedEvent PointerCaptureChangingEvent = + RoutedEvent.Register( + nameof(PointerCaptureChanging), + RoutingStrategies.Direct); + /// /// Defines the routed event. /// @@ -240,6 +248,7 @@ namespace Avalonia.Input PointerMovedEvent.AddClassHandler((x, e) => x.OnPointerMoved(e)); PointerPressedEvent.AddClassHandler((x, e) => x.OnPointerPressed(e)); PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); + PointerCaptureChangingEvent.AddClassHandler((x, e) => x.OnPointerCaptureChanging(e)); PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); @@ -381,9 +390,19 @@ namespace Avalonia.Input remove { RemoveHandler(PointerReleasedEvent, value); } } + /// + /// Occurs when the control or its child control is about to lose capture, + /// event will not be triggered for a parent control if capture was transferred to another child of that parent control. + /// + internal event EventHandler? PointerCaptureChanging + { + add => AddHandler(PointerCaptureChangingEvent, value); + remove => RemoveHandler(PointerCaptureChangingEvent, 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 + /// event will not be triggered for a parent control if capture was transferred to another child of that parent control. /// public event EventHandler? PointerCaptureLost { @@ -770,6 +789,17 @@ namespace Avalonia.Input /// Last focusable element if available/>. protected internal virtual InputElement? GetLastFocusableElementOverride() => null; + /// + /// Invoked when an unhandled reaches an element in its + /// route that is derived from this class. Implement this method to add class handling + /// for this event. + /// + /// Data about the event. + internal virtual void OnPointerCaptureChanging(PointerCaptureChangingEventArgs e) + { + + } + /// /// Invoked when an unhandled reaches an element in its /// route that is derived from this class. Implement this method to add class handling diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index b09f195656..49945f1e8a 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -131,7 +131,7 @@ namespace Avalonia.Input if (source != null) { - _pointer.Capture(source); + _pointer.Capture(source, CaptureSource.Implicit); var settings = ((IInputRoot?)(source as Interactive)?.GetVisualRoot())?.PlatformSettings; if (settings is not null) @@ -206,7 +206,7 @@ namespace Avalonia.Input } finally { - _pointer.Capture(null); + _pointer.Capture(null, CaptureSource.Implicit); _pointer.CaptureGestureRecognizer(null); _pointer.IsGestureRecognitionSkipped = false; _lastMouseDownButton = default; diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs index 1f6741a09e..f243a2e382 100644 --- a/src/Avalonia.Base/Input/Pointer.cs +++ b/src/Avalonia.Base/Input/Pointer.cs @@ -6,6 +6,13 @@ using Avalonia.VisualTree; namespace Avalonia.Input { + internal enum CaptureSource + { + Explicit, + Implicit, + Platform + } + public class Pointer : IPointer, IDisposable { private static int s_NextFreePointerId = 1000; @@ -30,46 +37,60 @@ namespace Avalonia.Input protected virtual void PlatformCapture(IInputElement? element) { - + } internal void PlatformCaptureLost() { if (Captured != null) - Capture(null, platformInitiated: true); + Capture(null, CaptureSource.Platform); } public void Capture(IInputElement? control) { - Capture(control, platformInitiated: false); + Capture(control, CaptureSource.Explicit); } - private void Capture(IInputElement? control, bool platformInitiated) + internal void Capture(IInputElement? control, CaptureSource source) { var oldCapture = Captured; if (oldCapture == control) return; - if (oldCapture is Visual v1) - v1.DetachedFromVisualTree -= OnCaptureDetached; + var oldVisual = oldCapture as Visual; + + IInputElement? commonParent = null; + if (oldVisual != null) + { + commonParent = FindCommonParent(control, oldCapture); + foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType()) + { + if (notifyTarget == commonParent) + break; + var args = new PointerCaptureChangingEventArgs(notifyTarget, this, control, source); + notifyTarget.RaiseEvent(args); + if (args.Handled) + return; + } + } + + if (oldVisual != null) + oldVisual.DetachedFromVisualTree -= OnCaptureDetached; Captured = control; - - if (!platformInitiated) + + if (source != CaptureSource.Platform) PlatformCapture(control); - if (oldCapture is Visual v2) - { - var commonParent = FindCommonParent(control, oldCapture); - foreach (var notifyTarget in v2.GetSelfAndVisualAncestors().OfType()) + if (oldVisual != null) + foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType()) { if (notifyTarget == commonParent) break; notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this)); } - } - if (Captured is Visual v3) - v3.DetachedFromVisualTree += OnCaptureDetached; + if (Captured is Visual newVisual) + newVisual.DetachedFromVisualTree += OnCaptureDetached; if (Captured != null) CaptureGestureRecognizer(null); @@ -92,7 +113,7 @@ namespace Avalonia.Input public IInputElement? Captured { get; private set; } - + public PointerType Type { get; } public bool IsPrimary { get; } diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index bbcc5cccd8..7682b0fb22 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -202,11 +202,26 @@ namespace Avalonia.Input { public IPointer Pointer { get; } - [Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")] + [Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.")] public PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) { Pointer = pointer; Source = source; } } + + internal class PointerCaptureChangingEventArgs : RoutedEventArgs + { + public IPointer Pointer { get; } + public CaptureSource CaptureSource { get; } + public IInputElement? NewValue { get; } + + internal PointerCaptureChangingEventArgs(object source, IPointer pointer, IInputElement? newValue, CaptureSource captureSource) : base(InputElement.PointerCaptureChangingEvent) + { + Pointer = pointer; + Source = source; + NewValue = newValue; + CaptureSource = captureSource; + } + } }