From c8f029386d88a62a9403c98adb52e519a5529ac3 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 14:01:35 +0300 Subject: [PATCH 01/42] Map devices, add some native api --- src/Avalonia.Input/IPenDevice.cs | 10 + src/Avalonia.Input/ITouchPadDevice.cs | 10 + src/Avalonia.Input/PenDevice.cs | 488 ++++++++++++++++++ src/Avalonia.Input/TouchPadDevice.cs | 488 ++++++++++++++++++ .../Interop/UnmanagedMethods.cs | 64 +++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 155 ++++++ 6 files changed, 1215 insertions(+) create mode 100644 src/Avalonia.Input/IPenDevice.cs create mode 100644 src/Avalonia.Input/ITouchPadDevice.cs create mode 100644 src/Avalonia.Input/PenDevice.cs create mode 100644 src/Avalonia.Input/TouchPadDevice.cs diff --git a/src/Avalonia.Input/IPenDevice.cs b/src/Avalonia.Input/IPenDevice.cs new file mode 100644 index 0000000000..1cc0fcf76d --- /dev/null +++ b/src/Avalonia.Input/IPenDevice.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Input +{ + /// + /// Represents a pen/stylus device. + /// + public interface IPenDevice : IPointerDevice + { + + } +} diff --git a/src/Avalonia.Input/ITouchPadDevice.cs b/src/Avalonia.Input/ITouchPadDevice.cs new file mode 100644 index 0000000000..ea6c57f948 --- /dev/null +++ b/src/Avalonia.Input/ITouchPadDevice.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Input +{ + /// + /// Represents a touch pad device. + /// + public interface ITouchPadDevice : IPointerDevice + { + + } +} diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs new file mode 100644 index 0000000000..dd3d60ebb9 --- /dev/null +++ b/src/Avalonia.Input/PenDevice.cs @@ -0,0 +1,488 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + /// + /// Represents a pen/stylus device. + /// + public class PenDevice : IPenDevice, IDisposable + { + private int _clickCount; + private Rect _lastClickRect; + private ulong _lastClickTime; + + private readonly Pointer _pointer; + private bool _disposed; + private PixelPoint? _position; + + public PenDevice(Pointer? pointer = null) + { + _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + } + + /// + /// Gets the control that is currently capturing by the mouse, if any. + /// + /// + /// When an element captures the mouse, it receives mouse input whether the cursor is + /// within the control's bounds or not. To set the mouse capture, call the + /// method. + /// + [Obsolete("Use IPointer instead")] + public IInputElement? Captured => _pointer.Captured; + + /// + /// Gets the mouse position, in screen coordinates. + /// + [Obsolete("Use events instead")] + public PixelPoint Position + { + get => _position ?? new PixelPoint(-1, -1); + protected set => _position = value; + } + + /// + /// Captures mouse input to the specified control. + /// + /// The control. + /// + /// When an element captures the mouse, it receives mouse input whether the cursor is + /// within the control's bounds or not. The current mouse capture control is exposed + /// by the property. + /// + public void Capture(IInputElement? control) + { + _pointer.Capture(control); + } + + /// + /// Gets the mouse position relative to a control. + /// + /// The control. + /// The mouse position in the control's coordinates. + public Point GetPosition(IVisual relativeTo) + { + relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); + + if (relativeTo.VisualRoot == null) + { + throw new InvalidOperationException("Control is not attached to visual tree."); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var rootPoint = relativeTo.VisualRoot.PointToClient(Position); +#pragma warning restore CS0618 // Type or member is obsolete + var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); + return rootPoint * transform!.Value; + } + + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawPointerEventArgs margs) + ProcessRawEvent(margs); + } + + public void TopLevelClosed(IInputRoot root) + { + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + } + + public void SceneInvalidated(IInputRoot root, Rect rect) + { + // Pointer is outside of the target area + if (_position == null ) + { + if (root.PointerOverElement != null) + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + return; + } + + + var clientPoint = root.PointToClient(_position.Value); + + if (rect.Contains(clientPoint)) + { + if (_pointer.Captured == null) + { + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, + PointerPointProperties.None, KeyModifiers.None); + } + else + { + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, + PointerPointProperties.None, KeyModifiers.None); + } + } + } + + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + if (props.IsXButton1Pressed) + rv++; + if (props.IsXButton2Pressed) + rv++; + return rv; + } + + private void ProcessRawEvent(RawPointerEventArgs e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + var mouse = (PenDevice)e.Device; + if(mouse._disposed) + return; + + _position = e.Root.PointToScreen(e.Position); + var props = CreateProperties(e); + var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); + switch (e.Type) + { + case RawPointerEventType.LeaveWindow: + LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); + break; + case RawPointerEventType.LeftButtonDown: + case RawPointerEventType.RightButtonDown: + case RawPointerEventType.MiddleButtonDown: + case RawPointerEventType.XButton1Down: + case RawPointerEventType.XButton2Down: + if (ButtonCount(props) > 1) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + else + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, + props, keyModifiers); + break; + case RawPointerEventType.LeftButtonUp: + case RawPointerEventType.RightButtonUp: + case RawPointerEventType.MiddleButtonUp: + case RawPointerEventType.XButton1Up: + case RawPointerEventType.XButton2Up: + if (ButtonCount(props) != 0) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + else + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + break; + case RawPointerEventType.Move: + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + break; + case RawPointerEventType.Wheel: + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); + break; + } + } + + private void LeaveWindow(IPenDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + _position = null; + ClearPointerOver(this, timestamp, root, properties, inputModifiers); + } + + + PointerPointProperties CreateProperties(RawPointerEventArgs args) + { + + var kind = PointerUpdateKind.Other; + + if (args.Type == RawPointerEventType.LeftButtonDown) + kind = PointerUpdateKind.LeftButtonPressed; + if (args.Type == RawPointerEventType.MiddleButtonDown) + kind = PointerUpdateKind.MiddleButtonPressed; + if (args.Type == RawPointerEventType.RightButtonDown) + kind = PointerUpdateKind.RightButtonPressed; + if (args.Type == RawPointerEventType.XButton1Down) + kind = PointerUpdateKind.XButton1Pressed; + if (args.Type == RawPointerEventType.XButton2Down) + kind = PointerUpdateKind.XButton2Pressed; + if (args.Type == RawPointerEventType.LeftButtonUp) + kind = PointerUpdateKind.LeftButtonReleased; + if (args.Type == RawPointerEventType.MiddleButtonUp) + kind = PointerUpdateKind.MiddleButtonReleased; + if (args.Type == RawPointerEventType.RightButtonUp) + kind = PointerUpdateKind.RightButtonReleased; + if (args.Type == RawPointerEventType.XButton1Up) + kind = PointerUpdateKind.XButton1Released; + if (args.Type == RawPointerEventType.XButton2Up) + kind = PointerUpdateKind.XButton2Released; + + return new PointerPointProperties(args.InputModifiers, kind); + } + + private MouseButton _lastMouseDownButton; + private bool MouseDown(IPenDevice device, ulong timestamp, IInputElement root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + _pointer.Capture(hit); + var source = GetSource(hit); + if (source != null) + { + var settings = AvaloniaLocator.Current.GetService(); + var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; + var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); + + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; + } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); + source.RaiseEvent(e); + return e.Handled; + } + } + + return false; + } + + private bool MouseMove(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + IInputElement? source; + + if (_pointer.Captured == null) + { + source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); + } + else + { + SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); + source = _pointer.Captured; + } + + if (source is object) + { + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers); + + source.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool MouseUp(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + var source = GetSource(hit); + + if (source is not null) + { + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + _lastMouseDownButton); + + source?.RaiseEvent(e); + _pointer.Capture(null); + return e.Handled; + } + + return false; + } + + private bool MouseWheel(IPenDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + var source = GetSource(hit); + + if (source is not null) + { + var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private IInteractive? GetSource(IVisual? hit) + { + if (hit is null) + return null; + + return _pointer.Captured ?? + (hit as IInteractive) ?? + hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + } + + private IInputElement? HitTest(IInputElement root, Point p) + { + root = root ?? throw new ArgumentNullException(nameof(root)); + + return _pointer.Captured ?? root.InputHitTest(p); + } + + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + return new PointerEventArgs(ev, source, _pointer, null, default, + timestamp, properties, inputModifiers); + } + + private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var element = root.PointerOverElement; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); + + if (element!=null && !element.IsAttachedToVisualTree) + { + // element has been removed from visual tree so do top down cleanup + if (root.IsPointerOver) + ClearChildrenPointerOver(e, root,true); + } + while (element != null) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + element = (IInputElement?)element.VisualParent; + } + + root.PointerOverElement = null; + } + + private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsPointerOver) + { + ClearChildrenPointerOver(e, el, true); + break; + } + } + if(clearRoot) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + } + } + + private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var element = root.InputHitTest(p); + + if (element != root.PointerOverElement) + { + if (element != null) + { + SetPointerOver(device, timestamp, root, element, properties, inputModifiers); + } + else + { + ClearPointerOver(device, timestamp, root, properties, inputModifiers); + } + } + + return element; + } + + private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + element = element ?? throw new ArgumentNullException(nameof(element)); + + IInputElement? branch = null; + + IInputElement? el = element; + + while (el != null) + { + if (el.IsPointerOver) + { + branch = el; + break; + } + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement; + + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); + if (el!=null && branch!=null && !el.IsAttachedToVisualTree) + { + ClearChildrenPointerOver(e,branch,false); + } + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement = element; + e.RoutedEvent = InputElement.PointerEnterEvent; + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + } + + public void Dispose() + { + _disposed = true; + _pointer?.Dispose(); + } + } +} diff --git a/src/Avalonia.Input/TouchPadDevice.cs b/src/Avalonia.Input/TouchPadDevice.cs new file mode 100644 index 0000000000..fcd254f588 --- /dev/null +++ b/src/Avalonia.Input/TouchPadDevice.cs @@ -0,0 +1,488 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + /// + /// Represents a touch pad device. + /// + public class TouchPadDevice : ITouchPadDevice, IDisposable + { + private int _clickCount; + private Rect _lastClickRect; + private ulong _lastClickTime; + + private readonly Pointer _pointer; + private bool _disposed; + private PixelPoint? _position; + + public TouchPadDevice(Pointer? pointer = null) + { + _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + } + + /// + /// Gets the control that is currently capturing by the mouse, if any. + /// + /// + /// When an element captures the mouse, it receives mouse input whether the cursor is + /// within the control's bounds or not. To set the mouse capture, call the + /// method. + /// + [Obsolete("Use IPointer instead")] + public IInputElement? Captured => _pointer.Captured; + + /// + /// Gets the mouse position, in screen coordinates. + /// + [Obsolete("Use events instead")] + public PixelPoint Position + { + get => _position ?? new PixelPoint(-1, -1); + protected set => _position = value; + } + + /// + /// Captures mouse input to the specified control. + /// + /// The control. + /// + /// When an element captures the mouse, it receives mouse input whether the cursor is + /// within the control's bounds or not. The current mouse capture control is exposed + /// by the property. + /// + public void Capture(IInputElement? control) + { + _pointer.Capture(control); + } + + /// + /// Gets the mouse position relative to a control. + /// + /// The control. + /// The mouse position in the control's coordinates. + public Point GetPosition(IVisual relativeTo) + { + relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); + + if (relativeTo.VisualRoot == null) + { + throw new InvalidOperationException("Control is not attached to visual tree."); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var rootPoint = relativeTo.VisualRoot.PointToClient(Position); +#pragma warning restore CS0618 // Type or member is obsolete + var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); + return rootPoint * transform!.Value; + } + + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawPointerEventArgs margs) + ProcessRawEvent(margs); + } + + public void TopLevelClosed(IInputRoot root) + { + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + } + + public void SceneInvalidated(IInputRoot root, Rect rect) + { + // Pointer is outside of the target area + if (_position == null ) + { + if (root.PointerOverElement != null) + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + return; + } + + + var clientPoint = root.PointToClient(_position.Value); + + if (rect.Contains(clientPoint)) + { + if (_pointer.Captured == null) + { + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, + PointerPointProperties.None, KeyModifiers.None); + } + else + { + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, + PointerPointProperties.None, KeyModifiers.None); + } + } + } + + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + if (props.IsXButton1Pressed) + rv++; + if (props.IsXButton2Pressed) + rv++; + return rv; + } + + private void ProcessRawEvent(RawPointerEventArgs e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + var mouse = (TouchPadDevice)e.Device; + if(mouse._disposed) + return; + + _position = e.Root.PointToScreen(e.Position); + var props = CreateProperties(e); + var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); + switch (e.Type) + { + case RawPointerEventType.LeaveWindow: + LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); + break; + case RawPointerEventType.LeftButtonDown: + case RawPointerEventType.RightButtonDown: + case RawPointerEventType.MiddleButtonDown: + case RawPointerEventType.XButton1Down: + case RawPointerEventType.XButton2Down: + if (ButtonCount(props) > 1) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + else + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, + props, keyModifiers); + break; + case RawPointerEventType.LeftButtonUp: + case RawPointerEventType.RightButtonUp: + case RawPointerEventType.MiddleButtonUp: + case RawPointerEventType.XButton1Up: + case RawPointerEventType.XButton2Up: + if (ButtonCount(props) != 0) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + else + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + break; + case RawPointerEventType.Move: + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + break; + case RawPointerEventType.Wheel: + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); + break; + } + } + + private void LeaveWindow(ITouchPadDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + _position = null; + ClearPointerOver(this, timestamp, root, properties, inputModifiers); + } + + + PointerPointProperties CreateProperties(RawPointerEventArgs args) + { + + var kind = PointerUpdateKind.Other; + + if (args.Type == RawPointerEventType.LeftButtonDown) + kind = PointerUpdateKind.LeftButtonPressed; + if (args.Type == RawPointerEventType.MiddleButtonDown) + kind = PointerUpdateKind.MiddleButtonPressed; + if (args.Type == RawPointerEventType.RightButtonDown) + kind = PointerUpdateKind.RightButtonPressed; + if (args.Type == RawPointerEventType.XButton1Down) + kind = PointerUpdateKind.XButton1Pressed; + if (args.Type == RawPointerEventType.XButton2Down) + kind = PointerUpdateKind.XButton2Pressed; + if (args.Type == RawPointerEventType.LeftButtonUp) + kind = PointerUpdateKind.LeftButtonReleased; + if (args.Type == RawPointerEventType.MiddleButtonUp) + kind = PointerUpdateKind.MiddleButtonReleased; + if (args.Type == RawPointerEventType.RightButtonUp) + kind = PointerUpdateKind.RightButtonReleased; + if (args.Type == RawPointerEventType.XButton1Up) + kind = PointerUpdateKind.XButton1Released; + if (args.Type == RawPointerEventType.XButton2Up) + kind = PointerUpdateKind.XButton2Released; + + return new PointerPointProperties(args.InputModifiers, kind); + } + + private MouseButton _lastMouseDownButton; + private bool MouseDown(ITouchPadDevice device, ulong timestamp, IInputElement root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + _pointer.Capture(hit); + var source = GetSource(hit); + if (source != null) + { + var settings = AvaloniaLocator.Current.GetService(); + var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; + var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); + + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; + } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); + source.RaiseEvent(e); + return e.Handled; + } + } + + return false; + } + + private bool MouseMove(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + IInputElement? source; + + if (_pointer.Captured == null) + { + source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); + } + else + { + SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); + source = _pointer.Captured; + } + + if (source is object) + { + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers); + + source.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool MouseUp(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + var source = GetSource(hit); + + if (source is not null) + { + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + _lastMouseDownButton); + + source?.RaiseEvent(e); + _pointer.Capture(null); + return e.Handled; + } + + return false; + } + + private bool MouseWheel(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + var source = GetSource(hit); + + if (source is not null) + { + var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private IInteractive? GetSource(IVisual? hit) + { + if (hit is null) + return null; + + return _pointer.Captured ?? + (hit as IInteractive) ?? + hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + } + + private IInputElement? HitTest(IInputElement root, Point p) + { + root = root ?? throw new ArgumentNullException(nameof(root)); + + return _pointer.Captured ?? root.InputHitTest(p); + } + + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + return new PointerEventArgs(ev, source, _pointer, null, default, + timestamp, properties, inputModifiers); + } + + private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var element = root.PointerOverElement; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); + + if (element!=null && !element.IsAttachedToVisualTree) + { + // element has been removed from visual tree so do top down cleanup + if (root.IsPointerOver) + ClearChildrenPointerOver(e, root,true); + } + while (element != null) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + element = (IInputElement?)element.VisualParent; + } + + root.PointerOverElement = null; + } + + private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsPointerOver) + { + ClearChildrenPointerOver(e, el, true); + break; + } + } + if(clearRoot) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + } + } + + private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var element = root.InputHitTest(p); + + if (element != root.PointerOverElement) + { + if (element != null) + { + SetPointerOver(device, timestamp, root, element, properties, inputModifiers); + } + else + { + ClearPointerOver(device, timestamp, root, properties, inputModifiers); + } + } + + return element; + } + + private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, + PointerPointProperties properties, + KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + element = element ?? throw new ArgumentNullException(nameof(element)); + + IInputElement? branch = null; + + IInputElement? el = element; + + while (el != null) + { + if (el.IsPointerOver) + { + branch = el; + break; + } + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement; + + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); + if (el!=null && branch!=null && !el.IsAttachedToVisualTree) + { + ClearChildrenPointerOver(e,branch,false); + } + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement = element; + e.RoutedEvent = InputElement.PointerEnterEvent; + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + } + + public void Dispose() + { + _disposed = true; + _pointer?.Dispose(); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index c74c5fbc01..db81e8197f 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -514,6 +514,33 @@ namespace Avalonia.Win32.Interop CS_DROPSHADOW = 0x00020000 } + [Flags] + public enum PointerDeviceChangeFlags + { + PDC_ARRIVAL = 0x001, + PDC_REMOVAL = 0x002, + PDC_ORIENTATION_0 = 0x004, + PDC_ORIENTATION_90 = 0x008, + PDC_ORIENTATION_180 = 0x010, + PDC_ORIENTATION_270 = 0x020, + PDC_MODE_DEFAULT = 0x040, + PDC_MODE_CENTERED = 0x080, + PDC_MAPPING_CHANGE = 0x100, + PDC_RESOLUTION = 0x200, + PDC_ORIGIN = 0x400, + PDC_MODE_ASPECTRATIOPRESERVED = 0x800 + } + + public enum InputType + { + NONE = 0x00000000, + POINTER = 0x00000001, + TOUCH = 0x00000002, + PEN = 0x00000003, + MOUSE = 0x00000004, + TOUCHPAD = 0x00000005 + } + public enum WindowsMessage : uint { WM_NULL = 0x0000, @@ -689,6 +716,25 @@ namespace Avalonia.Win32.Interop WM_EXITSIZEMOVE = 0x0232, WM_DROPFILES = 0x0233, WM_MDIREFRESHMENU = 0x0234, + + WM_POINTERDEVICECHANGE = 0x0238, + WM_POINTERDEVICEINRANGE = 0x239, + WM_POINTERDEVICEOUTOFRANGE = 0x23A, + WM_NCPOINTERUPDATE = 0x0241, + WM_NCPOINTERDOWN = 0x0242, + WM_NCPOINTERUP = 0x0243, + WM_POINTERUPDATE = 0x0245, + WM_POINTERDOWN = 0x0246, + WM_POINTERUP = 0x0247, + WM_POINTERENTER = 0x0249, + WM_POINTERLEAVE = 0x024A, + WM_POINTERACTIVATE = 0x024B, + WM_POINTERCAPTURECHANGED = 0x024C, + WM_TOUCHHITTESTING = 0x024D, + WM_POINTERWHEEL = 0x024E, + WM_POINTERHWHEEL = 0x024F, + WM_POINTERHITTEST = 0x0250, + WM_IME_SETCONTEXT = 0x0281, WM_IME_NOTIFY = 0x0282, WM_IME_CONTROL = 0x0283, @@ -903,6 +949,24 @@ namespace Avalonia.Win32.Interop public const int SizeOf_BITMAPINFOHEADER = 40; + [DllImport("user32.dll", SetLastError = true)] + public static extern int EnableMouseInPointer(bool enable); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerCursorId(uint pointerID, out uint cursorId); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerType(uint pointerID, out InputType pointerType); + + [DllImport("user32.dll", SetLastError = true)] + public static extern void GetUnpredictedMessagePos(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool IsMouseInPointerEnabled(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SkipPointerFrameMessages(uint pointerID); + [DllImport("user32.dll")] public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumDelegate lpfnEnum, IntPtr dwData); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 88a0744e3e..e16901af2c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -166,6 +166,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN: { + if (BelowWin8) + { + break; + } shouldTakeFocus = ShouldTakeFocusOnClick; if (ShouldIgnoreTouchEmulatedMessage()) { @@ -195,6 +199,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { + if (BelowWin8) + { + break; + } if (ShouldIgnoreTouchEmulatedMessage()) { break; @@ -219,11 +227,19 @@ namespace Avalonia.Win32 } // Mouse capture is lost case WindowsMessage.WM_CANCELMODE: + if (BelowWin8) + { + break; + } _mouseDevice.Capture(null); break; case WindowsMessage.WM_MOUSEMOVE: { + if (BelowWin8) + { + break; + } if (ShouldIgnoreTouchEmulatedMessage()) { break; @@ -254,6 +270,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEWHEEL: { + if (BelowWin8) + { + break; + } e = new RawMouseWheelEventArgs( _mouseDevice, timestamp, @@ -265,6 +285,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEHWHEEL: { + if (BelowWin8) + { + break; + } e = new RawMouseWheelEventArgs( _mouseDevice, timestamp, @@ -276,6 +300,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSELEAVE: { + if (BelowWin8) + { + break; + } _trackingMouse = false; e = new RawPointerEventArgs( _mouseDevice, @@ -291,6 +319,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN: { + if (BelowWin8) + { + break; + } e = new RawPointerEventArgs( _mouseDevice, timestamp, @@ -311,6 +343,10 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_TOUCH: { + if (BelowWin8) + { + break; + } var touchInputCount = wParam.ToInt32(); var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount]; @@ -338,6 +374,117 @@ namespace Avalonia.Win32 break; } + + + + + + + + + + + + case WindowsMessage.WM_POINTERDEVICECHANGE: + case WindowsMessage.WM_POINTERDEVICEINRANGE: + case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: + { + + break; + } + case WindowsMessage.WM_NCPOINTERUPDATE: + case WindowsMessage.WM_NCPOINTERDOWN: + case WindowsMessage.WM_NCPOINTERUP: + { + + break; + } + case WindowsMessage.WM_POINTERUPDATE: + case WindowsMessage.WM_POINTERDOWN: + case WindowsMessage.WM_POINTERUP: + { + + break; + } + case WindowsMessage.WM_POINTERENTER: + case WindowsMessage.WM_POINTERLEAVE: + { + + break; + } + case WindowsMessage.WM_POINTERACTIVATE: + case WindowsMessage.WM_POINTERCAPTURECHANGED: + { + + break; + } + case WindowsMessage.WM_TOUCHHITTESTING: + { + + break; + } + case WindowsMessage.WM_POINTERWHEEL: + { + var pointerId = ToPointerId(wParam); + GetPointerType(pointerId, out var type); + IInputDevice device = _mouseDevice; + switch (type) + { + case InputType.PEN: + + break; + case InputType.TOUCH: + device = _touchDevice; + break; + case InputType.TOUCHPAD: + + break; + } + + + var delta = GetWheelDelta(wParam); + var point = PointFromLParam(lParam); + e = new RawMouseWheelEventArgs( + _mouseDevice, + timestamp, + _owner, + PointToClient(point), + new Vector(0, delta / wheelDelta), + GetMouseModifiers(wParam)); + break; + } + case WindowsMessage.WM_POINTERHWHEEL: + { + + break; + } + case WindowsMessage.WM_POINTERHITTEST: + { + + break; + } + + + + + + + + + + + + + + + + + + + + + + case WindowsMessage.WM_NCPAINT: { if (!HasFullDecorations) @@ -540,6 +687,14 @@ namespace Avalonia.Win32 } } + public static uint GetWheelDelta(IntPtr wParam) => HIWORD(wParam); + public static uint ToPointerId(IntPtr wParam) => LOWORD(wParam); + public static uint LOWORD(IntPtr param) => (uint)param & 0xffff; + public static uint HIWORD(IntPtr param) => (uint)param >> 16; + + + public bool BelowWin8 => Win32Platform.WindowsVersion < PlatformConstants.Windows8; + private void UpdateInputMethod(IntPtr hkl) { // note: for non-ime language, also create it so that emoji panel tracks cursor From 44fda62a9b93cb0f3f6c0096cbfc654f378ab919 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 16:56:47 +0300 Subject: [PATCH 02/42] Add PointerInfo, TouchInfo and PenInfo --- src/Avalonia.Input/ITouchPadDevice.cs | 10 - src/Avalonia.Input/TouchPadDevice.cs | 488 ------------------ .../Interop/UnmanagedMethods.cs | 166 +++++- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 14 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 + 5 files changed, 159 insertions(+), 521 deletions(-) delete mode 100644 src/Avalonia.Input/ITouchPadDevice.cs delete mode 100644 src/Avalonia.Input/TouchPadDevice.cs diff --git a/src/Avalonia.Input/ITouchPadDevice.cs b/src/Avalonia.Input/ITouchPadDevice.cs deleted file mode 100644 index ea6c57f948..0000000000 --- a/src/Avalonia.Input/ITouchPadDevice.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Avalonia.Input -{ - /// - /// Represents a touch pad device. - /// - public interface ITouchPadDevice : IPointerDevice - { - - } -} diff --git a/src/Avalonia.Input/TouchPadDevice.cs b/src/Avalonia.Input/TouchPadDevice.cs deleted file mode 100644 index fcd254f588..0000000000 --- a/src/Avalonia.Input/TouchPadDevice.cs +++ /dev/null @@ -1,488 +0,0 @@ -using System; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Input.Raw; -using Avalonia.Interactivity; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - /// - /// Represents a touch pad device. - /// - public class TouchPadDevice : ITouchPadDevice, IDisposable - { - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - - private readonly Pointer _pointer; - private bool _disposed; - private PixelPoint? _position; - - public TouchPadDevice(Pointer? pointer = null) - { - _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); - } - - /// - /// Gets the control that is currently capturing by the mouse, if any. - /// - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. To set the mouse capture, call the - /// method. - /// - [Obsolete("Use IPointer instead")] - public IInputElement? Captured => _pointer.Captured; - - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use events instead")] - public PixelPoint Position - { - get => _position ?? new PixelPoint(-1, -1); - protected set => _position = value; - } - - /// - /// Captures mouse input to the specified control. - /// - /// The control. - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. The current mouse capture control is exposed - /// by the property. - /// - public void Capture(IInputElement? control) - { - _pointer.Capture(control); - } - - /// - /// Gets the mouse position relative to a control. - /// - /// The control. - /// The mouse position in the control's coordinates. - public Point GetPosition(IVisual relativeTo) - { - relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); - - if (relativeTo.VisualRoot == null) - { - throw new InvalidOperationException("Control is not attached to visual tree."); - } - -#pragma warning disable CS0618 // Type or member is obsolete - var rootPoint = relativeTo.VisualRoot.PointToClient(Position); -#pragma warning restore CS0618 // Type or member is obsolete - var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform!.Value; - } - - public void ProcessRawEvent(RawInputEventArgs e) - { - if (!e.Handled && e is RawPointerEventArgs margs) - ProcessRawEvent(margs); - } - - public void TopLevelClosed(IInputRoot root) - { - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - } - - public void SceneInvalidated(IInputRoot root, Rect rect) - { - // Pointer is outside of the target area - if (_position == null ) - { - if (root.PointerOverElement != null) - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - return; - } - - - var clientPoint = root.PointToClient(_position.Value); - - if (rect.Contains(clientPoint)) - { - if (_pointer.Captured == null) - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, - PointerPointProperties.None, KeyModifiers.None); - } - else - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, - PointerPointProperties.None, KeyModifiers.None); - } - } - } - - int ButtonCount(PointerPointProperties props) - { - var rv = 0; - if (props.IsLeftButtonPressed) - rv++; - if (props.IsMiddleButtonPressed) - rv++; - if (props.IsRightButtonPressed) - rv++; - if (props.IsXButton1Pressed) - rv++; - if (props.IsXButton2Pressed) - rv++; - return rv; - } - - private void ProcessRawEvent(RawPointerEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - - var mouse = (TouchPadDevice)e.Device; - if(mouse._disposed) - return; - - _position = e.Root.PointToScreen(e.Position); - var props = CreateProperties(e); - var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); - switch (e.Type) - { - case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); - break; - case RawPointerEventType.LeftButtonDown: - case RawPointerEventType.RightButtonDown: - case RawPointerEventType.MiddleButtonDown: - case RawPointerEventType.XButton1Down: - case RawPointerEventType.XButton2Down: - if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - else - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, keyModifiers); - break; - case RawPointerEventType.LeftButtonUp: - case RawPointerEventType.RightButtonUp: - case RawPointerEventType.MiddleButtonUp: - case RawPointerEventType.XButton1Up: - case RawPointerEventType.XButton2Up: - if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - break; - case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - break; - case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); - break; - } - } - - private void LeaveWindow(ITouchPadDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - _position = null; - ClearPointerOver(this, timestamp, root, properties, inputModifiers); - } - - - PointerPointProperties CreateProperties(RawPointerEventArgs args) - { - - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; - - return new PointerPointProperties(args.InputModifiers, kind); - } - - private MouseButton _lastMouseDownButton; - private bool MouseDown(ITouchPadDevice device, ulong timestamp, IInputElement root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - _pointer.Capture(hit); - var source = GetSource(hit); - if (source != null) - { - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; - var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) - { - _clickCount = 0; - } - - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); - _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); - source.RaiseEvent(e); - return e.Handled; - } - } - - return false; - } - - private bool MouseMove(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - IInputElement? source; - - if (_pointer.Captured == null) - { - source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); - } - else - { - SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); - source = _pointer.Captured; - } - - if (source is object) - { - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers); - - source.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool MouseUp(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, - _lastMouseDownButton); - - source?.RaiseEvent(e); - _pointer.Capture(null); - return e.Handled; - } - - return false; - } - - private bool MouseWheel(ITouchPadDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private IInteractive? GetSource(IVisual? hit) - { - if (hit is null) - return null; - - return _pointer.Captured ?? - (hit as IInteractive) ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } - - private IInputElement? HitTest(IInputElement root, Point p) - { - root = root ?? throw new ArgumentNullException(nameof(root)); - - return _pointer.Captured ?? root.InputHitTest(p); - } - - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, properties, inputModifiers); - } - - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); - - if (element!=null && !element.IsAttachedToVisualTree) - { - // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) - ClearChildrenPointerOver(e, root,true); - } - while (element != null) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - element = (IInputElement?)element.VisualParent; - } - - root.PointerOverElement = null; - } - - private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) - { - foreach (IInputElement el in element.VisualChildren) - { - if (el.IsPointerOver) - { - ClearChildrenPointerOver(e, el, true); - break; - } - } - if(clearRoot) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - } - } - - private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.InputHitTest(p); - - if (element != root.PointerOverElement) - { - if (element != null) - { - SetPointerOver(device, timestamp, root, element, properties, inputModifiers); - } - else - { - ClearPointerOver(device, timestamp, root, properties, inputModifiers); - } - } - - return element; - } - - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - element = element ?? throw new ArgumentNullException(nameof(element)); - - IInputElement? branch = null; - - IInputElement? el = element; - - while (el != null) - { - if (el.IsPointerOver) - { - branch = el; - break; - } - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement; - - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); - if (el!=null && branch!=null && !el.IsAttachedToVisualTree) - { - ClearChildrenPointerOver(e,branch,false); - } - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement = element; - e.RoutedEvent = InputElement.PointerEnterEvent; - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - } - - public void Dispose() - { - _disposed = true; - _pointer?.Dispose(); - } - } -} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index db81e8197f..5aecd8e8f0 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -225,20 +225,17 @@ namespace Avalonia.Win32.Interop [Flags] public enum ModifierKeys { - MK_CONTROL = 0x0008, + MK_NONE = 0x0001, MK_LBUTTON = 0x0001, - - MK_MBUTTON = 0x0010, - MK_RBUTTON = 0x0002, - MK_SHIFT = 0x0004, - - MK_ALT = 0x0020, + MK_SHIFT = 0x0004, + MK_CONTROL = 0x0008, + MK_MBUTTON = 0x0010, + MK_ALT = 0x0020, MK_XBUTTON1 = 0x0020, - MK_XBUTTON2 = 0x0040 } @@ -531,14 +528,14 @@ namespace Avalonia.Win32.Interop PDC_MODE_ASPECTRATIOPRESERVED = 0x800 } - public enum InputType + public enum PointerInputType { - NONE = 0x00000000, - POINTER = 0x00000001, - TOUCH = 0x00000002, - PEN = 0x00000003, - MOUSE = 0x00000004, - TOUCHPAD = 0x00000005 + PT_NONE = 0x00000000, + PT_POINTER = 0x00000001, + PT_TOUCH = 0x00000002, + PT_PEN = 0x00000003, + PT_MOUSE = 0x00000004, + PT_TOUCHPAD = 0x00000005 } public enum WindowsMessage : uint @@ -882,6 +879,134 @@ namespace Avalonia.Win32.Interop SCF_ISSECURE = 0x00000001, } + [Flags] + public enum PointerFlags + { + POINTER_FLAG_NONE = 0x00000000, + POINTER_FLAG_NEW = 0x00000001, + POINTER_FLAG_INRANGE = 0x00000002, + POINTER_FLAG_INCONTACT = 0x00000004, + POINTER_FLAG_FIRSTBUTTON = 0x00000010, + POINTER_FLAG_SECONDBUTTON = 0x00000020, + POINTER_FLAG_THIRDBUTTON = 0x00000040, + POINTER_FLAG_FOURTHBUTTON = 0x00000080, + POINTER_FLAG_FIFTHBUTTON = 0x00000100, + POINTER_FLAG_PRIMARY = 0x00002000, + POINTER_FLAG_CONFIDENCE = 0x00000400, + POINTER_FLAG_CANCELED = 0x00000800, + POINTER_FLAG_DOWN = 0x00010000, + POINTER_FLAG_UPDATE = 0x00020000, + POINTER_FLAG_UP = 0x00040000, + POINTER_FLAG_WHEEL = 0x00080000, + POINTER_FLAG_HWHEEL = 0x00100000, + POINTER_FLAG_CAPTURECHANGED = 0x00200000, + POINTER_FLAG_HASTRANSFORM = 0x00400000 + } + + public enum PointerButtonChangeType : ulong + { + POINTER_CHANGE_NONE, + POINTER_CHANGE_FIRSTBUTTON_DOWN, + POINTER_CHANGE_FIRSTBUTTON_UP, + POINTER_CHANGE_SECONDBUTTON_DOWN, + POINTER_CHANGE_SECONDBUTTON_UP, + POINTER_CHANGE_THIRDBUTTON_DOWN, + POINTER_CHANGE_THIRDBUTTON_UP, + POINTER_CHANGE_FOURTHBUTTON_DOWN, + POINTER_CHANGE_FOURTHBUTTON_UP, + POINTER_CHANGE_FIFTHBUTTON_DOWN, + POINTER_CHANGE_FIFTHBUTTON_UP + } + + [Flags] + public enum PenFlags + { + PEN_FLAGS_NONE = 0x00000000, + PEN_FLAGS_BARREL = 0x00000001, + PEN_FLAGS_INVERTED = 0x00000002, + PEN_FLAGS_ERASER = 0x00000004, + } + + [Flags] + public enum PenMask + { + PEN_MASK_NONE = 0x00000000, + PEN_MASK_PRESSURE = 0x00000001, + PEN_MASK_ROTATION = 0x00000002, + PEN_MASK_TILT_X = 0x00000004, + PEN_MASK_TILT_Y = 0x00000008 + } + + [Flags] + public enum TouchFlags + { + TOUCH_FLAG_NONE = 0x00000000 + } + + [Flags] + public enum TouchMask + { + TOUCH_MASK_NONE = 0x00000000, + TOUCH_MASK_CONTACTAREA = 0x00000001, + TOUCH_MASK_ORIENTATION = 0x00000002, + TOUCH_MASK_PRESSURE = 0x00000004, + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct POINTER_TOUCH_INFO + { + public POINTER_INFO pointerInfo; + public TouchFlags touchFlags; + public TouchMask touchMask; + public int rcContactLeft; + public int rcContactTop; + public int rcContactRight; + public int rcContactBottom; + public int rcContactRawLeft; + public int rcContactRawTop; + public int rcContactRawRight; + public int rcContactRawBottom; + public uint orientation; + public uint pressure; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct POINTER_PEN_INFO + { + public POINTER_INFO pointerInfo; + public PenFlags penFlags; + public PenMask penMask; + public uint pressure; + public uint rotation; + public int tiltX; + public int tiltY; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct POINTER_INFO + { + public PointerInputType pointerType; + public uint pointerId; + public uint frameId; + public PointerFlags pointerFlags; + public IntPtr sourceDevice; + public IntPtr hwndTarget; + public int ptPixelLocationX; + public int ptPixelLocationY; + public int ptHimetricLocationX; + public int ptHimetricLocationY; + public int ptPixelLocationRawX; + public int ptPixelLocationRawY; + public int ptHimetricLocationRawX; + public int ptHimetricLocationRawY; + public uint dwTime; + public uint historyCount; + public int inputData; + public ModifierKeys dwKeyStates; + public UInt64 PerformanceCount; + public PointerButtonChangeType ButtonChangeType; + } + [StructLayout(LayoutKind.Sequential)] public struct RGBQUAD { @@ -956,7 +1081,16 @@ namespace Avalonia.Win32.Interop public static extern bool GetPointerCursorId(uint pointerID, out uint cursorId); [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetPointerType(uint pointerID, out InputType pointerType); + public static extern bool GetPointerType(uint pointerID, out PointerInputType pointerType); + + [DllImport("User32.dll", SetLastError = true)] + public static extern bool GetPointerInfo(uint pointerID, out POINTER_INFO pointerInfo); + + [DllImport("User32.dll", SetLastError = true)] + public static extern bool GetPointerPenInfo(uint pointerID, out POINTER_PEN_INFO penInfo); + + [DllImport("User32.dll", SetLastError = true)] + public static extern bool GetPointerTouchInfo(uint pointerID, out POINTER_TOUCH_INFO touchInfo); [DllImport("user32.dll", SetLastError = true)] public static extern void GetUnpredictedMessagePos(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index e16901af2c..8a0654a9ba 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -427,17 +427,17 @@ namespace Avalonia.Win32 { var pointerId = ToPointerId(wParam); GetPointerType(pointerId, out var type); - IInputDevice device = _mouseDevice; + IInputDevice device; switch (type) { - case InputType.PEN: - + case PointerInputType.PT_PEN: + device = _penDevice; break; - case InputType.TOUCH: + case PointerInputType.PT_TOUCH: device = _touchDevice; break; - case InputType.TOUCHPAD: - + default: + device = _mouseDevice; break; } @@ -445,7 +445,7 @@ namespace Avalonia.Win32 var delta = GetWheelDelta(wParam); var point = PointFromLParam(lParam); e = new RawMouseWheelEventArgs( - _mouseDevice, + device, timestamp, _owner, PointToClient(point), diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e4f5268285..8f6a7e9741 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -64,6 +64,7 @@ namespace Avalonia.Win32 private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); private readonly TouchDevice _touchDevice; private readonly MouseDevice _mouseDevice; + private readonly PenDevice _penDevice; private readonly ManagedDeferredRendererLock _rendererLock; private readonly FramebufferManager _framebuffer; private readonly IGlPlatformSurface _gl; @@ -96,6 +97,7 @@ namespace Avalonia.Win32 { _touchDevice = new TouchDevice(); _mouseDevice = new WindowsMouseDevice(); + _penDevice = new PenDevice(); #if USE_MANAGED_DRAG _managedDrag = new ManagedWindowResizeDragHelper(this, capture => From 1192e86d90f0d6f4df29acc442a21031e7fb0c3e Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 18:39:36 +0300 Subject: [PATCH 03/42] Investigate all the messages and note short comments about them and provide a link to a documentation. In case anyone will need this in the future --- .../Interop/UnmanagedMethods.cs | 2 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 139 ++++++++++++------ src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 3 files changed, 99 insertions(+), 44 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5aecd8e8f0..93457f7bbd 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -730,7 +730,7 @@ namespace Avalonia.Win32.Interop WM_TOUCHHITTESTING = 0x024D, WM_POINTERWHEEL = 0x024E, WM_POINTERHWHEEL = 0x024F, - WM_POINTERHITTEST = 0x0250, + DM_POINTERHITTEST = 0x0250, WM_IME_SETCONTEXT = 0x0281, WM_IME_NOTIFY = 0x0282, diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 8a0654a9ba..3587eddc85 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -227,10 +227,6 @@ namespace Avalonia.Win32 } // Mouse capture is lost case WindowsMessage.WM_CANCELMODE: - if (BelowWin8) - { - break; - } _mouseDevice.Capture(null); break; @@ -263,7 +259,8 @@ namespace Avalonia.Win32 timestamp, _owner, RawPointerEventType.Move, - DipFromLParam(lParam), GetMouseModifiers(wParam)); + DipFromLParam(lParam), + GetMouseModifiers(wParam)); break; } @@ -279,7 +276,8 @@ namespace Avalonia.Win32 timestamp, _owner, PointToClient(PointFromLParam(lParam)), - new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); + new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), + GetMouseModifiers(wParam)); break; } @@ -294,7 +292,8 @@ namespace Avalonia.Win32 timestamp, _owner, PointToClient(PointFromLParam(lParam)), - new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); + new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), + GetMouseModifiers(wParam)); break; } @@ -310,7 +309,8 @@ namespace Avalonia.Win32 timestamp, _owner, RawPointerEventType.LeaveWindow, - new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); + new Point(-1, -1), + WindowsKeyboardDevice.Instance.Modifiers); break; } @@ -386,16 +386,24 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERDEVICECHANGE: + { + //notifies about changes in the settings of a monitor that has a digitizer attached to it. + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdevicechange + break; + } case WindowsMessage.WM_POINTERDEVICEINRANGE: case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: { - + //notifies about proximity of pointer device to the digitizer. + //contains pointer id and proximity. + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdeviceinrange break; } case WindowsMessage.WM_NCPOINTERUPDATE: case WindowsMessage.WM_NCPOINTERDOWN: case WindowsMessage.WM_NCPOINTERUP: { + //NC stands for non-client area - window header and window border break; } @@ -407,60 +415,79 @@ namespace Avalonia.Win32 break; } case WindowsMessage.WM_POINTERENTER: - case WindowsMessage.WM_POINTERLEAVE: { + //this is not handled by WM_MOUSEENTER so I think there is no need to handle this too. + //but we can detect a new pointer by this message and calling IS_POINTER_NEW_WPARAM + //note: by using a pen there can be a pointer leave or enter inside a window coords + //when you are just lift up the pen above the display break; } - case WindowsMessage.WM_POINTERACTIVATE: - case WindowsMessage.WM_POINTERCAPTURECHANGED: + case WindowsMessage.WM_POINTERLEAVE: { + GetDeviceInfo(wParam, out var device, out var info); + var point = PointToClient(PointFromLParam(lParam)); + e = new RawPointerEventArgs( + device, + timestamp, + _owner, + RawPointerEventType.LeaveWindow, + point, + WindowsKeyboardDevice.Instance.Modifiers); break; } - case WindowsMessage.WM_TOUCHHITTESTING: + case WindowsMessage.WM_POINTERACTIVATE: { - + //occurs when a pointer activates an inactive window. + //we should handle this and return PA_ACTIVATE or PA_NOACTIVATE + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointeractivate break; } + case WindowsMessage.WM_POINTERCAPTURECHANGED: + { + _mouseDevice.Capture(null); + return IntPtr.Zero; + } case WindowsMessage.WM_POINTERWHEEL: { - var pointerId = ToPointerId(wParam); - GetPointerType(pointerId, out var type); - IInputDevice device; - switch (type) - { - case PointerInputType.PT_PEN: - device = _penDevice; - break; - case PointerInputType.PT_TOUCH: - device = _touchDevice; - break; - default: - device = _mouseDevice; - break; - } - + GetDeviceInfo(wParam, out var device, out var info); - var delta = GetWheelDelta(wParam); - var point = PointFromLParam(lParam); - e = new RawMouseWheelEventArgs( - device, - timestamp, - _owner, - PointToClient(point), - new Vector(0, delta / wheelDelta), - GetMouseModifiers(wParam)); + var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); + var delta = new Vector(0, GetWheelDelta(wParam) / wheelDelta); + e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } case WindowsMessage.WM_POINTERHWHEEL: { + GetDeviceInfo(wParam, out var device, out var info); + var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); + var delta = new Vector(GetWheelDelta(wParam) / wheelDelta, 0); + e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } - case WindowsMessage.WM_POINTERHITTEST: + case WindowsMessage.DM_POINTERHITTEST: { - + //DM stands for direct manipulation. + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/directmanipulation/direct-manipulation-portal + break; + } + case WindowsMessage.WM_TOUCHHITTESTING: + { + //This is to determine the most probable touch target. + //provides an input bounding box and receives hit proximity + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-touchhittesting + //https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-touch_hit_testing_input + break; + } + case WindowsMessage.WM_PARENTNOTIFY: + { + //This message is sent in a dialog scenarios. Contains mouse position in an old-way, + //but listed in the wm_pointer reference + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-parentnotify break; } @@ -687,13 +714,36 @@ namespace Avalonia.Win32 } } + private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info) + { + var pointerId = ToPointerId(wParam); + GetPointerType(pointerId, out var type);//ToDo we can cache this and invalidate in WM_POINTERDEVICECHANGE + switch (type) + { + case PointerInputType.PT_PEN: + device = _penDevice; + GetPointerPenInfo(pointerId, out var penInfo); + info = penInfo.pointerInfo; + break; + case PointerInputType.PT_TOUCH: + device = _touchDevice; + GetPointerTouchInfo(pointerId, out var touchInfo); + info = touchInfo.pointerInfo; + break; + default: + device = _mouseDevice; + GetPointerInfo(pointerId, out info); + break; + } + } + public static uint GetWheelDelta(IntPtr wParam) => HIWORD(wParam); public static uint ToPointerId(IntPtr wParam) => LOWORD(wParam); public static uint LOWORD(IntPtr param) => (uint)param & 0xffff; public static uint HIWORD(IntPtr param) => (uint)param >> 16; - public bool BelowWin8 => Win32Platform.WindowsVersion < PlatformConstants.Windows8; + public readonly bool BelowWin8 = Win32Platform.WindowsVersion < PlatformConstants.Windows8; private void UpdateInputMethod(IntPtr hkl) { @@ -747,6 +797,11 @@ namespace Avalonia.Win32 private static RawInputModifiers GetMouseModifiers(IntPtr wParam) { var keys = (ModifierKeys)ToInt32(wParam); + return GetInputModifiers(keys); + } + + private static RawInputModifiers GetInputModifiers(ModifierKeys keys) + { var modifiers = WindowsKeyboardDevice.Instance.Modifiers; if (keys.HasAllFlags(ModifierKeys.MK_LBUTTON)) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8f6a7e9741..0f99252d4d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -76,7 +76,7 @@ namespace Avalonia.Win32 private bool _multitouch; private IInputRoot _owner; private WindowProperties _windowProperties; - private bool _trackingMouse; + private bool _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk private bool _topmost; private double _scaling = 1; private WindowState _showWindowState; From 35a55c9a56dca3ccc957a559e0b50e1aa48fdab0 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 21:37:54 +0300 Subject: [PATCH 04/42] Enable WM_Pointer for mouse events, handle up/down events, some fixes --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 73 +++++++++++++------ src/Windows/Avalonia.Win32/WindowImpl.cs | 5 ++ 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 3587eddc85..5e31bba925 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -166,7 +166,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -199,7 +199,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -232,7 +232,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEMOVE: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -267,7 +267,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEWHEEL: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -283,7 +283,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEHWHEEL: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -299,7 +299,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSELEAVE: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -319,7 +319,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -343,7 +343,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_TOUCH: { - if (BelowWin8) + if (Win8Plus) { break; } @@ -400,18 +400,53 @@ namespace Avalonia.Win32 break; } case WindowsMessage.WM_NCPOINTERUPDATE: - case WindowsMessage.WM_NCPOINTERDOWN: - case WindowsMessage.WM_NCPOINTERUP: { //NC stands for non-client area - window header and window border - + //As I found above in an old message handling - we dont need to handle NC pointer move/updates. + //All we need is pointer down and up. So this is skipped for now. break; } - case WindowsMessage.WM_POINTERUPDATE: + case WindowsMessage.WM_NCPOINTERDOWN: + case WindowsMessage.WM_NCPOINTERUP: case WindowsMessage.WM_POINTERDOWN: case WindowsMessage.WM_POINTERUP: { + GetDeviceInfo(wParam, out var device, out var info); + var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); + var eventType = info.ButtonChangeType switch + { + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN => RawPointerEventType.LeftButtonDown, + PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown, + PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_DOWN => RawPointerEventType.MiddleButtonDown, + PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_DOWN => RawPointerEventType.XButton1Down, + PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_DOWN => RawPointerEventType.XButton2Down, + + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP => RawPointerEventType.LeftButtonUp, + PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_UP => RawPointerEventType.RightButtonUp, + PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_UP => RawPointerEventType.MiddleButtonUp, + PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up, + PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up, + }; + if (eventType == RawPointerEventType.NonClientLeftButtonDown && + (WindowsMessage)msg == WindowsMessage.WM_NCPOINTERDOWN) + { + eventType = RawPointerEventType.NonClientLeftButtonDown; + } + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + break; + } + case WindowsMessage.WM_POINTERUPDATE: + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + GetDeviceInfo(wParam, out var device, out var info); + var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); + e = new RawPointerEventArgs(device, timestamp, _owner, RawPointerEventType.Move, point, modifiers); break; } case WindowsMessage.WM_POINTERENTER: @@ -455,7 +490,7 @@ namespace Avalonia.Win32 var point = PointToClient(PointFromLParam(lParam)); var modifiers = GetInputModifiers(info.dwKeyStates); - var delta = new Vector(0, GetWheelDelta(wParam) / wheelDelta); + var delta = new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } @@ -465,7 +500,7 @@ namespace Avalonia.Win32 var point = PointToClient(PointFromLParam(lParam)); var modifiers = GetInputModifiers(info.dwKeyStates); - var delta = new Vector(GetWheelDelta(wParam) / wheelDelta, 0); + var delta = new Vector((ToInt32(wParam) >> 16) / wheelDelta, 0); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } @@ -716,7 +751,7 @@ namespace Avalonia.Win32 private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info) { - var pointerId = ToPointerId(wParam); + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); GetPointerType(pointerId, out var type);//ToDo we can cache this and invalidate in WM_POINTERDEVICECHANGE switch (type) { @@ -737,13 +772,7 @@ namespace Avalonia.Win32 } } - public static uint GetWheelDelta(IntPtr wParam) => HIWORD(wParam); - public static uint ToPointerId(IntPtr wParam) => LOWORD(wParam); - public static uint LOWORD(IntPtr param) => (uint)param & 0xffff; - public static uint HIWORD(IntPtr param) => (uint)param >> 16; - - - public readonly bool BelowWin8 = Win32Platform.WindowsVersion < PlatformConstants.Windows8; + public readonly bool Win8Plus = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; private void UpdateInputMethod(IntPtr hkl) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0f99252d4d..0ad198e991 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -126,6 +126,11 @@ namespace Avalonia.Win32 egl.Display is AngleWin32EglDisplay angleDisplay && angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; + if (Win8Plus && !IsMouseInPointerEnabled()) + { + EnableMouseInPointer(true); + } + CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); UpdateInputMethod(GetKeyboardLayout(0)); From 4cea62fd13701931ba7aedbb2b2f6fce9f9477a6 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 23:34:43 +0300 Subject: [PATCH 05/42] PenDevice tuning, Add pen properties to the PointerPointProperties --- src/Avalonia.Input/IKeyboardDevice.cs | 2 + src/Avalonia.Input/IPointer.cs | 3 +- src/Avalonia.Input/PenDevice.cs | 134 +++--------------- src/Avalonia.Input/PointerPoint.cs | 30 +++- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 8 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 64 +++------ 6 files changed, 72 insertions(+), 169 deletions(-) diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index 9506dc36fb..b3ea7c5f4f 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -47,6 +47,8 @@ namespace Avalonia.Input MiddleMouseButton = 64, XButton1MouseButton = 128, XButton2MouseButton = 256, + BarrelPenButton = 512, + PenEraser = 1024, KeyboardMask = Alt | Control | Shift | Meta } diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs index 7af48cef82..361f3ac370 100644 --- a/src/Avalonia.Input/IPointer.cs +++ b/src/Avalonia.Input/IPointer.cs @@ -13,6 +13,7 @@ namespace Avalonia.Input public enum PointerType { Mouse, - Touch + Touch, + Pen } } diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index dd3d60ebb9..3c0f3c7709 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Reactive.Linq; using Avalonia.Input.Raw; using Avalonia.Interactivity; -using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Input @@ -13,17 +12,13 @@ namespace Avalonia.Input /// public class PenDevice : IPenDevice, IDisposable { - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - private readonly Pointer _pointer; private bool _disposed; private PixelPoint? _position; public PenDevice(Pointer? pointer = null) { - _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Pen, true); } /// @@ -37,16 +32,6 @@ namespace Avalonia.Input [Obsolete("Use IPointer instead")] public IInputElement? Captured => _pointer.Captured; - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use events instead")] - public PixelPoint Position - { - get => _position ?? new PixelPoint(-1, -1); - protected set => _position = value; - } - /// /// Captures mouse input to the specified control. /// @@ -76,7 +61,7 @@ namespace Avalonia.Input } #pragma warning disable CS0618 // Type or member is obsolete - var rootPoint = relativeTo.VisualRoot.PointToClient(Position); + var rootPoint = relativeTo.VisualRoot.PointToClient(_position ?? new PixelPoint(-1, -1)); #pragma warning restore CS0618 // Type or member is obsolete var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); return rootPoint * transform!.Value; @@ -120,29 +105,13 @@ namespace Avalonia.Input } } } - - int ButtonCount(PointerPointProperties props) - { - var rv = 0; - if (props.IsLeftButtonPressed) - rv++; - if (props.IsMiddleButtonPressed) - rv++; - if (props.IsRightButtonPressed) - rv++; - if (props.IsXButton1Pressed) - rv++; - if (props.IsXButton2Pressed) - rv++; - return rv; - } private void ProcessRawEvent(RawPointerEventArgs e) { e = e ?? throw new ArgumentNullException(nameof(e)); - var mouse = (PenDevice)e.Device; - if(mouse._disposed) + var pen = (PenDevice)e.Device; + if(pen._disposed) return; _position = e.Root.PointToScreen(e.Position); @@ -151,34 +120,16 @@ namespace Avalonia.Input switch (e.Type) { case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); + LeaveWindow(pen, e.Timestamp, e.Root, props, keyModifiers); break; - case RawPointerEventType.LeftButtonDown: - case RawPointerEventType.RightButtonDown: - case RawPointerEventType.MiddleButtonDown: - case RawPointerEventType.XButton1Down: - case RawPointerEventType.XButton2Down: - if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - else - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, keyModifiers); + case RawPointerEventType.PenBegin: + e.Handled = PenDown(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; - case RawPointerEventType.LeftButtonUp: - case RawPointerEventType.RightButtonUp: - case RawPointerEventType.MiddleButtonUp: - case RawPointerEventType.XButton1Up: - case RawPointerEventType.XButton2Up: - if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + case RawPointerEventType.PenEnd: + e.Handled = PenUp(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; - case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - break; - case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); + case RawPointerEventType.PenUpdate: + e.Handled = PenMove(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; } } @@ -196,35 +147,18 @@ namespace Avalonia.Input PointerPointProperties CreateProperties(RawPointerEventArgs args) { - var kind = PointerUpdateKind.Other; - if (args.Type == RawPointerEventType.LeftButtonDown) + if (args.Type == RawPointerEventType.PenBegin) kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) + if (args.Type == RawPointerEventType.PenEnd) kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; return new PointerPointProperties(args.InputModifiers, kind); } private MouseButton _lastMouseDownButton; - private bool MouseDown(IPenDevice device, ulong timestamp, IInputElement root, Point p, + private bool PenDown(IPenDevice device, ulong timestamp, IInputElement root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { @@ -239,21 +173,8 @@ namespace Avalonia.Input var source = GetSource(hit); if (source != null) { - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; - var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) - { - _clickCount = 0; - } - - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, 1); source.RaiseEvent(e); return e.Handled; } @@ -262,7 +183,7 @@ namespace Avalonia.Input return false; } - private bool MouseMove(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, + private bool PenMove(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, KeyModifiers inputModifiers) { device = device ?? throw new ArgumentNullException(nameof(device)); @@ -292,7 +213,7 @@ namespace Avalonia.Input return false; } - private bool MouseUp(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, + private bool PenUp(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, KeyModifiers inputModifiers) { device = device ?? throw new ArgumentNullException(nameof(device)); @@ -314,27 +235,6 @@ namespace Avalonia.Input return false; } - private bool MouseWheel(IPenDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - private IInteractive? GetSource(IVisual? hit) { if (hit is null) diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 9f8285a8e1..16fab7410c 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -20,6 +20,14 @@ namespace Avalonia.Input public bool IsRightButtonPressed { get; } public bool IsXButton1Pressed { get; } public bool IsXButton2Pressed { get; } + public bool IsBarrelButtonPressed { get; } + public bool IsEraser { get; } + + public float Twist { get; } + public float Pressure { get; } + public float XTilt { get; } + public float YTilt { get; } + public PointerUpdateKind PointerUpdateKind { get; } @@ -36,10 +44,12 @@ namespace Avalonia.Input IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton); IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton); IsXButton2Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton2MouseButton); + IsBarrelButtonPressed = modifiers.HasAllFlags(RawInputModifiers.BarrelPenButton); + IsEraser = modifiers.HasAllFlags(RawInputModifiers.PenEraser); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state - + if (kind == PointerUpdateKind.LeftButtonPressed) IsLeftButtonPressed = true; if (kind == PointerUpdateKind.LeftButtonReleased) @@ -60,6 +70,20 @@ namespace Avalonia.Input IsXButton2Pressed = true; if (kind == PointerUpdateKind.XButton2Released) IsXButton2Pressed = false; + if (kind == PointerUpdateKind.BarrelButtonPressed) + IsBarrelButtonPressed = true; + if (kind == PointerUpdateKind.BarrelButtonReleased) + IsBarrelButtonPressed = false; + } + + public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind, + float twist, float pressure, float xTilt, float yTilt + ) : this (modifiers, kind) + { + Twist = twist; + Pressure = pressure; + XTilt = xTilt; + YTilt = yTilt; } public static PointerPointProperties None { get; } = new PointerPointProperties(); @@ -77,7 +101,9 @@ namespace Avalonia.Input RightButtonReleased, XButton1Released, XButton2Released, - Other + Other, + BarrelButtonPressed, + BarrelButtonReleased } public static class PointerUpdateKindExtensions diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 62a1dd5d84..ea160efef5 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -21,7 +21,13 @@ namespace Avalonia.Input.Raw TouchBegin, TouchUpdate, TouchEnd, - TouchCancel + TouchCancel, + PenBegin, + PenUpdate, + PenEnd, + PenCancel, + BarrelUp, + BarrelDown, } /// diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 5e31bba925..db11508ed7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -374,17 +374,6 @@ namespace Avalonia.Win32 break; } - - - - - - - - - - - case WindowsMessage.WM_POINTERDEVICECHANGE: { //notifies about changes in the settings of a monitor that has a digitizer attached to it. @@ -414,7 +403,10 @@ namespace Avalonia.Win32 GetDeviceInfo(wParam, out var device, out var info); var point = PointToClient(PointFromLParam(lParam)); var modifiers = GetInputModifiers(info.dwKeyStates); - var eventType = info.ButtonChangeType switch + + if (info.ButtonChangeType != PointerButtonChangeType.POINTER_CHANGE_NONE) + { + var eventType = info.ButtonChangeType switch { PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN => RawPointerEventType.LeftButtonDown, PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown, @@ -428,12 +420,13 @@ namespace Avalonia.Win32 PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up, PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up, }; - if (eventType == RawPointerEventType.NonClientLeftButtonDown && - (WindowsMessage)msg == WindowsMessage.WM_NCPOINTERDOWN) - { - eventType = RawPointerEventType.NonClientLeftButtonDown; + if (eventType == RawPointerEventType.NonClientLeftButtonDown && + (WindowsMessage)msg == WindowsMessage.WM_NCPOINTERDOWN) + { + eventType = RawPointerEventType.NonClientLeftButtonDown; + } + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); } - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); break; } case WindowsMessage.WM_POINTERUPDATE: @@ -446,7 +439,8 @@ namespace Avalonia.Win32 var point = PointToClient(PointFromLParam(lParam)); var modifiers = GetInputModifiers(info.dwKeyStates); - e = new RawPointerEventArgs(device, timestamp, _owner, RawPointerEventType.Move, point, modifiers); + e = new RawPointerEventArgs( + device, timestamp, _owner, RawPointerEventType.Move, point, modifiers); break; } case WindowsMessage.WM_POINTERENTER: @@ -462,14 +456,10 @@ namespace Avalonia.Win32 { GetDeviceInfo(wParam, out var device, out var info); var point = PointToClient(PointFromLParam(lParam)); + var modifiers = GetInputModifiers(info.dwKeyStates); e = new RawPointerEventArgs( - device, - timestamp, - _owner, - RawPointerEventType.LeaveWindow, - point, - WindowsKeyboardDevice.Instance.Modifiers); + device, timestamp, _owner, RawPointerEventType.LeaveWindow, point, modifiers); break; } case WindowsMessage.WM_POINTERACTIVATE: @@ -520,33 +510,11 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_PARENTNOTIFY: { - //This message is sent in a dialog scenarios. Contains mouse position in an old-way, - //but listed in the wm_pointer reference + //This message is sent in a dialog scenarios. Contains mouse position. + //Old message, but listed in the wm_pointer reference //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-parentnotify break; } - - - - - - - - - - - - - - - - - - - - - - case WindowsMessage.WM_NCPAINT: { if (!HasFullDecorations) From d3f3166914428a945d24b8369c2b6c18ff0aab34 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 14:16:35 +0300 Subject: [PATCH 06/42] Provide Pressure, Tilt, and other parameters to PointerPointProperties. Provide touch events (still a lot to do), use point position from pointer info. --- src/Avalonia.Input/PenDevice.cs | 25 ++-- src/Avalonia.Input/PointerPoint.cs | 12 +- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 4 - .../Interop/UnmanagedMethods.cs | 2 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 111 ++++++++++++------ 5 files changed, 98 insertions(+), 56 deletions(-) diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index 3c0f3c7709..c993e7fac4 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -32,6 +32,14 @@ namespace Avalonia.Input [Obsolete("Use IPointer instead")] public IInputElement? Captured => _pointer.Captured; + public bool IsEraser { get; set; } + public bool IsInverted { get; set; } + public bool IsBarrel { get; set; } + public int XTilt { get; set; } + public int YTilt { get; set; } + public uint Pressure { get; set; } + public uint Twist { get; set; } + /// /// Captures mouse input to the specified control. /// @@ -122,13 +130,13 @@ namespace Avalonia.Input case RawPointerEventType.LeaveWindow: LeaveWindow(pen, e.Timestamp, e.Root, props, keyModifiers); break; - case RawPointerEventType.PenBegin: + case RawPointerEventType.LeftButtonDown: e.Handled = PenDown(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; - case RawPointerEventType.PenEnd: + case RawPointerEventType.LeftButtonUp: e.Handled = PenUp(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; - case RawPointerEventType.PenUpdate: + case RawPointerEventType.Move: e.Handled = PenMove(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; } @@ -145,16 +153,17 @@ namespace Avalonia.Input } - PointerPointProperties CreateProperties(RawPointerEventArgs args) + private PointerPointProperties CreateProperties(RawPointerEventArgs args) { var kind = PointerUpdateKind.Other; - if (args.Type == RawPointerEventType.PenBegin) + if (args.Type == RawPointerEventType.LeftButtonDown) kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.PenEnd) + if (args.Type == RawPointerEventType.LeftButtonUp) kind = PointerUpdateKind.LeftButtonReleased; - - return new PointerPointProperties(args.InputModifiers, kind); + + return new PointerPointProperties(args.InputModifiers, kind, + Twist, Pressure, XTilt, YTilt, IsEraser, IsInverted, IsBarrel); } private MouseButton _lastMouseDownButton; diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 16fab7410c..07ef130fcc 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -22,6 +22,7 @@ namespace Avalonia.Input public bool IsXButton2Pressed { get; } public bool IsBarrelButtonPressed { get; } public bool IsEraser { get; } + public bool IsInverted { get; } public float Twist { get; } public float Pressure { get; } @@ -70,20 +71,19 @@ namespace Avalonia.Input IsXButton2Pressed = true; if (kind == PointerUpdateKind.XButton2Released) IsXButton2Pressed = false; - if (kind == PointerUpdateKind.BarrelButtonPressed) - IsBarrelButtonPressed = true; - if (kind == PointerUpdateKind.BarrelButtonReleased) - IsBarrelButtonPressed = false; } public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind, - float twist, float pressure, float xTilt, float yTilt + float twist, float pressure, float xTilt, float yTilt, bool isEraser, bool isInverted, bool isBarrel ) : this (modifiers, kind) { Twist = twist; Pressure = pressure; XTilt = xTilt; YTilt = yTilt; + IsEraser = isEraser; + IsInverted = isInverted; + IsBarrelButtonPressed = isBarrel; } public static PointerPointProperties None { get; } = new PointerPointProperties(); @@ -102,8 +102,6 @@ namespace Avalonia.Input XButton1Released, XButton2Released, Other, - BarrelButtonPressed, - BarrelButtonReleased } public static class PointerUpdateKindExtensions diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 6a35ec3fad..c75eb69bfd 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -22,10 +22,6 @@ namespace Avalonia.Input.Raw TouchUpdate, TouchEnd, TouchCancel, - PenBegin, - PenUpdate, - PenEnd, - PenCancel, BarrelUp, BarrelDown, Magnify, diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 93457f7bbd..b3cab5d052 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1003,7 +1003,7 @@ namespace Avalonia.Win32.Interop public uint historyCount; public int inputData; public ModifierKeys dwKeyStates; - public UInt64 PerformanceCount; + public ulong PerformanceCount; public PointerButtonChangeType ButtonChangeType; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index db11508ed7..34cd9acf25 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -22,8 +22,8 @@ namespace Avalonia.Win32 uint timestamp = unchecked((uint)GetMessageTime()); RawInputEventArgs e = null; var shouldTakeFocus = false; - - switch ((WindowsMessage)msg) + var message = (WindowsMessage)msg; + switch (message) { case WindowsMessage.WM_ACTIVATE: { @@ -180,7 +180,7 @@ namespace Avalonia.Win32 _mouseDevice, timestamp, _owner, - (WindowsMessage)msg switch + message switch { WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, @@ -212,7 +212,7 @@ namespace Avalonia.Win32 _mouseDevice, timestamp, _owner, - (WindowsMessage)msg switch + message switch { WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, @@ -327,7 +327,7 @@ namespace Avalonia.Win32 _mouseDevice, timestamp, _owner, - (WindowsMessage)msg switch + message switch { WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType .NonClientLeftButtonDown, @@ -400,33 +400,13 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERDOWN: case WindowsMessage.WM_POINTERUP: { + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var eventType = GetEventType(message, info); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); - if (info.ButtonChangeType != PointerButtonChangeType.POINTER_CHANGE_NONE) - { - var eventType = info.ButtonChangeType switch - { - PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN => RawPointerEventType.LeftButtonDown, - PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown, - PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_DOWN => RawPointerEventType.MiddleButtonDown, - PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_DOWN => RawPointerEventType.XButton1Down, - PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_DOWN => RawPointerEventType.XButton2Down, - - PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP => RawPointerEventType.LeftButtonUp, - PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_UP => RawPointerEventType.RightButtonUp, - PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_UP => RawPointerEventType.MiddleButtonUp, - PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up, - PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up, - }; - if (eventType == RawPointerEventType.NonClientLeftButtonDown && - (WindowsMessage)msg == WindowsMessage.WM_NCPOINTERDOWN) - { - eventType = RawPointerEventType.NonClientLeftButtonDown; - } - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); - } + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); break; } case WindowsMessage.WM_POINTERUPDATE: @@ -436,11 +416,11 @@ namespace Avalonia.Win32 break; } GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); + var eventType = device is TouchDevice ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move; - e = new RawPointerEventArgs( - device, timestamp, _owner, RawPointerEventType.Move, point, modifiers); + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); break; } case WindowsMessage.WM_POINTERENTER: @@ -455,7 +435,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERLEAVE: { GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); e = new RawPointerEventArgs( @@ -478,7 +458,7 @@ namespace Avalonia.Win32 { GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); var delta = new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); @@ -488,7 +468,7 @@ namespace Avalonia.Win32 { GetDeviceInfo(wParam, out var device, out var info); - var point = PointToClient(PointFromLParam(lParam)); + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); var delta = new Vector((ToInt32(wParam) >> 16) / wheelDelta, 0); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); @@ -696,7 +676,7 @@ namespace Avalonia.Win32 { Input(e); - if ((WindowsMessage)msg == WindowsMessage.WM_KEYDOWN) + if (message == WindowsMessage.WM_KEYDOWN) { // Handling a WM_KEYDOWN message should cause the subsequent WM_CHAR message to // be ignored. This should be safe to do as WM_CHAR should only be produced in @@ -717,6 +697,44 @@ namespace Avalonia.Win32 } } + private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) + { + switch (info.pointerType) + { + case PointerInputType.PT_PEN: + return ToEventType(info.ButtonChangeType); + case PointerInputType.PT_TOUCH: + if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED) || + !info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CONFIDENCE)) + { + return RawPointerEventType.TouchCancel; + } + return message == WindowsMessage.WM_POINTERDOWN || message == WindowsMessage.WM_NCPOINTERDOWN + ? RawPointerEventType.TouchBegin + : RawPointerEventType.TouchEnd; + default: + var eventType = ToEventType(info.ButtonChangeType); + if (eventType == RawPointerEventType.LeftButtonDown && + message == WindowsMessage.WM_NCPOINTERDOWN) + { + eventType = RawPointerEventType.NonClientLeftButtonDown; + } + return eventType; + } + } + + private unsafe void ApplyPenInfo(POINTER_PEN_INFO penInfo) + { + _penDevice.IsBarrel = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); + _penDevice.IsEraser = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); + _penDevice.IsInverted = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_INVERTED); + + _penDevice.XTilt = penInfo.tiltX; + _penDevice.YTilt = penInfo.tiltY; + _penDevice.Pressure = penInfo.pressure; + _penDevice.Twist = penInfo.rotation; + } + private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info) { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); @@ -727,6 +745,8 @@ namespace Avalonia.Win32 device = _penDevice; GetPointerPenInfo(pointerId, out var penInfo); info = penInfo.pointerInfo; + + ApplyPenInfo(penInfo); break; case PointerInputType.PT_TOUCH: device = _touchDevice; @@ -740,6 +760,25 @@ namespace Avalonia.Win32 } } + private static RawPointerEventType ToEventType(PointerButtonChangeType type) + { + return type switch + { + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN => RawPointerEventType.LeftButtonDown, + PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown, + PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_DOWN => RawPointerEventType.MiddleButtonDown, + PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_DOWN => RawPointerEventType.XButton1Down, + PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_DOWN => RawPointerEventType.XButton2Down, + + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP => RawPointerEventType.LeftButtonUp, + PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_UP => RawPointerEventType.RightButtonUp, + PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_UP => RawPointerEventType.MiddleButtonUp, + PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up, + PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up, + _ => RawPointerEventType.Move + }; + } + public readonly bool Win8Plus = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; private void UpdateInputMethod(IntPtr hkl) From c046165714c37e20b305cfae823347966cd95aca Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 15:36:29 +0300 Subject: [PATCH 07/42] Add History fetch methods (will be implemented a bit later). Raise touch events. --- .../Interop/UnmanagedMethods.cs | 25 +++-- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 103 +++++++++++------- 2 files changed, 76 insertions(+), 52 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b3cab5d052..d71efcb074 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1074,32 +1074,35 @@ namespace Avalonia.Win32.Interop public const int SizeOf_BITMAPINFOHEADER = 40; + [DllImport("user32.dll", SetLastError = true)] + public static extern bool IsMouseInPointerEnabled(); + [DllImport("user32.dll", SetLastError = true)] public static extern int EnableMouseInPointer(bool enable); [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetPointerCursorId(uint pointerID, out uint cursorId); + public static extern bool GetPointerCursorId(uint pointerId, out uint cursorId); [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetPointerType(uint pointerID, out PointerInputType pointerType); + public static extern bool GetPointerType(uint pointerId, out PointerInputType pointerType); - [DllImport("User32.dll", SetLastError = true)] - public static extern bool GetPointerInfo(uint pointerID, out POINTER_INFO pointerInfo); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerInfo(uint pointerId, out POINTER_INFO pointerInfo); - [DllImport("User32.dll", SetLastError = true)] - public static extern bool GetPointerPenInfo(uint pointerID, out POINTER_PEN_INFO penInfo); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_INFO[] pointerInfos); - [DllImport("User32.dll", SetLastError = true)] - public static extern bool GetPointerTouchInfo(uint pointerID, out POINTER_TOUCH_INFO touchInfo); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetPointerPenInfo(uint pointerId, out POINTER_PEN_INFO penInfo); [DllImport("user32.dll", SetLastError = true)] - public static extern void GetUnpredictedMessagePos(); + public static extern bool GetPointerPenInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_PEN_INFO[] penInfos); [DllImport("user32.dll", SetLastError = true)] - public static extern bool IsMouseInPointerEnabled(); + public static extern bool GetPointerTouchInfo(uint pointerId, out POINTER_TOUCH_INFO touchInfo); [DllImport("user32.dll", SetLastError = true)] - public static extern bool SkipPointerFrameMessages(uint pointerID); + public static extern bool GetPointerTouchInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_TOUCH_INFO[] touchInfos); [DllImport("user32.dll")] public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 34cd9acf25..fda68e1561 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -401,26 +401,41 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUP: { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var eventType = GetEventType(message, info); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + if (device is TouchDevice) + { + e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId); + } + else + { + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + } break; } case WindowsMessage.WM_POINTERUPDATE: { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); - var eventType = device is TouchDevice ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move; - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + if (device is TouchDevice) + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, + RawPointerEventType.TouchUpdate, point, modifiers, info.pointerId); + } + else + { + e = new RawPointerEventArgs(device, timestamp, _owner, + RawPointerEventType.Move, point, modifiers); + } break; } case WindowsMessage.WM_POINTERENTER: @@ -434,7 +449,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERLEAVE: { - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -456,7 +471,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERWHEEL: { - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -466,7 +481,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERHWHEEL: { - GetDeviceInfo(wParam, out var device, out var info); + GetDeviceInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -697,36 +712,10 @@ namespace Avalonia.Win32 } } - private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) - { - switch (info.pointerType) - { - case PointerInputType.PT_PEN: - return ToEventType(info.ButtonChangeType); - case PointerInputType.PT_TOUCH: - if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED) || - !info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CONFIDENCE)) - { - return RawPointerEventType.TouchCancel; - } - return message == WindowsMessage.WM_POINTERDOWN || message == WindowsMessage.WM_NCPOINTERDOWN - ? RawPointerEventType.TouchBegin - : RawPointerEventType.TouchEnd; - default: - var eventType = ToEventType(info.ButtonChangeType); - if (eventType == RawPointerEventType.LeftButtonDown && - message == WindowsMessage.WM_NCPOINTERDOWN) - { - eventType = RawPointerEventType.NonClientLeftButtonDown; - } - return eventType; - } - } - private unsafe void ApplyPenInfo(POINTER_PEN_INFO penInfo) { _penDevice.IsBarrel = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); - _penDevice.IsEraser = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); + _penDevice.IsEraser = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_ERASER); _penDevice.IsInverted = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_INVERTED); _penDevice.XTilt = penInfo.tiltX; @@ -735,10 +724,11 @@ namespace Avalonia.Win32 _penDevice.Twist = penInfo.rotation; } - private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info) + private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info, ref uint timestamp) { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - GetPointerType(pointerId, out var type);//ToDo we can cache this and invalidate in WM_POINTERDEVICECHANGE + GetPointerType(pointerId, out var type); + //GetPointerCursorId(pointerId, out var cursorId); switch (type) { case PointerInputType.PT_PEN: @@ -758,6 +748,37 @@ namespace Avalonia.Win32 GetPointerInfo(pointerId, out info); break; } + + if (info.dwTime != 0) + { + timestamp = info.dwTime; + } + } + + private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) + { + switch (info.pointerType) + { + case PointerInputType.PT_PEN: + return ToEventType(info.ButtonChangeType); + case PointerInputType.PT_TOUCH: + if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED) || + !info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CONFIDENCE)) + { + return RawPointerEventType.TouchCancel; + } + return message == WindowsMessage.WM_POINTERDOWN || message == WindowsMessage.WM_NCPOINTERDOWN + ? RawPointerEventType.TouchBegin + : RawPointerEventType.TouchEnd; + default: + var eventType = ToEventType(info.ButtonChangeType); + if (eventType == RawPointerEventType.LeftButtonDown && + message == WindowsMessage.WM_NCPOINTERDOWN) + { + eventType = RawPointerEventType.NonClientLeftButtonDown; + } + return eventType; + } } private static RawPointerEventType ToEventType(PointerButtonChangeType type) From cbc813235a80918b5f0a57ca54f5abe5b47b08cb Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 16:35:25 +0300 Subject: [PATCH 08/42] Combine some message handlers --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 68 +++++++------------ 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index fda68e1561..0266134199 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -399,26 +399,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCPOINTERUP: case WindowsMessage.WM_POINTERDOWN: case WindowsMessage.WM_POINTERUP: - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); - var eventType = GetEventType(message, info); - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - - if (device is TouchDevice) - { - e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId); - } - else - { - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); - } - break; - } case WindowsMessage.WM_POINTERUPDATE: { - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); + GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + var eventType = GetEventType(message, info); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -428,13 +412,11 @@ namespace Avalonia.Win32 { break; } - e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, - RawPointerEventType.TouchUpdate, point, modifiers, info.pointerId); + e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId); } else { - e = new RawPointerEventArgs(device, timestamp, _owner, - RawPointerEventType.Move, point, modifiers); + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); } break; } @@ -449,7 +431,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERLEAVE: { - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); + GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -457,6 +439,18 @@ namespace Avalonia.Win32 device, timestamp, _owner, RawPointerEventType.LeaveWindow, point, modifiers); break; } + case WindowsMessage.WM_POINTERWHEEL: + case WindowsMessage.WM_POINTERHWHEEL: + { + GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + + var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); + var modifiers = GetInputModifiers(info.dwKeyStates); + var val = (ToInt32(wParam) >> 16) / wheelDelta; + var delta = message == WindowsMessage.WM_POINTERHWHEEL ? new Vector(0, val) : new Vector(val, 0); + e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); + break; + } case WindowsMessage.WM_POINTERACTIVATE: { //occurs when a pointer activates an inactive window. @@ -469,26 +463,6 @@ namespace Avalonia.Win32 _mouseDevice.Capture(null); return IntPtr.Zero; } - case WindowsMessage.WM_POINTERWHEEL: - { - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); - - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - var delta = new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta); - e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); - break; - } - case WindowsMessage.WM_POINTERHWHEEL: - { - GetDeviceInfo(wParam, out var device, out var info, ref timestamp); - - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - var delta = new Vector((ToInt32(wParam) >> 16) / wheelDelta, 0); - e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); - break; - } case WindowsMessage.DM_POINTERHITTEST: { //DM stands for direct manipulation. @@ -724,7 +698,7 @@ namespace Avalonia.Win32 _penDevice.Twist = penInfo.rotation; } - private void GetDeviceInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info, ref uint timestamp) + private void GetDevicePointerInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info, ref uint timestamp) { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); GetPointerType(pointerId, out var type); @@ -757,6 +731,12 @@ namespace Avalonia.Win32 private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) { + if (message == WindowsMessage.WM_POINTERUPDATE) + { + return info.pointerType == PointerInputType.PT_TOUCH + ? RawPointerEventType.TouchUpdate + : RawPointerEventType.Move; + } switch (info.pointerType) { case PointerInputType.PT_PEN: From c880572c5f711edb016a727f17003dabc2b1427f Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 19:27:17 +0300 Subject: [PATCH 09/42] Handle history pointer infos for touch and pen --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0266134199..0aaa9d4ce0 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -402,6 +402,62 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUPDATE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + + if (info.historyCount > 1) + { + if (info.pointerType == PointerInputType.PT_TOUCH) + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyCount = (int)info.historyCount; + var historyTouchInfos = new POINTER_TOUCH_INFO[historyCount]; + if (GetPointerTouchInfoHistory(pointerId, ref historyCount, historyTouchInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyTouchInfo = historyTouchInfos[i]; + var historyInfo = historyTouchInfo.pointerInfo; + var historyEventType = GetEventType(message, historyInfo); + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + var historyModifiers = GetInputModifiers(historyInfo.dwKeyStates); + var historyTimestamp = historyInfo.dwTime == 0 ? timestamp : historyInfo.dwTime; + Input?.Invoke(new RawTouchEventArgs(_touchDevice, historyTimestamp, _owner, + historyEventType, historyPoint, historyModifiers, historyInfo.pointerId)); + } + } + } + else if (info.pointerType == PointerInputType.PT_PEN) + { + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyCount = (int)info.historyCount; + var historyPenInfos = new POINTER_PEN_INFO[historyCount]; + if (GetPointerPenInfoHistory(pointerId, ref historyCount, historyPenInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyPenInfo = historyPenInfos[i]; + var historyInfo = historyPenInfo.pointerInfo; + var historyEventType = GetEventType(message, historyInfo); + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + var historyModifiers = GetInputModifiers(historyInfo.dwKeyStates); + var historyTimestamp = historyInfo.dwTime == 0 ? timestamp : historyInfo.dwTime; + + ApplyPenInfo(historyPenInfo); + Input?.Invoke(new RawPointerEventArgs(_penDevice, historyTimestamp, _owner, + historyEventType, historyPoint, historyModifiers)); + } + } + } + } + var eventType = GetEventType(message, info); var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -447,7 +503,7 @@ namespace Avalonia.Win32 var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); var val = (ToInt32(wParam) >> 16) / wheelDelta; - var delta = message == WindowsMessage.WM_POINTERHWHEEL ? new Vector(0, val) : new Vector(val, 0); + var delta = message == WindowsMessage.WM_POINTERWHEEL ? new Vector(0, val) : new Vector(val, 0); e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); break; } From 8cbd26ffdf6709e5e22487aeadc3d6b99f55af2c Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 19:49:32 +0300 Subject: [PATCH 10/42] Remove redundant changes, improve tab control pointer type check --- src/Avalonia.Controls/TabControl.cs | 6 ++++-- src/Avalonia.Input/IKeyboardDevice.cs | 2 -- src/Avalonia.Input/PointerPoint.cs | 2 +- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 2 -- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 306a9d3e6a..955b29f3f9 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -211,7 +211,8 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + (e.Pointer.Type == PointerType.Mouse || e.Pointer.Type == PointerType.Pen)) { e.Handled = UpdateSelectionFromEventSource(e.Source); } @@ -219,7 +220,8 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) + if (e.InitialPressMouseButton == MouseButton.Left && + (e.Pointer.Type == PointerType.Mouse || e.Pointer.Type == PointerType.Pen)) { var container = GetContainerFromEventSource(e.Source); if (container != null diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index b3ea7c5f4f..9506dc36fb 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -47,8 +47,6 @@ namespace Avalonia.Input MiddleMouseButton = 64, XButton1MouseButton = 128, XButton2MouseButton = 256, - BarrelPenButton = 512, - PenEraser = 1024, KeyboardMask = Alt | Control | Shift | Meta } diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 07ef130fcc..cdfcb8da3a 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -101,7 +101,7 @@ namespace Avalonia.Input RightButtonReleased, XButton1Released, XButton2Released, - Other, + Other } public static class PointerUpdateKindExtensions diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index c75eb69bfd..d6406121c7 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -22,8 +22,6 @@ namespace Avalonia.Input.Raw TouchUpdate, TouchEnd, TouchCancel, - BarrelUp, - BarrelDown, Magnify, Rotate, Swipe From 5127006d1b84880b03fbfca7a25889c376fcc8be Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 22:12:34 +0300 Subject: [PATCH 11/42] fix touch issues --- .../GestureRecognizers/ScrollGestureRecognizer.cs | 2 +- src/Avalonia.Input/PointerPoint.cs | 2 -- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 7 +++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index f8cedb636f..1d97fdfe53 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -101,7 +101,7 @@ namespace Avalonia.Input.GestureRecognizers if (_scrolling) { var vector = _trackedRootPoint - rootPoint; - var elapsed = _lastMoveTimestamp.HasValue ? + var elapsed = _lastMoveTimestamp.HasValue && _lastMoveTimestamp < e.Timestamp ? TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) : TimeSpan.Zero; diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index cdfcb8da3a..728bb1c579 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -45,8 +45,6 @@ namespace Avalonia.Input IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton); IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton); IsXButton2Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton2MouseButton); - IsBarrelButtonPressed = modifiers.HasAllFlags(RawInputModifiers.BarrelPenButton); - IsEraser = modifiers.HasAllFlags(RawInputModifiers.PenEraser); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0aaa9d4ce0..52981ac7ec 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -488,6 +488,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERLEAVE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + if (device is TouchDevice) + { + break; + } var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -798,8 +802,7 @@ namespace Avalonia.Win32 case PointerInputType.PT_PEN: return ToEventType(info.ButtonChangeType); case PointerInputType.PT_TOUCH: - if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED) || - !info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CONFIDENCE)) + if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED)) { return RawPointerEventType.TouchCancel; } From 8dee09b830462ed3e0c5d6197c44a95f2049a935 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 23 Jan 2022 23:13:39 +0300 Subject: [PATCH 12/42] Allow double click for pen device --- src/Avalonia.Input/PenDevice.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index c993e7fac4..aa4f89bb85 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reactive.Linq; using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Input @@ -12,6 +13,10 @@ namespace Avalonia.Input /// public class PenDevice : IPenDevice, IDisposable { + private int _clickCount; + private Rect _lastClickRect; + private ulong _lastClickTime; + private readonly Pointer _pointer; private bool _disposed; private PixelPoint? _position; @@ -182,8 +187,21 @@ namespace Avalonia.Input var source = GetSource(hit); if (source != null) { + var settings = AvaloniaLocator.Current.GetService(); + var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; + var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); + + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; + } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, 1); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; } From 600bb3198c775a76ac9034a5473fea795056be7b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 23 Jan 2022 21:20:02 -0500 Subject: [PATCH 13/42] Add pressure preview --- samples/ControlCatalog/Pages/PointersPage.cs | 266 ++++++++++++------- 1 file changed, 169 insertions(+), 97 deletions(-) diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs index 2901013cea..9702e8e3e7 100644 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ b/samples/ControlCatalog/Pages/PointersPage.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; @@ -25,7 +26,8 @@ public class PointersPage : Decorator Items = new[] { new TabItem() { Header = "Contacts", Content = new PointerContactsTab() }, - new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() } + new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() }, + new TabItem() { Header = "Pressure", Content = new PointerPressureTab() } } }; } @@ -148,7 +150,7 @@ public class PointersPage : Decorator { Children = { - new PointerCanvas(slider, status), + new PointerCanvas(slider, status, true), new Border { Background = Brushes.LightYellow, @@ -182,140 +184,210 @@ public class PointersPage : Decorator } }; } + } - class PointerCanvas : Control + public class PointerPressureTab : Decorator + { + public PointerPressureTab() { - private readonly Slider _slider; - private readonly TextBlock _status; - private int _events; - private Stopwatch _stopwatch = Stopwatch.StartNew(); - private Dictionary _pointers = new(); - class PointerPoints + this[TextBlock.ForegroundProperty] = Brushes.Black; + + var status = new TextBlock() + { + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + FontSize = 12 + }; + Child = new Grid { - struct CanvasPoint + Children = { - public IBrush Brush; - public Point Point; - public double Radius; + new PointerCanvas(null, status, false), + status } + }; + } + } + + class PointerCanvas : Control + { + private readonly Slider? _slider; + private readonly TextBlock _status; + private readonly bool _drawPoints; + private int _events; + private Stopwatch _stopwatch = Stopwatch.StartNew(); + private IDisposable? _statusUpdated; + private Dictionary _pointers = new(); + private PointerPointProperties? _lastProperties; + class PointerPoints + { + struct CanvasPoint + { + public IBrush Brush; + public Point Point; + public double Radius; + public double Pressure; + } + + readonly CanvasPoint[] _points = new CanvasPoint[1000]; + int _index; - readonly CanvasPoint[] _points = new CanvasPoint[1000]; - int _index; - - public void Render(DrawingContext context) + public void Render(DrawingContext context, bool drawPoints) + { + + CanvasPoint? prev = null; + for (var c = 0; c < _points.Length; c++) { - - CanvasPoint? prev = null; - for (var c = 0; c < _points.Length; c++) + var i = (c + _index) % _points.Length; + var pt = _points[i]; + var thickness = pt.Pressure == 0 ? 1 : (pt.Pressure / 1024) * 5; + + if (drawPoints) { - var i = (c + _index) % _points.Length; - var pt = _points[i]; if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null) - context.DrawLine(new Pen(Brushes.Black), prev.Value.Point, pt.Point); - prev = pt; + context.DrawLine(new Pen(Brushes.Black, thickness), prev.Value.Point, pt.Point); if (pt.Brush != null) context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius); - } - - } - - void AddPoint(Point pt, IBrush brush, double radius) - { - _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius }; - _index = (_index + 1) % _points.Length; - } - - public void HandleEvent(PointerEventArgs e, Visual v) - { - e.Handled = true; - if (e.RoutedEvent == PointerPressedEvent) - AddPoint(e.GetPosition(v), Brushes.Green, 10); - else if (e.RoutedEvent == PointerReleasedEvent) - AddPoint(e.GetPosition(v), Brushes.Red, 10); else { - var pts = e.GetIntermediatePoints(v); - for (var c = 0; c < pts.Count; c++) - { - var pt = pts[c]; - AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, - c == pts.Count - 1 ? 5 : 2); - } + if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null) + context.DrawLine(new Pen(Brushes.Black, thickness, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round), prev.Value.Point, pt.Point); } + prev = pt; } + } - - public PointerCanvas(Slider slider, TextBlock status) + + void AddPoint(Point pt, IBrush brush, double radius, float pressure) + { + _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure }; + _index = (_index + 1) % _points.Length; + } + + public void HandleEvent(PointerEventArgs e, Visual v) { - _slider = slider; - _status = status; - DispatcherTimer.Run(() => + e.Handled = true; + var currentPoint = e.GetCurrentPoint(v); + if (e.RoutedEvent == PointerPressedEvent) + AddPoint(currentPoint.Position, Brushes.Green, 10, currentPoint.Properties.Pressure); + else if (e.RoutedEvent == PointerReleasedEvent) + AddPoint(currentPoint.Position, Brushes.Red, 10, currentPoint.Properties.Pressure); + else { - if (_stopwatch.Elapsed.TotalSeconds > 1) + var pts = e.GetIntermediatePoints(v); + for (var c = 0; c < pts.Count; c++) { - _status.Text = "Events per second: " + (_events / _stopwatch.Elapsed.TotalSeconds); - _stopwatch.Restart(); - _events = 0; + var pt = pts[c]; + AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, + c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure); } - - return this.GetVisualRoot() != null; - }, TimeSpan.FromMilliseconds(10)); + } } + } + public PointerCanvas(Slider? slider, TextBlock status, bool drawPoints) + { + _slider = slider; + _status = status; + _drawPoints = drawPoints; + } - void HandleEvent(PointerEventArgs e) - { - _events++; - Thread.Sleep((int)_slider.Value); - InvalidateVisual(); + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); - if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) + _statusUpdated = DispatcherTimer.Run(() => + { + if (_stopwatch.Elapsed.TotalSeconds > 1) { - _pointers.Remove(e.Pointer.Id); - return; + _status.Text = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} +PointerUpdateKind: {_lastProperties?.PointerUpdateKind} +IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} +IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} +IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} +IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed} +IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed} +IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed} +IsEraser: {_lastProperties?.IsEraser} +IsInverted: {_lastProperties?.IsInverted} +Pressure: {_lastProperties?.Pressure} +XTilt: {_lastProperties?.XTilt} +YTilt: {_lastProperties?.YTilt} +Twist: {_lastProperties?.Twist}"; + _stopwatch.Restart(); + _events = 0; } - if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) - _pointers[e.Pointer.Id] = pt = new PointerPoints(); - pt.HandleEvent(e, this); - - - } - - public override void Render(DrawingContext context) + return true; + }, TimeSpan.FromMilliseconds(10)); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _statusUpdated?.Dispose(); + } + + void HandleEvent(PointerEventArgs e) + { + _events++; + if (_slider != null) { - context.FillRectangle(Brushes.White, Bounds); - foreach(var pt in _pointers.Values) - pt.Render(context); - base.Render(context); + Thread.Sleep((int)_slider.Value); } + InvalidateVisual(); - protected override void OnPointerPressed(PointerPressedEventArgs e) + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) { - if (e.ClickCount == 2) - { - _pointers.Clear(); - InvalidateVisual(); - return; - } - - HandleEvent(e); - base.OnPointerPressed(e); + _pointers.Remove(e.Pointer.Id); + return; } + + var lastPointer = e.GetCurrentPoint(this); + _lastProperties = lastPointer.Properties; - protected override void OnPointerMoved(PointerEventArgs e) + if (e.Pointer.Type != PointerType.Pen + || lastPointer.Properties.Pressure > 0) { - HandleEvent(e); - base.OnPointerMoved(e); + if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) + _pointers[e.Pointer.Id] = pt = new PointerPoints(); + pt.HandleEvent(e, this); } + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, Bounds); + foreach (var pt in _pointers.Values) + pt.Render(context, _drawPoints); + base.Render(context); + } - protected override void OnPointerReleased(PointerReleasedEventArgs e) + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.ClickCount == 2) { - HandleEvent(e); - base.OnPointerReleased(e); + _pointers.Clear(); + InvalidateVisual(); + return; } + + HandleEvent(e); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + HandleEvent(e); + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + HandleEvent(e); + base.OnPointerReleased(e); } - } } From bef72b3477574e8db361ee22f58cba6354ce435a Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 24 Jan 2022 11:39:13 +0300 Subject: [PATCH 14/42] fix some issues by requests --- src/Avalonia.Controls/TabControl.cs | 6 +-- .../ScrollGestureRecognizer.cs | 3 +- src/Avalonia.Input/MouseDevice.cs | 39 +++++++------------ src/Avalonia.Input/PenDevice.cs | 13 +++---- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 955b29f3f9..306a9d3e6a 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -211,8 +211,7 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && - (e.Pointer.Type == PointerType.Mouse || e.Pointer.Type == PointerType.Pen)) + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse) { e.Handled = UpdateSelectionFromEventSource(e.Source); } @@ -220,8 +219,7 @@ namespace Avalonia.Controls protected override void OnPointerReleased(PointerReleasedEventArgs e) { - if (e.InitialPressMouseButton == MouseButton.Left && - (e.Pointer.Type == PointerType.Mouse || e.Pointer.Type == PointerType.Pen)) + if (e.InitialPressMouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) { var container = GetContainerFromEventSource(e.Source); if (container != null diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index 1d97fdfe53..889b7e3b82 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -66,7 +66,8 @@ namespace Avalonia.Input.GestureRecognizers public void PointerPressed(PointerPressedEventArgs e) { - if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch) + if (e.Pointer.IsPrimary && + (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { EndGesture(); _tracking = e.Pointer; diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 087a806f77..38f63402ba 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -205,31 +205,20 @@ namespace Avalonia.Input PointerPointProperties CreateProperties(RawPointerEventArgs args) { - - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; - - return new PointerPointProperties(args.InputModifiers, kind); + return new PointerPointProperties(args.InputModifiers, args.Type switch + { + RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.MiddleButtonDown => PointerUpdateKind.MiddleButtonPressed, + RawPointerEventType.RightButtonDown => PointerUpdateKind.RightButtonPressed, + RawPointerEventType.XButton1Down => PointerUpdateKind.XButton1Pressed, + RawPointerEventType.XButton2Down => PointerUpdateKind.XButton2Pressed, + RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased, + RawPointerEventType.MiddleButtonUp => PointerUpdateKind.MiddleButtonReleased, + RawPointerEventType.RightButtonUp => PointerUpdateKind.RightButtonReleased, + RawPointerEventType.XButton1Up => PointerUpdateKind.XButton1Released, + RawPointerEventType.XButton2Up => PointerUpdateKind.XButton2Released, + _ => PointerUpdateKind.Other, + }); } private MouseButton _lastMouseDownButton; diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index aa4f89bb85..0fef462831 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -160,13 +160,12 @@ namespace Avalonia.Input private PointerPointProperties CreateProperties(RawPointerEventArgs args) { - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - + var kind = args.Type switch + { + RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased, + _ => PointerUpdateKind.Other, + }; return new PointerPointProperties(args.InputModifiers, kind, Twist, Pressure, XTilt, YTilt, IsEraser, IsInverted, IsBarrel); } From dc9fc4ab146432c1b07e0af563487c6b53a7a47d Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 24 Jan 2022 12:01:30 +0300 Subject: [PATCH 15/42] Handle Pointer History. Put into intermediate points. --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 52981ac7ec..7b5e42e490 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -402,6 +402,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUPDATE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + Point[]? intermediatePoints = null; if (info.historyCount > 1) { @@ -456,6 +457,23 @@ namespace Avalonia.Win32 } } } + else + { + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyCount = (int)info.historyCount; + var historyInfos = new POINTER_INFO[historyCount]; + if (GetPointerInfoHistory(pointerId, ref historyCount, historyInfos)) + { + //last info is the same as the current so skip it + intermediatePoints = new Point[historyCount - 1]; + for (int i = 0;i < historyCount - 1; i++) + { + var historyInfo = historyInfos[i]; + intermediatePoints[i] = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + } + } + } } var eventType = GetEventType(message, info); @@ -468,11 +486,17 @@ namespace Avalonia.Win32 { break; } - e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId); + e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId) + { + IntermediatePoints = intermediatePoints + }; } else { - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers); + e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers) + { + IntermediatePoints = intermediatePoints + }; } break; } From f04adadc89f04ba881c0c3811786706038059b7e Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 24 Jan 2022 12:52:58 +0300 Subject: [PATCH 16/42] Release mouse capture if pen is close to digitizer. Release pen capture if pen is out of range of digitizer. --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 7b5e42e490..dd961aec48 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -381,13 +381,18 @@ namespace Avalonia.Win32 break; } case WindowsMessage.WM_POINTERDEVICEINRANGE: - case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: { + _mouseDevice.Capture(null); //notifies about proximity of pointer device to the digitizer. //contains pointer id and proximity. //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdeviceinrange break; } + case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: + { + _penDevice.Capture(null); + break; + } case WindowsMessage.WM_NCPOINTERUPDATE: { //NC stands for non-client area - window header and window border @@ -545,6 +550,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERCAPTURECHANGED: { _mouseDevice.Capture(null); + _penDevice.Capture(null); return IntPtr.Zero; } case WindowsMessage.DM_POINTERHITTEST: From 903685839645adf7aa55e718372b26797c75c25f Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 24 Jan 2022 17:02:22 +0300 Subject: [PATCH 17/42] fix MK_NONE --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index d71efcb074..0431808a43 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -225,7 +225,7 @@ namespace Avalonia.Win32.Interop [Flags] public enum ModifierKeys { - MK_NONE = 0x0001, + MK_NONE = 0x0000, MK_LBUTTON = 0x0001, MK_RBUTTON = 0x0002, From c646343beee6da4f8568751ffd56bdd2da1f92ff Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 20 Mar 2022 23:54:04 -0400 Subject: [PATCH 18/42] WIP on intermediate points win --- src/Avalonia.Input/PointerEventArgs.cs | 3 +- src/Avalonia.Input/PointerPoint.cs | 17 +++ src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 10 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 141 +++++++++++------- 4 files changed, 112 insertions(+), 59 deletions(-) diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 0604d09dc4..a30c1c51dc 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -128,7 +128,8 @@ namespace Avalonia.Input for (var c = 0; c < previousPoints.Count; c++) { var pt = previousPoints[c]; - points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), _properties); + var pointProperties = new PointerPointProperties(_properties, pt); + points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), pointProperties); } points[points.Length - 1] = GetCurrentPoint(relativeTo); diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 728bb1c579..ba69add7d8 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -84,6 +84,23 @@ namespace Avalonia.Input IsBarrelButtonPressed = isBarrel; } + internal PointerPointProperties(PointerPointProperties basedOn, Raw.RawPointerPoint rawPoint) + { + IsLeftButtonPressed = basedOn.IsLeftButtonPressed; + IsMiddleButtonPressed = basedOn.IsMiddleButtonPressed; + IsRightButtonPressed = basedOn.IsRightButtonPressed; + IsXButton1Pressed = basedOn.IsXButton1Pressed; + IsXButton2Pressed = basedOn.IsXButton2Pressed; + IsInverted = basedOn.IsInverted; + IsEraser = basedOn.IsEraser; + IsBarrelButtonPressed = basedOn.IsBarrelButtonPressed; + + Twist = rawPoint.Twist; + Pressure = rawPoint.Pressure; + XTilt = rawPoint.XTilt; + YTilt = rawPoint.YTilt; + } + public static PointerPointProperties None { get; } = new PointerPointProperties(); } diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index c157fa059c..cd99cdc23b 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -128,10 +128,16 @@ namespace Avalonia.Input.Raw /// Pointer position, in client DIPs. /// public Point Position { get; set; } - + + public float Twist { get; set; } + public float Pressure { get; set; } + public float XTilt { get; set; } + public float YTilt { get; set; } + + public RawPointerPoint() { - Position = default; + this = default; } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 1ef3cbaba2..5c17caa13b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -1,4 +1,6 @@ using System; +using System.Buffers; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Text; @@ -413,78 +415,105 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUPDATE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); - Point[]? intermediatePoints = null; + if (info.pointerType == PointerInputType.PT_TOUCH + && ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + var historyCount = (int)info.historyCount; + Lazy> intermediatePoints = null; if (info.historyCount > 1) { - if (info.pointerType == PointerInputType.PT_TOUCH) + intermediatePoints = new Lazy>(() => { - if (ShouldIgnoreTouchEmulatedMessage()) + var list = new List(historyCount - 1); + if (info.pointerType == PointerInputType.PT_TOUCH) { - break; - } - - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyCount = (int)info.historyCount; - var historyTouchInfos = new POINTER_TOUCH_INFO[historyCount]; - if (GetPointerTouchInfoHistory(pointerId, ref historyCount, historyTouchInfos)) - { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyTouchInfos = ArrayPool.Shared.Rent(historyCount); + try + { + if (GetPointerTouchInfoHistory(pointerId, ref historyCount, historyTouchInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyTouchInfo = historyTouchInfos[i]; + var historyInfo = historyTouchInfo.pointerInfo; + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + list.Add(new RawPointerPoint + { + Position = historyPoint, + }); + } + } + } + finally { - var historyTouchInfo = historyTouchInfos[i]; - var historyInfo = historyTouchInfo.pointerInfo; - var historyEventType = GetEventType(message, historyInfo); - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - var historyModifiers = GetInputModifiers(historyInfo.dwKeyStates); - var historyTimestamp = historyInfo.dwTime == 0 ? timestamp : historyInfo.dwTime; - Input?.Invoke(new RawTouchEventArgs(_touchDevice, historyTimestamp, _owner, - historyEventType, historyPoint, historyModifiers, historyInfo.pointerId)); + ArrayPool.Shared.Return(historyTouchInfos); } } - } - else if (info.pointerType == PointerInputType.PT_PEN) - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyCount = (int)info.historyCount; - var historyPenInfos = new POINTER_PEN_INFO[historyCount]; - if (GetPointerPenInfoHistory(pointerId, ref historyCount, historyPenInfos)) + else if (info.pointerType == PointerInputType.PT_PEN) { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyPenInfos = ArrayPool.Shared.Rent(historyCount); + try { - var historyPenInfo = historyPenInfos[i]; - var historyInfo = historyPenInfo.pointerInfo; - var historyEventType = GetEventType(message, historyInfo); - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - var historyModifiers = GetInputModifiers(historyInfo.dwKeyStates); - var historyTimestamp = historyInfo.dwTime == 0 ? timestamp : historyInfo.dwTime; - - ApplyPenInfo(historyPenInfo); - Input?.Invoke(new RawPointerEventArgs(_penDevice, historyTimestamp, _owner, - historyEventType, historyPoint, historyModifiers)); + if (GetPointerPenInfoHistory(pointerId, ref historyCount, historyPenInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyPenInfo = historyPenInfos[i]; + var historyInfo = historyPenInfo.pointerInfo; + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + list.Add(new RawPointerPoint + { + Position = historyPoint, + Pressure = historyPenInfo.pressure, + Twist = historyPenInfo.rotation, + XTilt = historyPenInfo.tiltX, + YTilt = historyPenInfo.tiltX + }); + } + } + } + finally + { + ArrayPool.Shared.Return(historyPenInfos); } } - } - else - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyCount = (int)info.historyCount; - var historyInfos = new POINTER_INFO[historyCount]; - if (GetPointerInfoHistory(pointerId, ref historyCount, historyInfos)) + else { - //last info is the same as the current so skip it - intermediatePoints = new Point[historyCount - 1]; - for (int i = 0;i < historyCount - 1; i++) + var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); + var historyInfos = ArrayPool.Shared.Rent(historyCount); + try + { + if (GetPointerInfoHistory(pointerId, ref historyCount, historyInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyInfo = historyInfos[i]; + var historyPoint = PointToClient(new PixelPoint( + historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + list.Add(new RawPointerPoint + { + Position = historyPoint + }); + } + } + } + finally { - var historyInfo = historyInfos[i]; - intermediatePoints[i] = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); + ArrayPool.Shared.Return(historyInfos); } } - } + return list; + }); } var eventType = GetEventType(message, info); From d8f7dc5d1f2200a114404f9ed9cc7f34a58d0091 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 24 Mar 2022 20:28:56 -0400 Subject: [PATCH 19/42] Make EnableWmPointerEvents optional --- src/Windows/Avalonia.Win32/Win32Platform.cs | 9 +++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 66 +++++++++++++------ src/Windows/Avalonia.Win32/WindowImpl.cs | 14 ++-- 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 5cfbab40e4..25a6717122 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -63,8 +63,17 @@ namespace Avalonia /// /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. /// + [Obsolete("Multitouch is always enabled")] public bool? EnableMultitouch { get; set; } = true; + /// + /// Enables Win8+ WM_POINTER events support. The default value is false. + /// + /// + /// Required for extended Pen and Touch support. + /// + public bool? EnableWmPointerEvents { get; set; } = false; + /// /// Embeds popups to the window when set to true. The default value is false. /// diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 5c17caa13b..d52115425f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -174,7 +174,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -207,7 +207,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -240,7 +240,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEMOVE: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -275,7 +275,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEWHEEL: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -291,7 +291,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEHWHEEL: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -307,7 +307,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSELEAVE: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -327,7 +327,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -351,7 +351,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_TOUCH: { - if (Win8Plus) + if (_wmPointerEnabled) { break; } @@ -384,12 +384,20 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERDEVICECHANGE: { + if (!_wmPointerEnabled) + { + break; + } //notifies about changes in the settings of a monitor that has a digitizer attached to it. //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdevicechange break; } case WindowsMessage.WM_POINTERDEVICEINRANGE: { + if (!_wmPointerEnabled) + { + break; + } _mouseDevice.Capture(null); //notifies about proximity of pointer device to the digitizer. //contains pointer id and proximity. @@ -398,11 +406,19 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: { + if (!_wmPointerEnabled) + { + break; + } _penDevice.Capture(null); break; } case WindowsMessage.WM_NCPOINTERUPDATE: { + if (!_wmPointerEnabled) + { + break; + } //NC stands for non-client area - window header and window border //As I found above in an old message handling - we dont need to handle NC pointer move/updates. //All we need is pointer down and up. So this is skipped for now. @@ -415,8 +431,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUPDATE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); - if (info.pointerType == PointerInputType.PT_TOUCH - && ShouldIgnoreTouchEmulatedMessage()) + if (!_wmPointerEnabled) { break; } @@ -522,10 +537,6 @@ namespace Avalonia.Win32 if (device is TouchDevice) { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId) { IntermediatePoints = intermediatePoints @@ -542,6 +553,10 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERENTER: { + if (!_wmPointerEnabled) + { + break; + } //this is not handled by WM_MOUSEENTER so I think there is no need to handle this too. //but we can detect a new pointer by this message and calling IS_POINTER_NEW_WPARAM @@ -552,6 +567,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERLEAVE: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + if (!_wmPointerEnabled) + { + break; + } if (device is TouchDevice) { break; @@ -567,6 +586,10 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERHWHEEL: { GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); + if (!_wmPointerEnabled) + { + break; + } var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); var modifiers = GetInputModifiers(info.dwKeyStates); @@ -577,6 +600,10 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERACTIVATE: { + if (!_wmPointerEnabled) + { + break; + } //occurs when a pointer activates an inactive window. //we should handle this and return PA_ACTIVATE or PA_NOACTIVATE //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointeractivate @@ -584,6 +611,10 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_POINTERCAPTURECHANGED: { + if (!_wmPointerEnabled) + { + break; + } _mouseDevice.Capture(null); _penDevice.Capture(null); return IntPtr.Zero; @@ -913,8 +944,6 @@ namespace Avalonia.Win32 }; } - public readonly bool Win8Plus = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; - private void UpdateInputMethod(IntPtr hkl) { // note: for non-ime language, also create it so that emoji panel tracks cursor @@ -951,10 +980,7 @@ namespace Avalonia.Win32 private bool ShouldIgnoreTouchEmulatedMessage() { - if (!_multitouch) - { - return false; - } + // Note: GetMessageExtraInfo doesn't work with WM_POINTER events. // MI_WP_SIGNATURE // https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index becbbe7561..53dde352f7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -75,7 +75,7 @@ namespace Avalonia.Win32 private WndProc _wndProcDelegate; private string _className; private IntPtr _hwnd; - private bool _multitouch; + private bool _wmPointerEnabled; private IInputRoot _owner; private WindowProperties _windowProperties; private bool _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk @@ -128,7 +128,10 @@ namespace Avalonia.Win32 egl.Display is AngleWin32EglDisplay angleDisplay && angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; - if (Win8Plus && !IsMouseInPointerEnabled()) + _wmPointerEnabled = Win32Platform.Options.EnableWmPointerEvents + ?? Win32Platform.WindowsVersion >= PlatformConstants.Windows8; + + if (_wmPointerEnabled && !IsMouseInPointerEnabled()) { EnableMouseInPointer(true); } @@ -795,12 +798,7 @@ namespace Avalonia.Win32 Handle = new WindowImplPlatformHandle(this); - _multitouch = Win32Platform.Options.EnableMultitouch ?? true; - - if (_multitouch) - { - RegisterTouchWindow(_hwnd, 0); - } + RegisterTouchWindow(_hwnd, 0); if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) { From b599f295f6b94d01bec4d267bc6759da1d69833c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 01:10:45 -0400 Subject: [PATCH 20/42] Add RawPointerId to the RawPointerEventArgs --- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 5 ++++ src/Avalonia.Input/Raw/RawTouchEventArgs.cs | 17 ++++++++++--- src/Avalonia.Input/TouchDevice.cs | 13 +++++----- src/Shared/RawEventGrouping.cs | 12 ++++------ .../TouchDeviceTests.cs | 24 ++++++++++++------- 5 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 58ea076379..1faa20fbf1 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -87,6 +87,11 @@ namespace Avalonia.Input.Raw InputModifiers = inputModifiers; } + /// + /// Gets the raw pointer identifier. + /// + public long RawPointerId { get; set; } + /// /// Gets the pointer properties and position, in client DIPs. /// diff --git a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs index 020b40e55b..6706a45f48 100644 --- a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs @@ -1,15 +1,26 @@ +using System; + namespace Avalonia.Input.Raw { public class RawTouchEventArgs : RawPointerEventArgs { public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, RawPointerEventType type, Point position, RawInputModifiers inputModifiers, - long touchPointId) + long rawPointerId) : base(device, timestamp, root, type, position, inputModifiers) { - TouchPointId = touchPointId; + RawPointerId = rawPointerId; + } + + public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, + RawPointerEventType type, RawPointerPoint point, RawInputModifiers inputModifiers, + long rawPointerId) + : base(device, timestamp, root, type, point, inputModifiers) + { + RawPointerId = rawPointerId; } - public long TouchPointId { get; set; } + [Obsolete("Use RawPointerId")] + public long TouchPointId { get => RawPointerId; set => RawPointerId = value; } } } diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 54dcc4051e..e914d860fd 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -40,14 +40,14 @@ namespace Avalonia.Input { if (ev.Handled || _disposed) return; - var args = (RawTouchEventArgs)ev; - if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) + var args = (RawPointerEventArgs)ev; + if (!_pointers.TryGetValue(args.RawPointerId, out var pointer)) { if (args.Type == RawPointerEventType.TouchEnd) return; var hit = args.InputHitTestResult; - _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), + _pointers[args.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Touch, _pointers.Count == 0); pointer.Capture(hit); } @@ -88,7 +88,7 @@ namespace Avalonia.Input if (args.Type == RawPointerEventType.TouchEnd) { - _pointers.Remove(args.TouchPointId); + _pointers.Remove(args.RawPointerId); using (pointer) { target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, @@ -101,7 +101,7 @@ namespace Avalonia.Input if (args.Type == RawPointerEventType.TouchCancel) { - _pointers.Remove(args.TouchPointId); + _pointers.Remove(args.RawPointerId); using (pointer) pointer.Capture(null); _lastPointer = null; @@ -129,8 +129,7 @@ namespace Avalonia.Input public IPointer? TryGetPointer(RawPointerEventArgs ev) { - return ev is RawTouchEventArgs args - && _pointers.TryGetValue(args.TouchPointId, out var pointer) + return _pointers.TryGetValue(ev.RawPointerId, out var pointer) ? pointer : null; } diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs index 084593ffc6..966744888c 100644 --- a/src/Shared/RawEventGrouping.cs +++ b/src/Shared/RawEventGrouping.cs @@ -2,10 +2,8 @@ using System; using System.Collections.Generic; using Avalonia.Collections.Pooled; -using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Threading; -using JetBrains.Annotations; namespace Avalonia; @@ -19,7 +17,7 @@ internal class RawEventGrouper : IDisposable private readonly Action _eventCallback; private readonly Queue _inputQueue = new(); private readonly Action _dispatchFromQueue; - readonly Dictionary _lastTouchPoints = new(); + readonly Dictionary _lastTouchPoints = new(); RawInputEventArgs? _lastEvent; public RawEventGrouper(Action eventCallback) @@ -49,7 +47,7 @@ internal class RawEventGrouper : IDisposable _lastEvent = null; if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate) - _lastTouchPoints.Remove(touchUpdate.TouchPointId); + _lastTouchPoints.Remove(touchUpdate.RawPointerId); _eventCallback?.Invoke(ev); @@ -88,11 +86,11 @@ internal class RawEventGrouper : IDisposable { if (args is RawTouchEventArgs touchEvent) { - if (_lastTouchPoints.TryGetValue(touchEvent.TouchPointId, out var lastTouchEvent)) + if (_lastTouchPoints.TryGetValue(touchEvent.RawPointerId, out var lastTouchEvent)) MergeEvents(lastTouchEvent, touchEvent); else { - _lastTouchPoints[touchEvent.TouchPointId] = touchEvent; + _lastTouchPoints[touchEvent.RawPointerId] = touchEvent; AddToQueue(touchEvent); } } @@ -105,7 +103,7 @@ internal class RawEventGrouper : IDisposable { _lastTouchPoints.Clear(); if (args is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchEvent) - _lastTouchPoints[touchEvent.TouchPointId] = touchEvent; + _lastTouchPoints[touchEvent.RawPointerId] = touchEvent; } AddToQueue(args); } diff --git a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs index 80c5a45c1a..7b7d547346 100644 --- a/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/TouchDeviceTests.cs @@ -219,30 +219,36 @@ namespace Avalonia.Input.UnitTests { for (int i = 0; i < touchPointIds.Length; i++) { - inputManager.ProcessInput(new RawTouchEventArgs(device, 0, + inputManager.ProcessInput(new RawPointerEventArgs(device, 0, root, type, new Point(0, 0), - RawInputModifiers.None, - touchPointIds[i])); + RawInputModifiers.None) + { + RawPointerId = touchPointIds[i] + }); } } private static void TapOnce(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0) { - inputManager.ProcessInput(new RawTouchEventArgs(device, timestamp, + inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp, root, RawPointerEventType.TouchBegin, new Point(0, 0), - RawInputModifiers.None, - touchPointId)); - inputManager.ProcessInput(new RawTouchEventArgs(device, timestamp, + RawInputModifiers.None) + { + RawPointerId = touchPointId + }); + inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp, root, RawPointerEventType.TouchEnd, new Point(0, 0), - RawInputModifiers.None, - touchPointId)); + RawInputModifiers.None) + { + RawPointerId = touchPointId + }); } } } From afb828d4ff3aac67f122b2badd6d5c7399c9081b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 04:00:29 -0400 Subject: [PATCH 21/42] Move IntermediatePoints creation and update RawInputModifiers --- src/Avalonia.Input/IKeyboardDevice.cs | 7 +- src/Avalonia.Input/PenDevice.cs | 404 ++++-------------- src/Avalonia.Input/PointerPoint.cs | 12 +- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 4 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 379 ++++++++-------- src/Windows/Avalonia.Win32/WindowImpl.cs | 7 + 6 files changed, 278 insertions(+), 535 deletions(-) diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index d0e84e5ad0..1b9a056272 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -42,12 +42,17 @@ namespace Avalonia.Input Control = 2, Shift = 4, Meta = 8, + LeftMouseButton = 16, RightMouseButton = 32, MiddleMouseButton = 64, XButton1MouseButton = 128, XButton2MouseButton = 256, - KeyboardMask = Alt | Control | Shift | Meta + KeyboardMask = Alt | Control | Shift | Meta, + + PenInverted = 512, + PenEraser = 1024, + PenBarrelButton = 2048 } public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged diff --git a/src/Avalonia.Input/PenDevice.cs b/src/Avalonia.Input/PenDevice.cs index 0fef462831..d22b48562c 100644 --- a/src/Avalonia.Input/PenDevice.cs +++ b/src/Avalonia.Input/PenDevice.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Input.Raw; -using Avalonia.Interactivity; using Avalonia.Platform; using Avalonia.VisualTree; @@ -13,72 +13,14 @@ namespace Avalonia.Input /// public class PenDevice : IPenDevice, IDisposable { + private readonly Dictionary _pointers = new(); + private readonly Dictionary _lastPositions = new(); private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; + private MouseButton _lastMouseDownButton; - private readonly Pointer _pointer; private bool _disposed; - private PixelPoint? _position; - - public PenDevice(Pointer? pointer = null) - { - _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Pen, true); - } - - /// - /// Gets the control that is currently capturing by the mouse, if any. - /// - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. To set the mouse capture, call the - /// method. - /// - [Obsolete("Use IPointer instead")] - public IInputElement? Captured => _pointer.Captured; - - public bool IsEraser { get; set; } - public bool IsInverted { get; set; } - public bool IsBarrel { get; set; } - public int XTilt { get; set; } - public int YTilt { get; set; } - public uint Pressure { get; set; } - public uint Twist { get; set; } - - /// - /// Captures mouse input to the specified control. - /// - /// The control. - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. The current mouse capture control is exposed - /// by the property. - /// - public void Capture(IInputElement? control) - { - _pointer.Capture(control); - } - - /// - /// Gets the mouse position relative to a control. - /// - /// The control. - /// The mouse position in the control's coordinates. - public Point GetPosition(IVisual relativeTo) - { - relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); - - if (relativeTo.VisualRoot == null) - { - throw new InvalidOperationException("Control is not attached to visual tree."); - } - -#pragma warning disable CS0618 // Type or member is obsolete - var rootPoint = relativeTo.VisualRoot.PointToClient(_position ?? new PixelPoint(-1, -1)); -#pragma warning restore CS0618 // Type or member is obsolete - var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform!.Value; - } public void ProcessRawEvent(RawInputEventArgs e) { @@ -86,151 +28,93 @@ namespace Avalonia.Input ProcessRawEvent(margs); } - public void TopLevelClosed(IInputRoot root) + private void ProcessRawEvent(RawPointerEventArgs e) { - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - } + e = e ?? throw new ArgumentNullException(nameof(e)); - public void SceneInvalidated(IInputRoot root, Rect rect) - { - // Pointer is outside of the target area - if (_position == null ) + if (!_pointers.TryGetValue(e.RawPointerId, out var pointer)) { - if (root.PointerOverElement != null) - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - return; - } - - - var clientPoint = root.PointToClient(_position.Value); + if (e.Type == RawPointerEventType.LeftButtonUp + || e.Type == RawPointerEventType.TouchEnd) + return; - if (rect.Contains(clientPoint)) - { - if (_pointer.Captured == null) - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, - PointerPointProperties.None, KeyModifiers.None); - } - else - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, - PointerPointProperties.None, KeyModifiers.None); - } + _pointers[e.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(), + PointerType.Pen, _pointers.Count == 0); } - } - - private void ProcessRawEvent(RawPointerEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - var pen = (PenDevice)e.Device; - if(pen._disposed) - return; + _lastPositions[e.RawPointerId] = e.Root.PointToScreen(e.Position); + + var props = new PointerPointProperties(e.InputModifiers, e.Type.ToUpdateKind(), + e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt); + var keyModifiers = e.InputModifiers.ToKeyModifiers(); - _position = e.Root.PointToScreen(e.Position); - var props = CreateProperties(e); - var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); + bool shouldReleasePointer = false; switch (e.Type) { case RawPointerEventType.LeaveWindow: - LeaveWindow(pen, e.Timestamp, e.Root, props, keyModifiers); + shouldReleasePointer = true; break; case RawPointerEventType.LeftButtonDown: - e.Handled = PenDown(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); + e.Handled = PenDown(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.LeftButtonUp: - e.Handled = PenUp(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); + e.Handled = PenUp(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Move: - e.Handled = PenMove(pen, e.Timestamp, e.Root, e.Position, props, keyModifiers); + e.Handled = PenMove(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult, e.IntermediatePoints); break; } - } - - private void LeaveWindow(IPenDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - _position = null; - ClearPointerOver(this, timestamp, root, properties, inputModifiers); - } - - private PointerPointProperties CreateProperties(RawPointerEventArgs args) - { - var kind = args.Type switch + if (shouldReleasePointer) { - RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed, - RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased, - _ => PointerUpdateKind.Other, - }; - return new PointerPointProperties(args.InputModifiers, kind, - Twist, Pressure, XTilt, YTilt, IsEraser, IsInverted, IsBarrel); + pointer.Dispose(); + _pointers.Remove(e.RawPointerId); + _lastPositions.Remove(e.RawPointerId); + } } - private MouseButton _lastMouseDownButton; - private bool PenDown(IPenDevice device, ulong timestamp, IInputElement root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) + private bool PenDown(Pointer pointer, ulong timestamp, + IInputElement root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); + var source = pointer.Captured ?? hitTest; - if (hit != null) + if (source != null) { - _pointer.Capture(hit); - var source = GetSource(hit); - if (source != null) - { - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; - var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) - { - _clickCount = 0; - } + pointer.Capture(source); + var settings = AvaloniaLocator.Current.GetService(); + var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; + var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); - _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); - source.RaiseEvent(e); - return e.Handled; + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); + var e = new PointerPressedEventArgs(source, pointer, root, p, timestamp, properties, inputModifiers, _clickCount); + source.RaiseEvent(e); + return e.Handled; } return false; } - private bool PenMove(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers) + private bool PenMove(Pointer pointer, ulong timestamp, + IInputRoot root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest, + Lazy?>? intermediatePoints) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - IInputElement? source; - - if (_pointer.Captured == null) - { - source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); - } - else - { - SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); - source = _pointer.Captured; - } + var source = pointer.Captured ?? hitTest; - if (source is object) + if (source is not null) { - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers); + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, pointer, root, + p, timestamp, properties, inputModifiers, intermediatePoints); source.RaiseEvent(e); return e.Handled; @@ -239,176 +123,52 @@ namespace Avalonia.Input return false; } - private bool PenUp(IPenDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - KeyModifiers inputModifiers) + private bool PenUp(Pointer pointer, ulong timestamp, + IInputElement root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); + var source = pointer.Captured ?? hitTest; if (source is not null) { - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + var e = new PointerReleasedEventArgs(source, pointer, root, p, timestamp, properties, inputModifiers, _lastMouseDownButton); source?.RaiseEvent(e); - _pointer.Capture(null); + pointer.Capture(null); return e.Handled; } return false; } - private IInteractive? GetSource(IVisual? hit) - { - if (hit is null) - return null; - - return _pointer.Captured ?? - (hit as IInteractive) ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } - - private IInputElement? HitTest(IInputElement root, Point p) - { - root = root ?? throw new ArgumentNullException(nameof(root)); - - return _pointer.Captured ?? root.InputHitTest(p); - } - - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, properties, inputModifiers); - } - - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); - - if (element!=null && !element.IsAttachedToVisualTree) - { - // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) - ClearChildrenPointerOver(e, root,true); - } - while (element != null) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - element = (IInputElement?)element.VisualParent; - } - - root.PointerOverElement = null; - } - - private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) - { - foreach (IInputElement el in element.VisualChildren) - { - if (el.IsPointerOver) - { - ClearChildrenPointerOver(e, el, true); - break; - } - } - if(clearRoot) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - } - } - - private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) + public void Dispose() { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.InputHitTest(p); - - if (element != root.PointerOverElement) - { - if (element != null) - { - SetPointerOver(device, timestamp, root, element, properties, inputModifiers); - } - else - { - ClearPointerOver(device, timestamp, root, properties, inputModifiers); - } - } - - return element; + if (_disposed) + return; + var values = _pointers.Values.ToList(); + _pointers.Clear(); + _disposed = true; + foreach (var p in values) + p.Dispose(); } - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - element = element ?? throw new ArgumentNullException(nameof(element)); + [Obsolete] + IInputElement? IPointerDevice.Captured => _pointers.Values + .FirstOrDefault(p => p.IsPrimary)?.Captured; - IInputElement? branch = null; + [Obsolete] + void IPointerDevice.Capture(IInputElement? control) => _pointers.Values + .FirstOrDefault(p => p.IsPrimary)?.Capture(control); - IInputElement? el = element; + [Obsolete] + Point IPointerDevice.GetPosition(IVisual relativeTo) => new Point(-1, -1); - while (el != null) - { - if (el.IsPointerOver) - { - branch = el; - break; - } - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement; - - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); - if (el!=null && branch!=null && !el.IsAttachedToVisualTree) - { - ClearChildrenPointerOver(e,branch,false); - } - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement = element; - e.RoutedEvent = InputElement.PointerEnterEvent; - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - } - - public void Dispose() + public IPointer? TryGetPointer(RawPointerEventArgs ev) { - _disposed = true; - _pointer?.Dispose(); + return _pointers.TryGetValue(ev.RawPointerId, out var pointer) + ? pointer + : null; } } } diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index ba69add7d8..c704aa28c6 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -1,3 +1,5 @@ +using Avalonia.Input.Raw; + namespace Avalonia.Input { public sealed class PointerPoint @@ -45,6 +47,9 @@ namespace Avalonia.Input IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton); IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton); IsXButton2Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton2MouseButton); + IsInverted = modifiers.HasAllFlags(RawInputModifiers.PenInverted); + IsEraser = modifiers.HasAllFlags(RawInputModifiers.PenEraser); + IsBarrelButtonPressed = modifiers.HasAllFlags(RawInputModifiers.PenBarrelButton); // The underlying input source might be reporting the previous state, // so make sure that we reflect the current state @@ -72,19 +77,16 @@ namespace Avalonia.Input } public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind, - float twist, float pressure, float xTilt, float yTilt, bool isEraser, bool isInverted, bool isBarrel + float twist, float pressure, float xTilt, float yTilt ) : this (modifiers, kind) { Twist = twist; Pressure = pressure; XTilt = xTilt; YTilt = yTilt; - IsEraser = isEraser; - IsInverted = isInverted; - IsBarrelButtonPressed = isBarrel; } - internal PointerPointProperties(PointerPointProperties basedOn, Raw.RawPointerPoint rawPoint) + internal PointerPointProperties(PointerPointProperties basedOn, RawPointerPoint rawPoint) { IsLeftButtonPressed = basedOn.IsLeftButtonPressed; IsMiddleButtonPressed = basedOn.IsMiddleButtonPressed; diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 1faa20fbf1..0e4e0ed3e2 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -56,11 +56,12 @@ namespace Avalonia.Input.Raw Contract.Requires(device != null); Contract.Requires(root != null); + Point = new RawPointerPoint(); Position = position; Type = type; InputModifiers = inputModifiers; } - + /// /// Initializes a new instance of the class. /// @@ -145,6 +146,7 @@ namespace Avalonia.Input.Raw public RawPointerPoint() { this = default; + Pressure = 0.5f; } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index d52115425f..6619c60152 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -380,48 +380,6 @@ namespace Avalonia.Win32 return IntPtr.Zero; } - break; - } - case WindowsMessage.WM_POINTERDEVICECHANGE: - { - if (!_wmPointerEnabled) - { - break; - } - //notifies about changes in the settings of a monitor that has a digitizer attached to it. - //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdevicechange - break; - } - case WindowsMessage.WM_POINTERDEVICEINRANGE: - { - if (!_wmPointerEnabled) - { - break; - } - _mouseDevice.Capture(null); - //notifies about proximity of pointer device to the digitizer. - //contains pointer id and proximity. - //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdeviceinrange - break; - } - case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: - { - if (!_wmPointerEnabled) - { - break; - } - _penDevice.Capture(null); - break; - } - case WindowsMessage.WM_NCPOINTERUPDATE: - { - if (!_wmPointerEnabled) - { - break; - } - //NC stands for non-client area - window header and window border - //As I found above in an old message handling - we dont need to handle NC pointer move/updates. - //All we need is pointer down and up. So this is skipped for now. break; } case WindowsMessage.WM_NCPOINTERDOWN: @@ -430,194 +388,92 @@ namespace Avalonia.Win32 case WindowsMessage.WM_POINTERUP: case WindowsMessage.WM_POINTERUPDATE: { - GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); if (!_wmPointerEnabled) { break; } - - var historyCount = (int)info.historyCount; - Lazy> intermediatePoints = null; - if (info.historyCount > 1) - { - intermediatePoints = new Lazy>(() => - { - var list = new List(historyCount - 1); - if (info.pointerType == PointerInputType.PT_TOUCH) - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyTouchInfos = ArrayPool.Shared.Rent(historyCount); - try - { - if (GetPointerTouchInfoHistory(pointerId, ref historyCount, historyTouchInfos)) - { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) - { - var historyTouchInfo = historyTouchInfos[i]; - var historyInfo = historyTouchInfo.pointerInfo; - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - list.Add(new RawPointerPoint - { - Position = historyPoint, - }); - } - } - } - finally - { - ArrayPool.Shared.Return(historyTouchInfos); - } - } - else if (info.pointerType == PointerInputType.PT_PEN) - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyPenInfos = ArrayPool.Shared.Rent(historyCount); - try - { - if (GetPointerPenInfoHistory(pointerId, ref historyCount, historyPenInfos)) - { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) - { - var historyPenInfo = historyPenInfos[i]; - var historyInfo = historyPenInfo.pointerInfo; - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - list.Add(new RawPointerPoint - { - Position = historyPoint, - Pressure = historyPenInfo.pressure, - Twist = historyPenInfo.rotation, - XTilt = historyPenInfo.tiltX, - YTilt = historyPenInfo.tiltX - }); - } - } - } - finally - { - ArrayPool.Shared.Return(historyPenInfos); - } - } - else - { - var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); - var historyInfos = ArrayPool.Shared.Rent(historyCount); - try - { - if (GetPointerInfoHistory(pointerId, ref historyCount, historyInfos)) - { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) - { - var historyInfo = historyInfos[i]; - var historyPoint = PointToClient(new PixelPoint( - historyInfo.ptPixelLocationX, historyInfo.ptPixelLocationY)); - list.Add(new RawPointerPoint - { - Position = historyPoint - }); - } - } - } - finally - { - ArrayPool.Shared.Return(historyInfos); - } - } - return list; - }); - } - + GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); var eventType = GetEventType(message, info); - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - if (device is TouchDevice) - { - e = new RawTouchEventArgs(_touchDevice, timestamp, _owner, eventType, point, modifiers, info.pointerId) - { - IntermediatePoints = intermediatePoints - }; - } - else - { - e = new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers) - { - IntermediatePoints = intermediatePoints - }; - } - break; - } - case WindowsMessage.WM_POINTERENTER: - { - if (!_wmPointerEnabled) - { - break; - } - //this is not handled by WM_MOUSEENTER so I think there is no need to handle this too. - //but we can detect a new pointer by this message and calling IS_POINTER_NEW_WPARAM - - //note: by using a pen there can be a pointer leave or enter inside a window coords - //when you are just lift up the pen above the display + var args = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); + args.IntermediatePoints = CreateLazyIntermediatePoints(info); + e = args; break; } + case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE: case WindowsMessage.WM_POINTERLEAVE: + case WindowsMessage.WM_POINTERCAPTURECHANGED: { - GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); if (!_wmPointerEnabled) { break; } - if (device is TouchDevice) - { - break; - } - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); - - e = new RawPointerEventArgs( - device, timestamp, _owner, RawPointerEventType.LeaveWindow, point, modifiers); + GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); + var eventType = device is TouchDevice ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow; + e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); break; } case WindowsMessage.WM_POINTERWHEEL: case WindowsMessage.WM_POINTERHWHEEL: { - GetDevicePointerInfo(wParam, out var device, out var info, ref timestamp); if (!_wmPointerEnabled) { break; } + GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); - var point = PointToClient(new PixelPoint(info.ptPixelLocationX, info.ptPixelLocationY)); - var modifiers = GetInputModifiers(info.dwKeyStates); var val = (ToInt32(wParam) >> 16) / wheelDelta; var delta = message == WindowsMessage.WM_POINTERWHEEL ? new Vector(0, val) : new Vector(val, 0); - e = new RawMouseWheelEventArgs(device, timestamp, _owner, point, delta, modifiers); + e = new RawMouseWheelEventArgs(device, timestamp, _owner, point.Position, delta, modifiers) + { + RawPointerId = info.pointerId + }; break; } - case WindowsMessage.WM_POINTERACTIVATE: + case WindowsMessage.WM_POINTERDEVICEINRANGE: { if (!_wmPointerEnabled) { break; } + + // Do not generate events, but release mouse capture on any other device input. + GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); + if (device != _mouseDevice) + { + _mouseDevice.Capture(null); + return IntPtr.Zero; + } + break; + } + case WindowsMessage.WM_POINTERACTIVATE: + { //occurs when a pointer activates an inactive window. //we should handle this and return PA_ACTIVATE or PA_NOACTIVATE //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointeractivate break; } - case WindowsMessage.WM_POINTERCAPTURECHANGED: + case WindowsMessage.WM_POINTERDEVICECHANGE: { - if (!_wmPointerEnabled) - { - break; - } - _mouseDevice.Capture(null); - _penDevice.Capture(null); - return IntPtr.Zero; + //notifies about changes in the settings of a monitor that has a digitizer attached to it. + //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdevicechange + break; + } + case WindowsMessage.WM_NCPOINTERUPDATE: + { + //NC stands for non-client area - window header and window border + //As I found above in an old message handling - we dont need to handle NC pointer move/updates. + //All we need is pointer down and up. So this is skipped for now. + break; + } + case WindowsMessage.WM_POINTERENTER: + { + //this is not handled by WM_MOUSEENTER so I think there is no need to handle this too. + //but we can detect a new pointer by this message and calling IS_POINTER_NEW_WPARAM + + //note: by using a pen there can be a pointer leave or enter inside a window coords + //when you are just lift up the pen above the display + break; } case WindowsMessage.DM_POINTERHITTEST: { @@ -839,6 +695,11 @@ namespace Avalonia.Win32 _ignoreWmChar = e.Handled; } + if (s_intermediatePointsPooledList.Count > 0) + { + s_intermediatePointsPooledList.Dispose(); + } + if (e.Handled) { return IntPtr.Zero; @@ -851,40 +712,110 @@ namespace Avalonia.Win32 } } - private unsafe void ApplyPenInfo(POINTER_PEN_INFO penInfo) + private unsafe Lazy> CreateLazyIntermediatePoints(POINTER_INFO info) { - _penDevice.IsBarrel = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL); - _penDevice.IsEraser = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_ERASER); - _penDevice.IsInverted = penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_INVERTED); - - _penDevice.XTilt = penInfo.tiltX; - _penDevice.YTilt = penInfo.tiltY; - _penDevice.Pressure = penInfo.pressure; - _penDevice.Twist = penInfo.rotation; + // Limit history size with reasonable value. + // With sizeof(POINTER_TOUCH_INFO) * 100 we can get maximum 14400 bytes. + var historyCount = Math.Min((int)info.historyCount, MaxPointerHistorySize); + if (historyCount > 1) + { + return new Lazy>(() => + { + s_intermediatePointsPooledList.Clear(); + s_intermediatePointsPooledList.Capacity = historyCount; + if (info.pointerType == PointerInputType.PT_TOUCH) + { + if (GetPointerTouchInfoHistory(info.pointerId, ref historyCount, s_historyTouchInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyTouchInfo = s_historyTouchInfos[i]; + s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyTouchInfo)); + } + } + } + else if (info.pointerType == PointerInputType.PT_PEN) + { + if (GetPointerPenInfoHistory(info.pointerId, ref historyCount, s_historyPenInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyPenInfo = s_historyPenInfos[i]; + s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyPenInfo)); + } + } + } + else + { + // Currently Windows does not return history info for mouse input, but we handle it just for case. + if (GetPointerInfoHistory(info.pointerId, ref historyCount, s_historyInfos)) + { + //last info is the same as the current so skip it + for (int i = 0; i < historyCount - 1; i++) + { + var historyInfo = s_historyInfos[i]; + s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyInfo)); + } + } + } + return s_intermediatePointsPooledList; + }); + } + + return null; + } + + private RawPointerEventArgs CreatePointerArgs(IInputDevice device, ulong timestamp, RawPointerEventType eventType, RawPointerPoint point, RawInputModifiers modifiers, uint rawPointerId) + { + return device is TouchDevice + ? new RawTouchEventArgs(device, timestamp, _owner, eventType, point, modifiers, rawPointerId) + : new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers) + { + RawPointerId = rawPointerId + }; } - private void GetDevicePointerInfo(IntPtr wParam, out IInputDevice device, out POINTER_INFO info, ref uint timestamp) + private void GetDevicePointerInfo(IntPtr wParam, + out IPointerDevice device, out POINTER_INFO info, out RawPointerPoint point, + out RawInputModifiers modifiers, ref uint timestamp) { var pointerId = (uint)(ToInt32(wParam) & 0xFFFF); GetPointerType(pointerId, out var type); - //GetPointerCursorId(pointerId, out var cursorId); + + modifiers = default; + switch (type) { case PointerInputType.PT_PEN: device = _penDevice; GetPointerPenInfo(pointerId, out var penInfo); info = penInfo.pointerInfo; - - ApplyPenInfo(penInfo); + point = CreateRawPointerPoint(penInfo); + if (penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL)) + { + modifiers |= RawInputModifiers.PenBarrelButton; + } + if (penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_ERASER)) + { + modifiers |= RawInputModifiers.PenEraser; + } + if (penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_INVERTED)) + { + modifiers |= RawInputModifiers.PenInverted; + } break; case PointerInputType.PT_TOUCH: device = _touchDevice; GetPointerTouchInfo(pointerId, out var touchInfo); info = touchInfo.pointerInfo; + point = CreateRawPointerPoint(touchInfo); break; default: device = _mouseDevice; GetPointerInfo(pointerId, out info); + point = CreateRawPointerPoint(info); break; } @@ -892,6 +823,44 @@ namespace Avalonia.Win32 { timestamp = info.dwTime; } + + modifiers |= GetInputModifiers(info.dwKeyStates); + } + + private RawPointerPoint CreateRawPointerPoint(POINTER_INFO pointerInfo) + { + var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY)); + return new RawPointerPoint + { + Position = point + }; + } + private RawPointerPoint CreateRawPointerPoint(POINTER_TOUCH_INFO info) + { + var pointerInfo = info.pointerInfo; + var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY)); + return new RawPointerPoint + { + Position = point, + // POINTER_PEN_INFO.pressure is normalized to a range between 0 and 1024, with 512 as a default. + // But in our API we use range from 0.0 to 1.0. + Pressure = info.pressure / 1024f + }; + } + private RawPointerPoint CreateRawPointerPoint(POINTER_PEN_INFO info) + { + var pointerInfo = info.pointerInfo; + var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY)); + return new RawPointerPoint + { + Position = point, + // POINTER_PEN_INFO.pressure is normalized to a range between 0 and 1024, with 512 as a default. + // But in our API we use range from 0.0 to 1.0. + Pressure = info.pressure / 1024f, + Twist = info.rotation, + XTilt = info.tiltX, + YTilt = info.tiltX + }; } private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) @@ -904,8 +873,6 @@ namespace Avalonia.Win32 } switch (info.pointerType) { - case PointerInputType.PT_PEN: - return ToEventType(info.ButtonChangeType); case PointerInputType.PT_TOUCH: if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED)) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 64295f8925..e0f6348c23 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -21,6 +21,7 @@ using Avalonia.Win32.OpenGl; using Avalonia.Win32.WinRT; using Avalonia.Win32.WinRT.Composition; using static Avalonia.Win32.Interop.UnmanagedMethods; +using Avalonia.Collections.Pooled; namespace Avalonia.Win32 { @@ -95,6 +96,12 @@ namespace Avalonia.Win32 private uint _langid; private bool _ignoreWmChar; + private const int MaxPointerHistorySize = 512; + private readonly static PooledList s_intermediatePointsPooledList = new(); + private readonly static POINTER_TOUCH_INFO[] s_historyTouchInfos = new POINTER_TOUCH_INFO[MaxPointerHistorySize]; + private readonly static POINTER_PEN_INFO[] s_historyPenInfos = new POINTER_PEN_INFO[MaxPointerHistorySize]; + private readonly static POINTER_INFO[] s_historyInfos = new POINTER_INFO[MaxPointerHistorySize]; + public WindowImpl() { _touchDevice = new TouchDevice(); From 99e444f5f19a9312ac4eb99076084b18b92040e3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 04:01:07 -0400 Subject: [PATCH 22/42] Update documentation of pointer types --- src/Avalonia.Input/IPointer.cs | 40 +++++++++++- src/Avalonia.Input/PointerEventArgs.cs | 19 +++++- src/Avalonia.Input/PointerPoint.cs | 86 ++++++++++++++++++++++++-- 3 files changed, 138 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs index 361f3ac370..98f7c81f02 100644 --- a/src/Avalonia.Input/IPointer.cs +++ b/src/Avalonia.Input/IPointer.cs @@ -1,15 +1,53 @@ namespace Avalonia.Input { + /// + /// Identifies specific pointer generated by input device. + /// + /// + /// Some devices, for instance, touchscreen might generate a pointer on each physical contact. + /// public interface IPointer { + /// + /// Gets a unique identifier for the input pointer. + /// int Id { get; } + + /// + /// Captures pointer input to the specified control. + /// + /// The control. + /// + /// When an element captures the pointer, it receives pointer input whether the cursor is + /// within the control's bounds or not. The current pointer capture control is exposed + /// by the property. + /// void Capture(IInputElement? control); + + /// + /// Gets the control that is currently capturing by the pointer, if any. + /// + /// + /// When an element captures the pointer, it receives pointer input whether the cursor is + /// within the control's bounds or not. To set the pointer capture, call the + /// method. + /// IInputElement? Captured { get; } + + /// + /// Gets the pointer device type. + /// PointerType Type { get; } + + /// + /// Gets a value that indicates whether the input is from the primary pointer when multiple pointers are registered. + /// bool IsPrimary { get; } - } + /// + /// Enumerates pointer device types. + /// public enum PointerType { Mouse, diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 79335eb9fc..058c2f9cc1 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -67,7 +67,14 @@ namespace Avalonia.Input public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer; } + /// + /// Gets specific pointer generated by input device. + /// public IPointer Pointer { get; } + + /// + /// Gets the time when the input occurred. + /// public ulong Timestamp { get; } private IPointerDevice? _device; @@ -91,7 +98,10 @@ namespace Avalonia.Input return mods; } } - + + /// + /// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated. + /// public KeyModifiers KeyModifiers { get; } private Point GetPosition(Point pt, IVisual? relativeTo) @@ -102,7 +112,12 @@ namespace Avalonia.Input return pt; return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; } - + + /// + /// Gets the pointer position relative to a control. + /// + /// The control. + /// The pointer position in the control's coordinates. public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); [Obsolete("Use GetCurrentPoint")] diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index c704aa28c6..71145b5cb0 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -2,6 +2,9 @@ using Avalonia.Input.Raw; namespace Avalonia.Input { + /// + /// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact. + /// public sealed class PointerPoint { public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) @@ -10,34 +13,109 @@ namespace Avalonia.Input Position = position; Properties = properties; } + + /// + /// Gets specific pointer generated by input device. + /// public IPointer Pointer { get; } + + /// + /// Gets extended information about the input pointer. + /// public PointerPointProperties Properties { get; } + + /// + /// Gets the location of the pointer input in client coordinates. + /// public Point Position { get; } } + /// + /// Provides extended properties for a PointerPoint object. + /// public sealed class PointerPointProperties { + /// + /// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device. + /// public bool IsLeftButtonPressed { get; } + + /// + /// Gets a value that indicates whether the pointer input was triggered by the tertiary action mode of an input device. + /// public bool IsMiddleButtonPressed { get; } + + /// + /// Gets a value that indicates whether the pointer input was triggered by the secondary action mode (if supported) of an input device. + /// public bool IsRightButtonPressed { get; } + + /// + /// Gets a value that indicates whether the pointer input was triggered by the first extended mouse button (XButton1). + /// public bool IsXButton1Pressed { get; } + + /// + /// Gets a value that indicates whether the pointer input was triggered by the second extended mouse button (XButton2). + /// public bool IsXButton2Pressed { get; } + + /// + /// Gets a value that indicates whether the barrel button of the pen/stylus device is pressed. + /// public bool IsBarrelButtonPressed { get; } + + /// + /// Gets a value that indicates whether the input is from a pen eraser. + /// public bool IsEraser { get; } + + /// + /// Gets a value that indicates whether the digitizer pen is inverted. + /// public bool IsInverted { get; } + /// + /// Gets the clockwise rotation in degrees of a pen device around its own major axis (such as when the user spins the pen in their fingers). + /// + /// + /// A value between 0.0 and 359.0 in degrees of rotation. The default value is 0.0. + /// public float Twist { get; } - public float Pressure { get; } + + /// + /// Gets a value that indicates the force that the pointer device (typically a pen/stylus) exerts on the surface of the digitizer. + /// + /// + /// A value from 0 to 1.0. The default value is 0.5. + /// + public float Pressure { get; } = 0.5f; + + /// + /// Gets the plane angle between the Y-Z plane and the plane that contains the Y axis and the axis of the input device (typically a pen/stylus). + /// + /// + /// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted to the right of perpendicular, and between 0.0 and -90.0 when tilted to the left of perpendicular. The default value is 0.0. + /// public float XTilt { get; } - public float YTilt { get; } + /// + /// Gets the plane angle between the X-Z plane and the plane that contains the X axis and the axis of the input device (typically a pen/stylus). + /// + /// + /// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted towards the user, and between 0.0 and -90.0 when tilted away from the user. The default value is 0.0. + /// + public float YTilt { get; } + /// + /// Gets the kind of pointer state change. + /// public PointerUpdateKind PointerUpdateKind { get; } private PointerPointProperties() - { + { } - + public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) { PointerUpdateKind = kind; From bd2578d68359abcc44eb83118b907f74abaf3ae2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 04:31:57 -0400 Subject: [PATCH 23/42] Update control catalog pointers page --- samples/ControlCatalog.NetCore/Program.cs | 2 +- samples/ControlCatalog/MainView.xaml | 2 +- samples/ControlCatalog/Pages/PointerCanvas.cs | 221 ++++++++++ .../Pages/PointerContactsTab.cs | 109 +++++ samples/ControlCatalog/Pages/PointersPage.cs | 394 ------------------ .../ControlCatalog/Pages/PointersPage.xaml | 65 +++ .../ControlCatalog/Pages/PointersPage.xaml.cs | 76 ++++ 7 files changed, 473 insertions(+), 396 deletions(-) create mode 100644 samples/ControlCatalog/Pages/PointerCanvas.cs create mode 100644 samples/ControlCatalog/Pages/PointerContactsTab.cs delete mode 100644 samples/ControlCatalog/Pages/PointersPage.cs create mode 100644 samples/ControlCatalog/Pages/PointersPage.xaml create mode 100644 samples/ControlCatalog/Pages/PointersPage.xaml.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 4b81935452..08ac17d0c4 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -114,7 +114,7 @@ namespace ControlCatalog.NetCore }) .With(new Win32PlatformOptions { - EnableMultitouch = true + EnableWmPointerEvents = true }) .UseSkia() .UseManagedSystemDialogs() diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 85f278b5fa..c61ba231e0 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -106,7 +106,7 @@ - + diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs new file mode 100644 index 0000000000..b815a573f2 --- /dev/null +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -0,0 +1,221 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Threading; + +namespace ControlCatalog.Pages; + +public class PointerCanvas : Control +{ + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _events; + private IDisposable? _statusUpdated; + private Dictionary _pointers = new(); + private PointerPointProperties? _lastProperties; + class PointerPoints + { + struct CanvasPoint + { + public IBrush Brush; + public Point Point; + public double Radius; + public double? Pressure; + } + + readonly CanvasPoint[] _points = new CanvasPoint[1000]; + int _index; + + public void Render(DrawingContext context, bool drawPoints) + { + CanvasPoint? prev = null; + for (var c = 0; c < _points.Length; c++) + { + var i = (c + _index) % _points.Length; + var pt = _points[i]; + var pressure = (pt.Pressure ?? prev?.Pressure ?? 0.5); + var thickness = pressure * 10; + var radius = pressure * pt.Radius; + + if (drawPoints) + { + if (pt.Brush != null) + { + context.DrawEllipse(pt.Brush, null, pt.Point, radius, radius); + } + } + else + { + if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null + && prev.Value.Pressure != null && pt.Pressure != null) + { + var linePen = new Pen(Brushes.Black, thickness, null, PenLineCap.Round, PenLineJoin.Round); + context.DrawLine(linePen, prev.Value.Point, pt.Point); + } + } + prev = pt; + } + + } + + void AddPoint(Point pt, IBrush brush, double radius, float? pressure = null) + { + _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure }; + _index = (_index + 1) % _points.Length; + } + + public void HandleEvent(PointerEventArgs e, Visual v) + { + e.Handled = true; + var currentPoint = e.GetCurrentPoint(v); + if (e.RoutedEvent == PointerPressedEvent) + AddPoint(currentPoint.Position, Brushes.Green, 10); + else if (e.RoutedEvent == PointerReleasedEvent) + AddPoint(currentPoint.Position, Brushes.Red, 10); + else + { + var pts = e.GetIntermediatePoints(v); + for (var c = 0; c < pts.Count; c++) + { + var pt = pts[c]; + AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, + c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure); + } + } + } + } + + private int _threadSleep; + public static DirectProperty ThreadSleepProperty = + AvaloniaProperty.RegisterDirect(nameof(ThreadSleep), c => c.ThreadSleep, (c, v) => c.ThreadSleep = v); + + public int ThreadSleep + { + get => _threadSleep; + set => SetAndRaise(ThreadSleepProperty, ref _threadSleep, value); + } + + private bool _drawOnlyPoints; + public static DirectProperty DrawOnlyPointsProperty = + AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.DrawOnlyPoints, (c, v) => c.DrawOnlyPoints = v); + + public bool DrawOnlyPoints + { + get => _drawOnlyPoints; + set => SetAndRaise(DrawOnlyPointsProperty, ref _drawOnlyPoints, value); + } + + private string? _status; + public static DirectProperty StatusProperty = + AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v, + defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); + + public string? Status + { + get => _status; + set => SetAndRaise(StatusProperty, ref _status, value); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + _statusUpdated = DispatcherTimer.Run(() => + { + if (_stopwatch.Elapsed.TotalSeconds > 1) + { + Status = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} +PointerUpdateKind: {_lastProperties?.PointerUpdateKind} +IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} +IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} +IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} +IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed} +IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed} +IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed} +IsEraser: {_lastProperties?.IsEraser} +IsInverted: {_lastProperties?.IsInverted} +Pressure: {_lastProperties?.Pressure} +XTilt: {_lastProperties?.XTilt} +YTilt: {_lastProperties?.YTilt} +Twist: {_lastProperties?.Twist}"; + _stopwatch.Restart(); + _events = 0; + } + + return true; + }, TimeSpan.FromMilliseconds(10)); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _statusUpdated?.Dispose(); + } + + void HandleEvent(PointerEventArgs e) + { + _events++; + if (_threadSleep != 0) + { + Thread.Sleep(_threadSleep); + } + InvalidateVisual(); + + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) + { + _pointers.Remove(e.Pointer.Id); + return; + } + + var lastPointer = e.GetCurrentPoint(this); + _lastProperties = lastPointer.Properties; + + if (e.Pointer.Type != PointerType.Pen + || lastPointer.Properties.Pressure > 0) + { + if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) + _pointers[e.Pointer.Id] = pt = new PointerPoints(); + pt.HandleEvent(e, this); + } + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, Bounds); + foreach (var pt in _pointers.Values) + pt.Render(context, _drawOnlyPoints); + base.Render(context); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.ClickCount == 2) + { + _pointers.Clear(); + InvalidateVisual(); + return; + } + + HandleEvent(e); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + HandleEvent(e); + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + HandleEvent(e); + base.OnPointerReleased(e); + } +} diff --git a/samples/ControlCatalog/Pages/PointerContactsTab.cs b/samples/ControlCatalog/Pages/PointerContactsTab.cs new file mode 100644 index 0000000000..b6aabebf99 --- /dev/null +++ b/samples/ControlCatalog/Pages/PointerContactsTab.cs @@ -0,0 +1,109 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace ControlCatalog.Pages; + +public class PointerContactsTab : Control +{ + class PointerInfo + { + public Point Point { get; set; } + public Color Color { get; set; } + } + + private static Color[] AllColors = new[] + { + Colors.Aqua, + Colors.Beige, + Colors.Chartreuse, + Colors.Coral, + Colors.Fuchsia, + Colors.Crimson, + Colors.Lavender, + Colors.Orange, + Colors.Orchid, + Colors.ForestGreen, + Colors.SteelBlue, + Colors.PapayaWhip, + Colors.PaleVioletRed, + Colors.Goldenrod, + Colors.Maroon, + Colors.Moccasin, + Colors.Navy, + Colors.Wheat, + Colors.Violet, + Colors.Sienna, + Colors.Indigo, + Colors.Honeydew + }; + + private Dictionary _pointers = new Dictionary(); + + public PointerContactsTab() + { + ClipToBounds = true; + } + + void UpdatePointer(PointerEventArgs e) + { + if (!_pointers.TryGetValue(e.Pointer, out var info)) + { + if (e.RoutedEvent == PointerMovedEvent) + return; + var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray(); + var color = colors[new Random().Next(0, colors.Length - 1)]; + _pointers[e.Pointer] = info = new PointerInfo { Color = color }; + } + + info.Point = e.GetPosition(this); + InvalidateVisual(); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + UpdatePointer(e); + e.Pointer.Capture(this); + e.Handled = true; + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + UpdatePointer(e); + e.Handled = true; + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + _pointers.Remove(e.Pointer); + e.Handled = true; + InvalidateVisual(); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _pointers.Remove(e.Pointer); + InvalidateVisual(); + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size)); + foreach (var pt in _pointers.Values) + { + var brush = new ImmutableSolidColorBrush(pt.Color); + + context.DrawEllipse(brush, null, pt.Point, 75, 75); + } + } +} diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs deleted file mode 100644 index 0668c248c7..0000000000 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ /dev/null @@ -1,394 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Linq; -using System.Runtime.InteropServices; -using System.Threading; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Documents; -using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Threading; -using Avalonia.VisualTree; - -namespace ControlCatalog.Pages; - -public class PointersPage : Decorator -{ - public PointersPage() - { - Child = new TabControl - { - Items = new[] - { - new TabItem() { Header = "Contacts", Content = new PointerContactsTab() }, - new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() }, - new TabItem() { Header = "Pressure", Content = new PointerPressureTab() } - } - }; - } - - - class PointerContactsTab : Control - { - class PointerInfo - { - public Point Point { get; set; } - public Color Color { get; set; } - } - - private static Color[] AllColors = new[] - { - Colors.Aqua, - Colors.Beige, - Colors.Chartreuse, - Colors.Coral, - Colors.Fuchsia, - Colors.Crimson, - Colors.Lavender, - Colors.Orange, - Colors.Orchid, - Colors.ForestGreen, - Colors.SteelBlue, - Colors.PapayaWhip, - Colors.PaleVioletRed, - Colors.Goldenrod, - Colors.Maroon, - Colors.Moccasin, - Colors.Navy, - Colors.Wheat, - Colors.Violet, - Colors.Sienna, - Colors.Indigo, - Colors.Honeydew - }; - - private Dictionary _pointers = new Dictionary(); - - public PointerContactsTab() - { - ClipToBounds = true; - } - - void UpdatePointer(PointerEventArgs e) - { - if (!_pointers.TryGetValue(e.Pointer, out var info)) - { - if (e.RoutedEvent == PointerMovedEvent) - return; - var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray(); - var color = colors[new Random().Next(0, colors.Length - 1)]; - _pointers[e.Pointer] = info = new PointerInfo {Color = color}; - } - - info.Point = e.GetPosition(this); - InvalidateVisual(); - } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - UpdatePointer(e); - e.Pointer.Capture(this); - e.Handled = true; - base.OnPointerPressed(e); - } - - protected override void OnPointerMoved(PointerEventArgs e) - { - UpdatePointer(e); - e.Handled = true; - base.OnPointerMoved(e); - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - _pointers.Remove(e.Pointer); - e.Handled = true; - InvalidateVisual(); - } - - protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) - { - _pointers.Remove(e.Pointer); - InvalidateVisual(); - } - - public override void Render(DrawingContext context) - { - context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size)); - foreach (var pt in _pointers.Values) - { - var brush = new ImmutableSolidColorBrush(pt.Color); - - context.DrawEllipse(brush, null, pt.Point, 75, 75); - } - } - } - - public class PointerIntermediatePointsTab : Decorator - { - public PointerIntermediatePointsTab() - { - this[TextElement.ForegroundProperty] = Brushes.Black; - var slider = new Slider - { - Margin = new Thickness(5), - Minimum = 0, - Maximum = 500 - }; - - var status = new TextBlock() - { - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - }; - Child = new Grid - { - Children = - { - new PointerCanvas(slider, status, true), - new Border - { - Background = Brushes.LightYellow, - Child = new StackPanel - { - Children = - { - new StackPanel - { - Orientation = Orientation.Horizontal, - Children = - { - new TextBlock { Text = "Thread sleep:" }, - new TextBlock() - { - [!TextBlock.TextProperty] =slider.GetObservable(Slider.ValueProperty) - .Select(x=>x.ToString()).ToBinding() - } - } - }, - slider - } - }, - - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Top, - Width = 300, - Height = 60 - }, - status - } - }; - } - } - - public class PointerPressureTab : Decorator - { - public PointerPressureTab() - { - this[TextBlock.ForegroundProperty] = Brushes.Black; - - var status = new TextBlock() - { - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - FontSize = 12 - }; - Child = new Grid - { - Children = - { - new PointerCanvas(null, status, false), - status - } - }; - } - } - - class PointerCanvas : Control - { - private readonly Slider? _slider; - private readonly TextBlock _status; - private readonly bool _drawPoints; - private int _events; - private Stopwatch _stopwatch = Stopwatch.StartNew(); - private IDisposable? _statusUpdated; - private Dictionary _pointers = new(); - private PointerPointProperties? _lastProperties; - class PointerPoints - { - struct CanvasPoint - { - public IBrush Brush; - public Point Point; - public double Radius; - public double Pressure; - } - - readonly CanvasPoint[] _points = new CanvasPoint[1000]; - int _index; - - public void Render(DrawingContext context, bool drawPoints) - { - - CanvasPoint? prev = null; - for (var c = 0; c < _points.Length; c++) - { - var i = (c + _index) % _points.Length; - var pt = _points[i]; - var thickness = pt.Pressure == 0 ? 1 : (pt.Pressure / 1024) * 5; - - if (drawPoints) - { - if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null) - context.DrawLine(new Pen(Brushes.Black, thickness), prev.Value.Point, pt.Point); - if (pt.Brush != null) - context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius); - } - else - { - if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null) - context.DrawLine(new Pen(Brushes.Black, thickness, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round), prev.Value.Point, pt.Point); - } - prev = pt; - } - - } - - void AddPoint(Point pt, IBrush brush, double radius, float pressure) - { - _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure }; - _index = (_index + 1) % _points.Length; - } - - public void HandleEvent(PointerEventArgs e, Visual v) - { - e.Handled = true; - var currentPoint = e.GetCurrentPoint(v); - if (e.RoutedEvent == PointerPressedEvent) - AddPoint(currentPoint.Position, Brushes.Green, 10, currentPoint.Properties.Pressure); - else if (e.RoutedEvent == PointerReleasedEvent) - AddPoint(currentPoint.Position, Brushes.Red, 10, currentPoint.Properties.Pressure); - else - { - var pts = e.GetIntermediatePoints(v); - for (var c = 0; c < pts.Count; c++) - { - var pt = pts[c]; - AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, - c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure); - } - } - } - } - - public PointerCanvas(Slider? slider, TextBlock status, bool drawPoints) - { - _slider = slider; - _status = status; - _drawPoints = drawPoints; - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - _statusUpdated = DispatcherTimer.Run(() => - { - if (_stopwatch.Elapsed.TotalSeconds > 1) - { - _status.Text = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} -PointerUpdateKind: {_lastProperties?.PointerUpdateKind} -IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} -IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} -IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} -IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed} -IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed} -IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed} -IsEraser: {_lastProperties?.IsEraser} -IsInverted: {_lastProperties?.IsInverted} -Pressure: {_lastProperties?.Pressure} -XTilt: {_lastProperties?.XTilt} -YTilt: {_lastProperties?.YTilt} -Twist: {_lastProperties?.Twist}"; - _stopwatch.Restart(); - _events = 0; - } - - return true; - }, TimeSpan.FromMilliseconds(10)); - } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - - _statusUpdated?.Dispose(); - } - - void HandleEvent(PointerEventArgs e) - { - _events++; - if (_slider != null) - { - Thread.Sleep((int)_slider.Value); - } - InvalidateVisual(); - - if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) - { - _pointers.Remove(e.Pointer.Id); - return; - } - - var lastPointer = e.GetCurrentPoint(this); - _lastProperties = lastPointer.Properties; - - if (e.Pointer.Type != PointerType.Pen - || lastPointer.Properties.Pressure > 0) - { - if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) - _pointers[e.Pointer.Id] = pt = new PointerPoints(); - pt.HandleEvent(e, this); - } - } - - public override void Render(DrawingContext context) - { - context.FillRectangle(Brushes.White, Bounds); - foreach (var pt in _pointers.Values) - pt.Render(context, _drawPoints); - base.Render(context); - } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - if (e.ClickCount == 2) - { - _pointers.Clear(); - InvalidateVisual(); - return; - } - - HandleEvent(e); - base.OnPointerPressed(e); - } - - protected override void OnPointerMoved(PointerEventArgs e) - { - HandleEvent(e); - base.OnPointerMoved(e); - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - HandleEvent(e); - base.OnPointerReleased(e); - } - } -} diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml b/samples/ControlCatalog/Pages/PointersPage.xaml new file mode 100644 index 0000000000..1281ec77b6 --- /dev/null +++ b/samples/ControlCatalog/Pages/PointersPage.xaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Capture 1 + + + Capture 2 + + + + + diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml.cs b/samples/ControlCatalog/Pages/PointersPage.xaml.cs new file mode 100644 index 0000000000..977cee3d58 --- /dev/null +++ b/samples/ControlCatalog/Pages/PointersPage.xaml.cs @@ -0,0 +1,76 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages; + +public class PointersPage : UserControl +{ + public PointersPage() + { + this.InitializeComponent(); + + var border1 = this.Get("BorderCapture1"); + var border2 = this.Get("BorderCapture2"); + + border1.PointerPressed += Border_PointerPressed; + border1.PointerReleased += Border_PointerReleased; + border1.PointerCaptureLost += Border_PointerCaptureLost; + border1.PointerMoved += Border_PointerUpdated; + border1.PointerEnter += Border_PointerUpdated; + border1.PointerLeave += Border_PointerUpdated; + + border2.PointerPressed += Border_PointerPressed; + border2.PointerReleased += Border_PointerReleased; + border2.PointerCaptureLost += Border_PointerCaptureLost; + border2.PointerMoved += Border_PointerUpdated; + border2.PointerEnter += Border_PointerUpdated; + border2.PointerLeave += Border_PointerUpdated; + } + + private void Border_PointerUpdated(object sender, PointerEventArgs e) + { + var textBlock = (TextBlock)((Border)sender).Child; + var position = e.GetPosition((Border)sender); + textBlock.Text = @$"Captured: {e.Pointer.Captured == sender} +PointerId: {e.Pointer.Id} +Position: {(int)position.X} {(int)position.Y}"; + e.Handled = true; + } + + private void Border_PointerCaptureLost(object sender, PointerCaptureLostEventArgs e) + { + var textBlock = (TextBlock)((Border)sender).Child; + textBlock.Text = @$"Captured: {e.Pointer.Captured == sender} +PointerId: {e.Pointer.Id} +Position: ??? ???"; + e.Handled = true; + } + + private void Border_PointerReleased(object sender, PointerReleasedEventArgs e) + { + if (e.Pointer.Captured == sender) + { + e.Pointer.Capture(null); + e.Handled = true; + } + else + { + throw new InvalidOperationException("How?"); + } + } + + private void Border_PointerPressed(object sender, PointerPressedEventArgs e) + { + e.Pointer.Capture((Border)sender); + e.Handled = true; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} From e7b281e3fd9cb86db8fbabb2af933821b9eba809 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Apr 2022 04:32:06 -0400 Subject: [PATCH 24/42] Fix GetIntermediatePoints reverse order --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 6619c60152..1f316a0995 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -714,8 +714,6 @@ namespace Avalonia.Win32 private unsafe Lazy> CreateLazyIntermediatePoints(POINTER_INFO info) { - // Limit history size with reasonable value. - // With sizeof(POINTER_TOUCH_INFO) * 100 we can get maximum 14400 bytes. var historyCount = Math.Min((int)info.historyCount, MaxPointerHistorySize); if (historyCount > 1) { @@ -723,12 +721,15 @@ namespace Avalonia.Win32 { s_intermediatePointsPooledList.Clear(); s_intermediatePointsPooledList.Capacity = historyCount; + + // Pointers in history are ordered from newest to oldest, so we need to reverse iteration. + // Also we skip the newest pointer, because original event arguments already contains it. + if (info.pointerType == PointerInputType.PT_TOUCH) { if (GetPointerTouchInfoHistory(info.pointerId, ref historyCount, s_historyTouchInfos)) { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + for (int i = historyCount - 1; i >= 1; i--) { var historyTouchInfo = s_historyTouchInfos[i]; s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyTouchInfo)); @@ -739,8 +740,8 @@ namespace Avalonia.Win32 { if (GetPointerPenInfoHistory(info.pointerId, ref historyCount, s_historyPenInfos)) { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + uint timestamp = 0; + for (int i = historyCount - 1; i >= 1; i--) { var historyPenInfo = s_historyPenInfos[i]; s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyPenInfo)); @@ -752,8 +753,7 @@ namespace Avalonia.Win32 // Currently Windows does not return history info for mouse input, but we handle it just for case. if (GetPointerInfoHistory(info.pointerId, ref historyCount, s_historyInfos)) { - //last info is the same as the current so skip it - for (int i = 0; i < historyCount - 1; i++) + for (int i = historyCount - 1; i >= 1; i--) { var historyInfo = s_historyInfos[i]; s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyInfo)); From a08ccd91cba6efec6367ebc9e7d1d3cce226ae6b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 17 Apr 2022 03:44:22 -0400 Subject: [PATCH 25/42] Fix DataGrid scrolling --- src/Avalonia.Controls.DataGrid/DataGridCell.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 67183781d3..ea7d91ed97 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -178,9 +178,9 @@ namespace Avalonia.Controls { var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); - // Do not handle PointerPressed with touch, + // Do not handle PointerPressed with touch or pen, // so we can start scroll gesture on the same event. - if (e.Pointer.Type != PointerType.Touch) + if (e.Pointer.Type != PointerType.Touch && e.Pointer.Type != PointerType.Pen) { e.Handled = handled; } From bf7f50690f6d0b18516daaba971a44bd17570c33 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Jun 2022 19:06:32 +0200 Subject: [PATCH 26/42] Don't promote layout property values to local values. When DevTools was opened, all layout property values were being promoted to `LocalValue`s because the `_updatingFromControl` flag was not being set during initialization, causing the initial values to be written back out to the control as local values. --- .../ViewModels/ControlLayoutViewModel.cs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index 0c0c005122..a453fef212 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -30,20 +30,28 @@ namespace Avalonia.Diagnostics.ViewModels if (control is AvaloniaObject ao) { - MarginThickness = ao.GetValue(Layoutable.MarginProperty); - - if (HasPadding) + try { - PaddingThickness = ao.GetValue(Decorator.PaddingProperty); - } + _updatingFromControl = true; + MarginThickness = ao.GetValue(Layoutable.MarginProperty); + + if (HasPadding) + { + PaddingThickness = ao.GetValue(Decorator.PaddingProperty); + } - if (HasBorder) + if (HasBorder) + { + BorderThickness = ao.GetValue(Border.BorderThicknessProperty); + } + + HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty); + VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty); + } + finally { - BorderThickness = ao.GetValue(Border.BorderThicknessProperty); + _updatingFromControl = false; } - - HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty); - VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty); } UpdateSize(); From 2edc2ec425c4b1373e08204d7ab01448ca48bca7 Mon Sep 17 00:00:00 2001 From: Lobster Uberlord Date: Thu, 23 Jun 2022 02:42:15 +0700 Subject: [PATCH 27/42] Prevent input focus leaving the TextBox on first click --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor | 3 ++- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor index 68662bd931..b501db16d3 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor @@ -5,7 +5,8 @@ @onpointerdown="OnPointerDown" @onpointerup="OnPointerUp" @onpointermove="OnPointerMove" - @onpointercancel="OnPointerCancel"> + @onpointercancel="OnPointerCancel" + @onfocus="OnFocus"> diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index b575bc6dbb..d42529985b 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -37,6 +37,7 @@ namespace Avalonia.Web.Blazor private const SKColorType ColorType = SKColorType.Rgba8888; private bool _initialised; + private bool _inputElementFocused; [Inject] private IJSRuntime Js { get; set; } = null!; @@ -221,6 +222,16 @@ namespace Avalonia.Web.Blazor _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, e.Key, GetModifiers(e)); } + private void OnFocus(FocusEventArgs e) + { + // if focus has unexpectedly moved from the input element to the container element, + // shift it back to the input element + if (_inputElementFocused && _inputHelper is not null) + { + _inputHelper.Focus(); + } + } + private void OnInput(ChangeEventArgs e) { if (e.Value != null) @@ -374,10 +385,12 @@ namespace Avalonia.Web.Blazor if (active) { _inputHelper.Show(); + _inputElementFocused = true; _inputHelper.Focus(); } else { + _inputElementFocused = false; _inputHelper.Hide(); } } From 2b1eba8c7cb2253dd5791351c69c579daaafe6b5 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Thu, 30 Jun 2022 14:09:28 +0300 Subject: [PATCH 28/42] Fix leak in the InternalSelectionModel --- src/Avalonia.Controls/Selection/InternalSelectionModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index d92ffb0d1a..d0715e402d 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -168,7 +168,7 @@ namespace Avalonia.Controls.Selection { if (_writableSelectedItems is INotifyCollectionChanged incc) { - incc.CollectionChanged += OnSelectedItemsCollectionChanged; + incc.CollectionChanged -= OnSelectedItemsCollectionChanged; } } From 5c6370aa5600f91ae7077258f503ee5f6ea619bf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Jun 2022 00:43:45 +0200 Subject: [PATCH 29/42] Don't make ResourceDictionary inherit... ...from `AvaloniaDictionary`. --- .../Controls/ResourceDictionary.cs | 119 ++++++++++++++---- 1 file changed, 98 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 3af14daa83..77863e5101 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -1,38 +1,45 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; -using Avalonia.Metadata; - -#nullable enable namespace Avalonia.Controls { /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary + public class ResourceDictionary : IResourceDictionary { + private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; /// /// Initializes a new instance of the class. /// - public ResourceDictionary() - { - CollectionChanged += OnCollectionChanged; - } + public ResourceDictionary() { } /// /// Initializes a new instance of the class. /// - public ResourceDictionary(IResourceHost owner) - : this() + public ResourceDictionary(IResourceHost owner) => Owner = owner; + + public int Count => _inner?.Count ?? 0; + + public object? this[object key] { - Owner = owner; + get => _inner?[key]; + set + { + Inner[key] = value; + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } } + public ICollection Keys => (ICollection?)_inner?.Keys ?? Array.Empty(); + public ICollection Values => (ICollection?)_inner?.Values ?? Array.Empty(); + public IResourceHost? Owner { get => _owner; @@ -80,7 +87,7 @@ namespace Avalonia.Controls { get { - if (Count > 0) + if (_inner?.Count > 0) { return true; } @@ -100,11 +107,43 @@ namespace Avalonia.Controls } } + bool ICollection>.IsReadOnly => false; + + private Dictionary Inner => _inner ??= new(); + public event EventHandler? OwnerChanged; + public void Add(object key, object? value) + { + Inner.Add(key, value); + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + + public void Clear() + { + if (_inner?.Count > 0) + { + _inner.Clear(); + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } + + public bool ContainsKey(object key) => _inner?.ContainsKey(key) ?? false; + + public bool Remove(object key) + { + if (_inner?.Remove(key) == true) + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + return true; + } + + return false; + } + public bool TryGetResource(object key, out object? value) { - if (TryGetValue(key, out value)) + if (_inner is not null && _inner.TryGetValue(key, out value)) { return true; } @@ -120,9 +159,52 @@ namespace Avalonia.Controls } } + value = null; return false; } + public bool TryGetValue(object key, out object? value) + { + if (_inner is not null) + return _inner.TryGetValue(key, out value); + value = null; + return false; + } + + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return (_inner as ICollection>)?.Contains(item) ?? false; + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + (_inner as ICollection>)?.CopyTo(array, arrayIndex); + } + + bool ICollection>.Remove(KeyValuePair item) + { + if ((_inner as ICollection>)?.Remove(item) == true) + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + return true; + } + + return false; + } + + public IEnumerator> GetEnumerator() + { + return _inner?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + void IResourceProvider.AddOwner(IResourceHost owner) { owner = owner ?? throw new ArgumentNullException(nameof(owner)); @@ -134,7 +216,7 @@ namespace Avalonia.Controls Owner = owner; - var hasResources = Count > 0; + var hasResources = _inner?.Count > 0; if (_mergedDictionaries is object) { @@ -159,7 +241,7 @@ namespace Avalonia.Controls { Owner = null; - var hasResources = Count > 0; + var hasResources = _inner?.Count > 0; if (_mergedDictionaries is object) { @@ -176,10 +258,5 @@ namespace Avalonia.Controls } } } - - private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); - } } } From c26173c4fe8e39b03a961666a94ebf74092f63af Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Jul 2022 16:44:22 +0200 Subject: [PATCH 30/42] Added failing test for #7381. --- .../Rendering/SceneGraph/SceneBuilderTests.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 879de9fca5..502575702a 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -594,6 +594,69 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Should_Update_When_Control_Moved_Causing_Layout_Change() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Decorator moveFrom; + Decorator moveTo; + Canvas moveMe; + var tree = new TestRoot + { + Width = 100, + Height = 100, + Child = new DockPanel + { + Children = + { + (moveFrom = new Decorator + { + Child = moveMe = new Canvas + { + Width = 100, + Height = 100, + }, + }), + (moveTo = new Decorator()), + } + } + }; + + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + + var scene = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + + var moveFromNode = (VisualNode)scene.FindNode(moveFrom); + var moveToNode = (VisualNode)scene.FindNode(moveTo); + + Assert.Equal(1, moveFromNode.Children.Count); + Assert.Same(moveMe, moveFromNode.Children[0].Visual); + Assert.Empty(moveToNode.Children); + + moveFrom.Child = null; + moveTo.Child = moveMe; + tree.LayoutManager.ExecuteLayoutPass(); + + scene = scene.CloneScene(); + moveFromNode = (VisualNode)scene.FindNode(moveFrom); + moveToNode = (VisualNode)scene.FindNode(moveTo); + + moveFromNode.SortChildren(scene); + moveToNode.SortChildren(scene); + sceneBuilder.Update(scene, moveFrom); + sceneBuilder.Update(scene, moveTo); + sceneBuilder.Update(scene, moveMe); + + Assert.Empty(moveFromNode.Children); + Assert.Equal(1, moveToNode.Children.Count); + Assert.Same(moveMe, moveToNode.Children[0].Visual); + } + } + [Fact] public void Should_Update_When_Control_Made_Invisible() { From cfe572f30bb40e749ecbb84cacc9477a35aa86e3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Jul 2022 16:56:53 +0200 Subject: [PATCH 31/42] Remove node from parent when reparented. When a control was move from one parent container to another, and that move caused the new parent container to be laid out in a different position, a code path was taken which resulted in the `VisualNode` being present under both the old and new containers. Ensure that the node is removed from its old parent in this case. Fixes #7381 Fixes #6103 (probably) --- src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index 5dc426ab06..e4d5a1ca68 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -158,6 +158,7 @@ namespace Avalonia.Rendering.SceneGraph if (result != null && result.Parent != parent) { Deindex(scene, result); + ((VisualNode?)result.Parent)?.RemoveChild(result); result = null; } From b4b869a42902431f40766ec0ed276cf2c2db6b18 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 2 Jul 2022 01:22:14 -0400 Subject: [PATCH 32/42] Fix after event were renamed --- samples/ControlCatalog/Pages/PointersPage.xaml.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml.cs b/samples/ControlCatalog/Pages/PointersPage.xaml.cs index 977cee3d58..b463cf364d 100644 --- a/samples/ControlCatalog/Pages/PointersPage.xaml.cs +++ b/samples/ControlCatalog/Pages/PointersPage.xaml.cs @@ -20,15 +20,15 @@ public class PointersPage : UserControl border1.PointerReleased += Border_PointerReleased; border1.PointerCaptureLost += Border_PointerCaptureLost; border1.PointerMoved += Border_PointerUpdated; - border1.PointerEnter += Border_PointerUpdated; - border1.PointerLeave += Border_PointerUpdated; + border1.PointerEntered += Border_PointerUpdated; + border1.PointerExited += Border_PointerUpdated; border2.PointerPressed += Border_PointerPressed; border2.PointerReleased += Border_PointerReleased; border2.PointerCaptureLost += Border_PointerCaptureLost; border2.PointerMoved += Border_PointerUpdated; - border2.PointerEnter += Border_PointerUpdated; - border2.PointerLeave += Border_PointerUpdated; + border2.PointerEntered += Border_PointerUpdated; + border2.PointerExited += Border_PointerUpdated; } private void Border_PointerUpdated(object sender, PointerEventArgs e) From 17562c16bd25f3b325ff23e4380aecc0c877cc3d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 2 Jul 2022 03:43:08 -0400 Subject: [PATCH 33/42] Fixes after review --- samples/ControlCatalog/Pages/PointerCanvas.cs | 22 +++++- .../ControlCatalog/Pages/PointersPage.xaml | 1 + .../ControlCatalog/Pages/PointersPage.xaml.cs | 6 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 75 ++++++++++++------- 4 files changed, 72 insertions(+), 32 deletions(-) diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs index b815a573f2..5843b13a0c 100644 --- a/samples/ControlCatalog/Pages/PointerCanvas.cs +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -19,6 +19,7 @@ public class PointerCanvas : Control private IDisposable? _statusUpdated; private Dictionary _pointers = new(); private PointerPointProperties? _lastProperties; + private PointerUpdateKind? _lastNonOtherUpdateKind; class PointerPoints { struct CanvasPoint @@ -128,10 +129,11 @@ public class PointerCanvas : Control _statusUpdated = DispatcherTimer.Run(() => { - if (_stopwatch.Elapsed.TotalSeconds > 1) + if (_stopwatch.Elapsed.TotalMilliseconds > 250) { Status = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} PointerUpdateKind: {_lastProperties?.PointerUpdateKind} +Last PointerUpdateKind != Other: {_lastNonOtherUpdateKind} IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} @@ -162,21 +164,27 @@ Twist: {_lastProperties?.Twist}"; void HandleEvent(PointerEventArgs e) { _events++; + if (_threadSleep != 0) { Thread.Sleep(_threadSleep); } InvalidateVisual(); + var lastPointer = e.GetCurrentPoint(this); + _lastProperties = lastPointer.Properties; + + if (_lastProperties.PointerUpdateKind != PointerUpdateKind.Other) + { + _lastNonOtherUpdateKind = _lastProperties.PointerUpdateKind; + } + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) { _pointers.Remove(e.Pointer.Id); return; } - var lastPointer = e.GetCurrentPoint(this); - _lastProperties = lastPointer.Properties; - if (e.Pointer.Type != PointerType.Pen || lastPointer.Properties.Pressure > 0) { @@ -218,4 +226,10 @@ Twist: {_lastProperties?.Twist}"; HandleEvent(e); base.OnPointerReleased(e); } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _lastProperties = null; + base.OnPointerCaptureLost(e); + } } diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml b/samples/ControlCatalog/Pages/PointersPage.xaml index 1281ec77b6..c39106f29e 100644 --- a/samples/ControlCatalog/Pages/PointersPage.xaml +++ b/samples/ControlCatalog/Pages/PointersPage.xaml @@ -19,6 +19,7 @@ diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml.cs b/samples/ControlCatalog/Pages/PointersPage.xaml.cs index b463cf364d..6fc468e37f 100644 --- a/samples/ControlCatalog/Pages/PointersPage.xaml.cs +++ b/samples/ControlCatalog/Pages/PointersPage.xaml.cs @@ -35,7 +35,8 @@ public class PointersPage : UserControl { var textBlock = (TextBlock)((Border)sender).Child; var position = e.GetPosition((Border)sender); - textBlock.Text = @$"Captured: {e.Pointer.Captured == sender} + textBlock.Text = @$"Type: {e.Pointer.Type} +Captured: {e.Pointer.Captured == sender} PointerId: {e.Pointer.Id} Position: {(int)position.X} {(int)position.Y}"; e.Handled = true; @@ -44,7 +45,8 @@ Position: {(int)position.X} {(int)position.Y}"; private void Border_PointerCaptureLost(object sender, PointerCaptureLostEventArgs e) { var textBlock = (TextBlock)((Border)sender).Child; - textBlock.Text = @$"Captured: {e.Pointer.Captured == sender} + textBlock.Text = @$"Type: {e.Pointer.Type} +Captured: {e.Pointer.Captured == sender} PointerId: {e.Pointer.Id} Position: ??? ???"; e.Handled = true; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 861110bf9a..2ce4d7d1f0 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -746,7 +746,6 @@ namespace Avalonia.Win32 { if (GetPointerPenInfoHistory(info.pointerId, ref historyCount, s_historyPenInfos)) { - uint timestamp = 0; for (int i = historyCount - 1; i >= 1; i--) { var historyPenInfo = s_historyPenInfos[i]; @@ -830,7 +829,7 @@ namespace Avalonia.Win32 timestamp = info.dwTime; } - modifiers |= GetInputModifiers(info.dwKeyStates); + modifiers |= GetInputModifiers(info.pointerFlags); } private RawPointerPoint CreateRawPointerPoint(POINTER_INFO pointerInfo) @@ -871,48 +870,40 @@ namespace Avalonia.Win32 private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info) { - if (message == WindowsMessage.WM_POINTERUPDATE) + var isTouch = info.pointerType == PointerInputType.PT_TOUCH; + if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED)) { - return info.pointerType == PointerInputType.PT_TOUCH - ? RawPointerEventType.TouchUpdate - : RawPointerEventType.Move; + return isTouch ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow; } - switch (info.pointerType) + + var eventType = ToEventType(info.ButtonChangeType, isTouch); + if (eventType == RawPointerEventType.LeftButtonDown && + message == WindowsMessage.WM_NCPOINTERDOWN) { - case PointerInputType.PT_TOUCH: - if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED)) - { - return RawPointerEventType.TouchCancel; - } - return message == WindowsMessage.WM_POINTERDOWN || message == WindowsMessage.WM_NCPOINTERDOWN - ? RawPointerEventType.TouchBegin - : RawPointerEventType.TouchEnd; - default: - var eventType = ToEventType(info.ButtonChangeType); - if (eventType == RawPointerEventType.LeftButtonDown && - message == WindowsMessage.WM_NCPOINTERDOWN) - { - eventType = RawPointerEventType.NonClientLeftButtonDown; - } - return eventType; + eventType = RawPointerEventType.NonClientLeftButtonDown; } + + return eventType; } - private static RawPointerEventType ToEventType(PointerButtonChangeType type) + private static RawPointerEventType ToEventType(PointerButtonChangeType type, bool isTouch) { return type switch { - PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN => RawPointerEventType.LeftButtonDown, + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN when isTouch => RawPointerEventType.TouchBegin, + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN when !isTouch => RawPointerEventType.LeftButtonDown, PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown, PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_DOWN => RawPointerEventType.MiddleButtonDown, PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_DOWN => RawPointerEventType.XButton1Down, PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_DOWN => RawPointerEventType.XButton2Down, - PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP => RawPointerEventType.LeftButtonUp, + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP when isTouch => RawPointerEventType.TouchEnd, + PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP when !isTouch => RawPointerEventType.LeftButtonUp, PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_UP => RawPointerEventType.RightButtonUp, PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_UP => RawPointerEventType.MiddleButtonUp, PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up, PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up, + _ when isTouch => RawPointerEventType.TouchUpdate, _ => RawPointerEventType.Move }; } @@ -1000,5 +991,37 @@ namespace Avalonia.Win32 return modifiers; } + + private static RawInputModifiers GetInputModifiers(PointerFlags flags) + { + var modifiers = WindowsKeyboardDevice.Instance.Modifiers; + + if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_FIRSTBUTTON)) + { + modifiers |= RawInputModifiers.LeftMouseButton; + } + + if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_SECONDBUTTON)) + { + modifiers |= RawInputModifiers.RightMouseButton; + } + + if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_THIRDBUTTON)) + { + modifiers |= RawInputModifiers.MiddleMouseButton; + } + + if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_FOURTHBUTTON)) + { + modifiers |= RawInputModifiers.XButton1MouseButton; + } + + if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_FIFTHBUTTON)) + { + modifiers |= RawInputModifiers.XButton2MouseButton; + } + + return modifiers; + } } } From 1c8b26bcde086be143bb25140e11f3ed97562469 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 2 Jul 2022 03:46:28 -0400 Subject: [PATCH 34/42] Fix YTilt --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 2ce4d7d1f0..a2f9b7886a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -864,7 +864,7 @@ namespace Avalonia.Win32 Pressure = info.pressure / 1024f, Twist = info.rotation, XTilt = info.tiltX, - YTilt = info.tiltX + YTilt = info.tiltY }; } From 5000a4ffe35699233e1bd7039a9e380718e93994 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 2 Jul 2022 18:17:35 -0400 Subject: [PATCH 35/42] Do not enable MouseInPointerEnabled automatically and remove EnableWmPointerEvents option --- .editorconfig | 2 +- samples/ControlCatalog.NetCore/Program.cs | 1 - src/Windows/Avalonia.Win32/Win32Platform.cs | 10 +---- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 42 ++++++++++--------- src/Windows/Avalonia.Win32/WindowImpl.cs | 20 ++++----- 5 files changed, 33 insertions(+), 42 deletions(-) diff --git a/.editorconfig b/.editorconfig index cb589a5ce1..30edee1633 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,7 +21,7 @@ csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true -trim_trailing_whitespace = true +# trim_trailing_whitespace = true # Indentation preferences csharp_indent_block_contents = true diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d6a297858d..13751b56b5 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -115,7 +115,6 @@ namespace ControlCatalog.NetCore }) .With(new Win32PlatformOptions { - EnableWmPointerEvents = true }) .UseSkia() .AfterSetup(builder => diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index aa597d6809..8f6993c040 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -68,17 +68,9 @@ namespace Avalonia /// /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. /// - [Obsolete("Multitouch is always enabled")] + [Obsolete("Multitouch is always enabled on supported Windows versions")] public bool? EnableMultitouch { get; set; } = true; - /// - /// Enables Win8+ WM_POINTER events support. The default value is false. - /// - /// - /// Required for extended Pen and Touch support. - /// - public bool? EnableWmPointerEvents { get; set; } = false; - /// /// Embeds popups to the window when set to true. The default value is false. /// diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index a2f9b7886a..4c7b9a0348 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -84,7 +84,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_DESTROY: { UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); - + // We need to release IMM context and state to avoid leaks. if (Imm32InputMethod.Current.HWND == _hwnd) { @@ -110,9 +110,9 @@ namespace Avalonia.Win32 var newDisplayRect = Marshal.PtrToStructure(lParam); _scaling = dpi / 96.0; ScalingChanged?.Invoke(_scaling); - + using (SetResizeReason(PlatformResizeReason.DpiChange)) - { + { SetWindowPos(hWnd, IntPtr.Zero, newDisplayRect.left, @@ -180,7 +180,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN: { - if (_wmPointerEnabled) + if (IsMouseInPointerEnabled) { break; } @@ -213,7 +213,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { - if (_wmPointerEnabled) + if (IsMouseInPointerEnabled) { break; } @@ -241,12 +241,16 @@ namespace Avalonia.Win32 } // Mouse capture is lost case WindowsMessage.WM_CANCELMODE: - _mouseDevice.Capture(null); + if (!IsMouseInPointerEnabled) + { + _mouseDevice.Capture(null); + } + break; case WindowsMessage.WM_MOUSEMOVE: { - if (_wmPointerEnabled) + if (IsMouseInPointerEnabled) { break; } @@ -273,7 +277,7 @@ namespace Avalonia.Win32 timestamp, _owner, RawPointerEventType.Move, - DipFromLParam(lParam), + DipFromLParam(lParam), GetMouseModifiers(wParam)); break; @@ -281,7 +285,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MOUSEWHEEL: { - if (_wmPointerEnabled) + if (IsMouseInPointerEnabled) { break; } @@ -290,14 +294,14 @@ namespace Avalonia.Win32 timestamp, _owner, PointToClient(PointFromLParam(lParam)), - new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), + new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); break; } case WindowsMessage.WM_MOUSEHWHEEL: { - if (_wmPointerEnabled) + if (IsMouseInPointerEnabled) { break; } @@ -306,14 +310,14 @@ namespace Avalonia.Win32 timestamp, _owner, PointToClient(PointFromLParam(lParam)), - new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), + new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); break; } case WindowsMessage.WM_MOUSELEAVE: { - if (_wmPointerEnabled) + if (IsMouseInPointerEnabled) { break; } @@ -323,7 +327,7 @@ namespace Avalonia.Win32 timestamp, _owner, RawPointerEventType.LeaveWindow, - new Point(-1, -1), + new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); break; } @@ -333,7 +337,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN: { - if (_wmPointerEnabled) + if (IsMouseInPointerEnabled) { break; } @@ -598,7 +602,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_GETMINMAXINFO: { MINMAXINFO mmi = Marshal.PtrToStructure(lParam); - + _maxTrackSize = mmi.ptMaxTrackSize; if (_minSize.Width > 0) @@ -682,7 +686,7 @@ namespace Avalonia.Win32 if (_managedDrag.PreprocessInputEvent(ref e)) return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); #endif - + if(shouldTakeFocus) { SetFocus(_hwnd); @@ -915,11 +919,11 @@ namespace Avalonia.Win32 if (langid == _langid && Imm32InputMethod.Current.HWND == Hwnd) { return; - } + } _langid = langid; Imm32InputMethod.Current.SetLanguageAndWindow(this, Hwnd, hkl); - + } private static int ToInt32(IntPtr ptr) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 95dac19d04..31f30a6e47 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -74,12 +74,12 @@ namespace Avalonia.Win32 private readonly ManagedDeferredRendererLock _rendererLock; private readonly FramebufferManager _framebuffer; private readonly IGlPlatformSurface _gl; + private readonly bool _wmPointerEnabled; private Win32NativeControlHost _nativeControlHost; private WndProc _wndProcDelegate; private string _className; private IntPtr _hwnd; - private bool _wmPointerEnabled; private IInputRoot _owner; private WindowProperties _windowProperties; private bool _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk @@ -100,10 +100,10 @@ namespace Avalonia.Win32 private bool _ignoreWmChar; private const int MaxPointerHistorySize = 512; - private readonly static PooledList s_intermediatePointsPooledList = new(); - private readonly static POINTER_TOUCH_INFO[] s_historyTouchInfos = new POINTER_TOUCH_INFO[MaxPointerHistorySize]; - private readonly static POINTER_PEN_INFO[] s_historyPenInfos = new POINTER_PEN_INFO[MaxPointerHistorySize]; - private readonly static POINTER_INFO[] s_historyInfos = new POINTER_INFO[MaxPointerHistorySize]; + private static readonly PooledList s_intermediatePointsPooledList = new(); + private static readonly POINTER_TOUCH_INFO[] s_historyTouchInfos = new POINTER_TOUCH_INFO[MaxPointerHistorySize]; + private static readonly POINTER_PEN_INFO[] s_historyPenInfos = new POINTER_PEN_INFO[MaxPointerHistorySize]; + private static readonly POINTER_INFO[] s_historyInfos = new POINTER_INFO[MaxPointerHistorySize]; public WindowImpl() { @@ -138,13 +138,7 @@ namespace Avalonia.Win32 egl.Display is AngleWin32EglDisplay angleDisplay && angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; - _wmPointerEnabled = Win32Platform.Options.EnableWmPointerEvents - ?? Win32Platform.WindowsVersion >= PlatformConstants.Windows8; - - if (_wmPointerEnabled && !IsMouseInPointerEnabled()) - { - EnableMouseInPointer(true); - } + _wmPointerEnabled = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); @@ -300,6 +294,8 @@ namespace Avalonia.Win32 protected IntPtr Hwnd => _hwnd; + private bool IsMouseInPointerEnabled => _wmPointerEnabled && IsMouseInPointerEnabled(); + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { TransparencyLevel = EnableBlur(transparencyLevel); From 6c2b145189115f557e64941d75b571606e76b9ca Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 2 Jul 2022 19:52:39 -0400 Subject: [PATCH 36/42] Remove DottedLineFocusAdorner --- .../Controls/FocusAdorner.xaml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml b/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml index e6577497da..91bf71ed4d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml @@ -19,16 +19,4 @@ - - - From 1408e521991d0b66cd3d4151da2918a900d058a5 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 4 Jul 2022 09:07:44 +0200 Subject: [PATCH 37/42] Add IAddChild support --- .../AvaloniaXamlIlLanguage.cs | 6 ++- .../Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- .../Avalonia.Markup.Xaml.csproj | 1 + src/Markup/Avalonia.Markup.Xaml/IAddChild.cs | 12 +++++ .../Xaml/BasicTests.cs | 51 +++++++++++++++++++ 5 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/IAddChild.cs diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 8ed94f6b20..54eac8f59e 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -41,7 +41,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions }, WhitespaceSignificantCollectionAttributes = { - typeSystem.GetType("Avalonia.Metadata.WhitespaceSignificantCollectionAttribute") + typeSystem.GetType("Avalonia.Metadata.WhitespaceSignificantCollectionAttribute") }, TrimSurroundingWhitespaceAttributes = { @@ -56,7 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XmlNamespaceInfoProvider = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlXmlNamespaceInfoProvider"), - DeferredContentPropertyAttributes = {typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute")}, + DeferredContentPropertyAttributes = { typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute") }, DeferredContentExecutorCustomizationDefaultTypeParameter = typeSystem.GetType("Avalonia.Controls.IControl"), DeferredContentExecutorCustomizationTypeParameterDeferredContentAttributePropertyNames = new List { @@ -70,6 +70,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions }, InnerServiceProviderFactoryMethod = runtimeHelpers.FindMethod(m => m.Name == "CreateInnerServiceProviderV1"), + IAddChild = typeSystem.GetType("Avalonia.Markup.Xaml.IAddChild"), + IAddChildOfT = typeSystem.GetType("Avalonia.Markup.Xaml.IAddChild`1") }; rv.CustomAttributeResolver = new AttributeResolver(typeSystem, rv); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index daaac590e0..a4e6be2d14 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit daaac590e078967b78045f74c38ef046d00d8582 +Subproject commit a4e6be2d1407abec4f35fcb208848830ce513ead diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 30d321426f..c35cbc35c0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/IAddChild.cs b/src/Markup/Avalonia.Markup.Xaml/IAddChild.cs new file mode 100644 index 0000000000..993eb6142d --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/IAddChild.cs @@ -0,0 +1,12 @@ +namespace Avalonia.Markup.Xaml +{ + public interface IAddChild + { + void AddChild(object child); + } + + public interface IAddChild : IAddChild + { + void AddChild(T child); + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index f12785a7ce..bed5f19ecb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -958,6 +958,29 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("Foo", toolTip.Content); } + [Fact] + public void AddChild_Child_Is_Set() + { + var xaml = @"Foo"; + + var target = AvaloniaRuntimeXamlLoader.Parse(xaml); + + Assert.NotNull(target); + Assert.Equal("Foo", target.Child); + } + + [Fact] + public void AddChildOfT_Child_Is_Set() + { + var xaml = @"Foo"; + + var target = AvaloniaRuntimeXamlLoader.Parse(xaml); + + Assert.NotNull(target); + Assert.Null(target.Child); + Assert.Equal("Foo", target.Text); + } + private class SelectedItemsViewModel : INotifyPropertyChanged { public string[] Items { get; set; } @@ -977,6 +1000,34 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } } + + public class ObjectWithAddChild : IAddChild + { + public object Child { get; set; } + + void IAddChild.AddChild(object child) + { + Child = child; + } + } + + public class ObjectWithAddChildOfT : IAddChild + { + public string Text { get; set; } + + public object Child { get; set; } + + void IAddChild.AddChild(object child) + { + Child = child; + } + + void IAddChild.AddChild(string child) + { + Text = child; + } + } + public class BasicTestsAttachedPropertyHolder { public static AvaloniaProperty FooProperty = From 49aad04861f19c02ddc6a0780750ad29cc474c3b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 4 Jul 2022 08:56:47 -0400 Subject: [PATCH 38/42] Apply target's TemplatedParent to a Flyout and Tooltip, when it's opened (#8412) * Apply target's TemplatedParent to a Flyout and Tooltip, when it's opened * Add flyout and tooltip leak tests * Fix Flyout_Is_Freed --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 1 + src/Avalonia.Controls/Primitives/Popup.cs | 17 +-- .../Primitives/TemplatedControl.cs | 10 +- src/Avalonia.Controls/ToolTip.cs | 5 +- tests/Avalonia.LeakTests/ControlTests.cs | 105 +++++++++++++++ .../Data/TemplateBindingTests.cs | 127 ++++++++++++++++++ 6 files changed, 242 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 4801fa69f0..1504d2b25f 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -223,6 +223,7 @@ namespace Avalonia.Controls.Primitives { Popup.PlacementTarget = Target = placementTarget; ((ISetLogicalParent)Popup).SetParent(placementTarget); + Popup.SetValue(StyledElement.TemplatedParentProperty, placementTarget.TemplatedParent); } if (Popup.Child == null) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 95e5e25c42..1501d97470 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -860,22 +860,7 @@ namespace Avalonia.Controls.Primitives { if (control != null) { - var templatedParent = TemplatedParent; - - if (control.TemplatedParent == null) - { - control.SetValue(TemplatedParentProperty, templatedParent); - } - - control.ApplyTemplate(); - - if (!(control is IPresenter) && control.TemplatedParent == templatedParent) - { - foreach (IControl child in control.VisualChildren) - { - SetTemplatedParentAndApplyChildTemplates(child); - } - } + TemplatedControl.ApplyTemplatedParent(control, TemplatedParent); } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..4403bfce51 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -285,7 +285,7 @@ namespace Avalonia.Controls.Primitives Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(this, "Creating control template"); var (child, nameScope) = template.Build(this); - ApplyTemplatedParent(child); + ApplyTemplatedParent(child, this); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); @@ -387,18 +387,18 @@ namespace Avalonia.Controls.Primitives /// Sets the TemplatedParent property for the created template children. /// /// The control. - private void ApplyTemplatedParent(IControl control) + internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent) { - control.SetValue(TemplatedParentProperty, this); + control.SetValue(TemplatedParentProperty, templatedParent); var children = control.LogicalChildren; var count = children.Count; for (var i = 0; i < count; i++) { - if (children[i] is IControl child) + if (children[i] is IStyledElement child) { - ApplyTemplatedParent(child); + ApplyTemplatedParent(child, templatedParent); } } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 91c93c87c8..bb18bf4c64 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -271,8 +271,9 @@ namespace Avalonia.Controls _popupHost = OverlayPopupHost.CreatePopupHost(control, null); _popupHost.SetChild(this); ((ISetLogicalParent)_popupHost).SetParent(control); - - _popupHost.ConfigurePosition(control, GetPlacement(control), + ApplyTemplatedParent(this, control.TemplatedParent); + + _popupHost.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); WindowManagerAddShadowHintChanged(_popupHost, false); diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 8c05f2a0a7..6fb7b1448c 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -7,6 +7,7 @@ using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -877,6 +878,110 @@ namespace Avalonia.LeakTests } } + [Fact] + public void ToolTip_Is_Freed() + { + using (Start()) + { + Func run = () => + { + var window = new Window(); + var source = new Button + { + Template = new FuncControlTemplate