From c8f029386d88a62a9403c98adb52e519a5529ac3 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 14:01:35 +0300 Subject: [PATCH 01/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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 f4cc30d4a10149d4a9d9f88578ee35b3ecfa0b0a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 29 May 2022 22:28:28 +0200 Subject: [PATCH 26/96] Add failing test for Window.Width/Height. --- .../WindowTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 63ccf74c2b..a8c9b68d12 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -695,6 +695,31 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_Manual() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Canvas + { + Width = 400, + Height = 800, + }; + + var target = new Window() + { + SizeToContent = SizeToContent.Manual, + Content = child + }; + + Show(target); + + // Values come from MockWindowingPlatform defaults. + Assert.Equal(800, target.Width); + Assert.Equal(600, target.Height); + } + } + [Fact] public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight() { @@ -712,6 +737,8 @@ namespace Avalonia.Controls.UnitTests Content = child }; + target.GetObservable(Window.WidthProperty).Subscribe(x => { }); + Show(target); Assert.Equal(400, target.Width); From 138be304a0d2c86c663846bfa7adfb1537058fd1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 30 May 2022 11:53:34 +0200 Subject: [PATCH 27/96] Ensure Window.Width/Height is initialized. - Run resize logic if `Width`/`Height` are still NaN (i.e. not set up) - Always call base class `WindowBase.HandleResize` - In `WindowBase.HandleResize` don't run a layout pass if client size unchanged Fixes `Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_Manual`. --- src/Avalonia.Controls/Window.cs | 40 ++++++++++++++--------------- src/Avalonia.Controls/WindowBase.cs | 12 ++++++--- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a4f4534b88..9b49e866b8 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -991,28 +991,28 @@ namespace Avalonia.Controls /// protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason) { - if (ClientSize == clientSize) - return; - - var sizeToContent = SizeToContent; - - // If auto-sizing is enabled, and the resize came from a user resize (or the reason was - // unspecified) then turn off auto-resizing for any window dimension that is not equal - // to the requested size. - if (sizeToContent != SizeToContent.Manual && - CanResize && - reason == PlatformResizeReason.Unspecified || - reason == PlatformResizeReason.User) + if (ClientSize != clientSize || double.IsNaN(Width) || double.IsNaN(Height)) { - if (clientSize.Width != ClientSize.Width) - sizeToContent &= ~SizeToContent.Width; - if (clientSize.Height != ClientSize.Height) - sizeToContent &= ~SizeToContent.Height; - SizeToContent = sizeToContent; - } + var sizeToContent = SizeToContent; + + // If auto-sizing is enabled, and the resize came from a user resize (or the reason was + // unspecified) then turn off auto-resizing for any window dimension that is not equal + // to the requested size. + if (sizeToContent != SizeToContent.Manual && + CanResize && + reason == PlatformResizeReason.Unspecified || + reason == PlatformResizeReason.User) + { + if (clientSize.Width != ClientSize.Width) + sizeToContent &= ~SizeToContent.Width; + if (clientSize.Height != ClientSize.Height) + sizeToContent &= ~SizeToContent.Height; + SizeToContent = sizeToContent; + } - Width = clientSize.Width; - Height = clientSize.Height; + Width = clientSize.Width; + Height = clientSize.Height; + } base.HandleResized(clientSize, reason); } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 12ba143c8a..d5e54a5c08 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -217,10 +217,16 @@ namespace Avalonia.Controls /// The reason for the resize. protected override void HandleResized(Size clientSize, PlatformResizeReason reason) { - ClientSize = clientSize; FrameSize = PlatformImpl?.FrameSize; - LayoutManager.ExecuteLayoutPass(); - Renderer?.Resized(clientSize); + + if (ClientSize != clientSize) + { + ClientSize = clientSize; + LayoutManager.ExecuteLayoutPass(); + Renderer?.Resized(clientSize); + } + + System.Diagnostics.Debug.WriteLine($"HandleResized: ClientSize {ClientSize} | FrameSize {FrameSize}"); } /// From 67e6c41abcbaed905bba22446ee54b995a29bed1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 30 May 2022 11:54:34 +0200 Subject: [PATCH 28/96] Use FrameSize for window startup location. ...if available. --- src/Avalonia.Controls/Window.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 9b49e866b8..92f74530e2 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -871,10 +871,10 @@ namespace Avalonia.Controls var scaling = owner?.DesktopScaling ?? PlatformImpl?.DesktopScaling ?? 1; - // TODO: We really need non-client size here. - var rect = new PixelRect( - PixelPoint.Origin, - PixelSize.FromSize(ClientSize, scaling)); + // Use frame size, falling back to client size if the platform can't give it to us. + var rect = FrameSize.HasValue ? + new PixelRect(PixelSize.FromSize(FrameSize.Value, scaling)) : + new PixelRect(PixelSize.FromSize(ClientSize, scaling)); if (startupLocation == WindowStartupLocation.CenterScreen) { From 4c8240b7bf349ca5bc7f74344ba45af73b77f57c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 30 May 2022 11:57:13 +0200 Subject: [PATCH 29/96] Added integration tests for WindowStartupLocation. --- samples/IntegrationTestApp/MainWindow.axaml | 17 ++ .../IntegrationTestApp/MainWindow.axaml.cs | 38 ++++ .../IntegrationTestApp/ShowWindowTest.axaml | 26 +++ .../ShowWindowTest.axaml.cs | 40 +++++ .../Avalonia.IntegrationTests.Appium.csproj | 4 + .../WindowTests.cs | 164 ++++++++++++++++++ 6 files changed, 289 insertions(+) create mode 100644 samples/IntegrationTestApp/ShowWindowTest.axaml create mode 100644 samples/IntegrationTestApp/ShowWindowTest.axaml.cs create mode 100644 tests/Avalonia.IntegrationTests.Appium/WindowTests.cs diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 19ac68b15b..5151d80932 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -94,6 +94,23 @@ + + + + + + NonOwned + Owned + Modal + + + Manual + CenterScreen + CenterOwner + + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 9a612aa94d..580548a433 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -5,6 +5,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; namespace IntegrationTestApp { @@ -46,6 +47,41 @@ namespace IntegrationTestApp } } + private void ShowWindow() + { + var sizeTextBox = this.GetControl("ShowWindowSize"); + var modeComboBox = this.GetControl("ShowWindowMode"); + var locationComboBox = this.GetControl("ShowWindowLocation"); + var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null; + var owner = (Window)this.GetVisualRoot()!; + + var window = new ShowWindowTest + { + WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, + }; + + if (size.HasValue) + { + window.Width = size.Value.Width; + window.Height = size.Value.Height; + } + + sizeTextBox.Text = string.Empty; + + switch (modeComboBox.SelectedIndex) + { + case 0: + window.Show(); + break; + case 1: + window.Show(owner); + break; + case 2: + window.ShowDialog(owner); + break; + } + } + private void MenuClicked(object? sender, RoutedEventArgs e) { var clickedMenuItemTextBlock = this.FindControl("ClickedMenuItem"); @@ -64,6 +100,8 @@ namespace IntegrationTestApp this.FindControl("BasicListBox").SelectedIndex = -1; if (source?.Name == "MenuClickedMenuItemReset") this.FindControl("ClickedMenuItem").Text = "None"; + if (source?.Name == "ShowWindow") + ShowWindow(); } } } diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml new file mode 100644 index 0000000000..e87ae0f32c --- /dev/null +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs new file mode 100644 index 0000000000..9b55864caa --- /dev/null +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -0,0 +1,40 @@ +using System; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Rendering; + +namespace IntegrationTestApp +{ + public class ShowWindowTest : Window + { + public ShowWindowTest() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnOpened(EventArgs e) + { + base.OnOpened(e); + this.GetControl("ClientSize").Text = $"{Width}, {Height}"; + this.GetControl("FrameSize").Text = $"{FrameSize}"; + this.GetControl("Position").Text = $"{Position}"; + this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; + this.GetControl("Scaling").Text = $"{((IRenderRoot)this).RenderScaling}"; + + if (Owner is not null) + { + var ownerRect = this.GetControl("OwnerRect"); + var owner = (Window)Owner; + ownerRect.Text = $"{owner.Position}, {owner.FrameSize}"; + } + } + + private void CloseWindow_Click(object sender, RoutedEventArgs e) => Close(); + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj index 095f0e63e0..03d9332051 100644 --- a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj +++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj @@ -5,6 +5,10 @@ enable + + + + diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs new file mode 100644 index 0000000000..771faa1925 --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -0,0 +1,164 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using OpenQA.Selenium.Appium; +using Xunit; +using Xunit.Sdk; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class WindowTests + { + private readonly AppiumDriver _session; + + public WindowTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Window"); + tab.Click(); + } + + [Theory] + [MemberData(nameof(StartupLocationData))] + public void StartupLocation(string? size, ShowWindowMode mode, WindowStartupLocation location) + { + var mainWindowHandle = GetCurrentWindowHandleHack(); + + try + { + var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); + var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); + var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var showButton = _session.FindElementByAccessibilityId("ShowWindow"); + + if (size is not null) + sizeTextBox.SendKeys(size); + + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + + showButton.Click(); + SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); + + var clientSize = Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text); + var frameSize = Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text); + var position = PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text); + var screenRect = PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text); + var scaling = double.Parse(_session.FindElementByAccessibilityId("Scaling").Text); + + Assert.True(frameSize.Width >= clientSize.Width, "Expected frame width >= client width."); + Assert.True(frameSize.Height > clientSize.Height, "Expected frame height > client height."); + + var frameRect = new PixelRect(position, PixelSize.FromSize(frameSize, scaling)); + + switch (location) + { + case WindowStartupLocation.CenterScreen: + { + var expected = screenRect.CenterRect(frameRect); + AssertCloseEnough(expected.Position, frameRect.Position); + break; + } + } + } + finally + { + try + { + var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); + closeButton.Click(); + SwitchToMainWindowHack(mainWindowHandle); + } + catch { } + } + } + + public static TheoryData StartupLocationData() + { + var sizes = new[] { null, "400,300" }; + var data = new TheoryData(); + + foreach (var size in sizes) + { + foreach (var mode in Enum.GetValues()) + { + foreach (var location in Enum.GetValues()) + { + if (!(location == WindowStartupLocation.CenterOwner && mode == ShowWindowMode.NonOwned)) + { + data.Add(size, mode, location); + } + } + } + } + + return data; + } + + private string? GetCurrentWindowHandleHack() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // HACK: WinAppDriver only seems to switch to a newly opened window if the window has an owner, + // otherwise the session remains targeting the previous window. Return the handle for the + // current window so we know which window to switch to when another is opened. + return _session.WindowHandles.Single(); + } + + return null; + } + + private void SwitchToNewWindowHack(string? oldWindowHandle) + { + if (oldWindowHandle is not null) + { + var newWindowHandle = _session.WindowHandles.FirstOrDefault(x => x != oldWindowHandle); + + // HACK: Looks like WinAppDriver only adds window handles for non-owned windows, but luckily + // non-owned windows is where we're having the problem, so if we find a window handle that + // isn't the main window handle then switch to it. + if (newWindowHandle is not null) + _session.SwitchTo().Window(newWindowHandle); + } + } + + private void SwitchToMainWindowHack(string? mainWindowHandle) + { + if (mainWindowHandle is not null) + _session.SwitchTo().Window(mainWindowHandle); + } + + private static void AssertCloseEnough(PixelPoint expected, PixelPoint actual) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // On win32, accurate frame information cannot be obtained until a window is shown but + // WindowStartupLocation needs to be calculated before the window is shown, meaning that + // the position of a centered window can be off by a bit. From initial testing, looks + // like this shouldn't be more than 10 pixels. + if (Math.Abs(expected.X - actual.X) > 10) + throw new EqualException(expected, actual); + if (Math.Abs(expected.Y - actual.Y) > 10) + throw new EqualException(expected, actual); + } + else + { + Assert.Equal(expected, actual); + } + } + + public enum ShowWindowMode + { + NonOwned, + Owned, + Modal + } + } +} From a2d83e8fae5a559bca83d87db7f5a30d901618ed Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 31 May 2022 13:11:07 +0100 Subject: [PATCH 30/96] [OSX] programatically implement child window relationship --- native/Avalonia.Native/src/OSX/AvnView.mm | 5 ++ native/Avalonia.Native/src/OSX/AvnWindow.mm | 38 +++++------ .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 7 +- native/Avalonia.Native/src/OSX/WindowImpl.h | 8 ++- native/Avalonia.Native/src/OSX/WindowImpl.mm | 66 +++++++++++++------ .../Avalonia.Native/src/OSX/WindowProtocol.h | 1 - 7 files changed, 81 insertions(+), 46 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 5436ad22f3..bbb4d59adb 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -300,6 +300,11 @@ - (void)mouseDown:(NSEvent *)event { + if(_parent != nullptr) + { + _parent->BringToFront(); + } + _isLeftPressed = true; _lastMouseDownEvent = event; [self mouseEvent:event withType:LeftButtonDown]; diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 590dc5e7ac..1445227cf5 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -183,6 +183,11 @@ return self; } +- (void)mouseDown:(NSEvent *)event +{ + _parent->BringToFront(); +} + - (BOOL)windowShouldClose:(NSWindow *)sender { auto window = dynamic_cast(_parent.getRaw()); @@ -209,7 +214,14 @@ { ComPtr parent = _parent; _parent = NULL; - [self restoreParentWindow]; + + auto window = dynamic_cast(parent.getRaw()); + + if(window != nullptr) + { + window->SetParent(nullptr); + } + parent->BaseEvents->Closed(); [parent->View onClosed]; } @@ -220,17 +232,11 @@ if(_canBecomeKeyWindow) { // If the window has a child window being shown as a dialog then don't allow it to become the key window. - for(NSWindow* uch in [self childWindows]) + auto parent = dynamic_cast(_parent.getRaw()); + + if(parent != nullptr) { - if (![uch conformsToProtocol:@protocol(AvnWindowProtocol)]) - { - continue; - } - - id ch = (id ) uch; - - if(ch.isDialog) - return false; + return parent->CanBecomeKeyWindow(); } return true; @@ -273,16 +279,6 @@ [super becomeKeyWindow]; } --(void) restoreParentWindow; -{ - auto parent = [self parentWindow]; - - if(parent != nil) - { - [parent removeChildWindow:self]; - } -} - - (void)windowDidMiniaturize:(NSNotification *)notification { auto parent = dynamic_cast(_parent.operator->()); diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 83850e780c..62c0e2069d 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -99,6 +99,8 @@ BEGIN_INTERFACE_MAP() virtual bool IsDialog(); id GetWindowProtocol (); + + virtual void BringToFront (); protected: virtual NSWindowStyleMask GetStyle(); diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 121679b942..77f0f47934 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -143,8 +143,6 @@ HRESULT WindowBaseImpl::Hide() { @autoreleasepool { if (Window != nullptr) { [Window orderOut:Window]; - - [GetWindowProtocol() restoreParentWindow]; } return S_OK; @@ -610,6 +608,11 @@ id WindowBaseImpl::GetWindowProtocol() { return (id ) Window; } +void WindowBaseImpl::BringToFront() +{ + // do nothing. +} + extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) { @autoreleasepool diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 35627685a2..76d5cbf6ea 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -8,6 +8,7 @@ #import "WindowBaseImpl.h" #include "IWindowStateChanged.h" +#include class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged { @@ -22,7 +23,8 @@ private: bool _transitioningWindowState; bool _isClientAreaExtended; bool _isDialog; - WindowImpl* _lastParent; + WindowImpl* _parent; + std::list _children; AvnExtendClientAreaChromeHints _extendClientHints; FORWARD_IUNKNOWN() @@ -91,6 +93,10 @@ BEGIN_INTERFACE_MAP() virtual bool IsDialog() override; virtual void OnInitialiseNSWindow() override; + + virtual void BringToFront () override; + + bool CanBecomeKeyWindow (); protected: virtual NSWindowStyleMask GetStyle() override; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ad804eb280..5333cb23c8 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -10,6 +10,7 @@ #include "WindowProtocol.h" WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBaseImpl(events, gl) { + _children = std::list(); _isClientAreaExtended = false; _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; @@ -20,7 +21,7 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase _lastWindowState = Normal; _actualWindowState = Normal; _lastTitle = @""; - _lastParent = nullptr; + _parent = nullptr; WindowEvents = events; } @@ -63,9 +64,9 @@ void WindowImpl::OnInitialiseNSWindow(){ SetExtendClientArea(true); } - if(_lastParent != nullptr) + if(_parent != nullptr) { - SetParent(_lastParent); + SetParent(_parent); } } @@ -96,33 +97,56 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) { START_COM_CALL; @autoreleasepool { - if (parent == nullptr) - return E_POINTER; + if(_parent != nullptr) + { + _parent->_children.remove(this); + } auto cparent = dynamic_cast(parent); - if (cparent == nullptr) - return E_INVALIDARG; - - _lastParent = cparent; + _parent = cparent; - if(Window != nullptr){ - // If one tries to show a child window with a minimized parent window, then the parent window will be - // restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive - // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. - if (cparent->WindowState() == Minimized) - cparent->SetWindowState(Normal); - - [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; - [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; - - UpdateStyle(); + if(_parent != nullptr && Window != nullptr){ + // If one tries to show a child window with a minimized parent window, then the parent window will be + // restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive + // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. + if (cparent->WindowState() == Minimized) + cparent->SetWindowState(Normal); + + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; + + cparent->_children.push_back(this); + + UpdateStyle(); } return S_OK; } } +void WindowImpl::BringToFront() +{ + Activate(); + + for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) + { + (*iterator)->BringToFront(); + } +} + +bool WindowImpl::CanBecomeKeyWindow() +{ + for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) + { + if((*iterator)->IsDialog()) + { + return false; + } + } + + return true; +} + void WindowImpl::StartStateTransition() { _transitioningWindowState = true; } @@ -534,7 +558,7 @@ bool WindowImpl::IsDialog() { } NSWindowStyleMask WindowImpl::GetStyle() { - unsigned long s = this->_isDialog ? NSWindowStyleMaskDocModalWindow : NSWindowStyleMaskBorderless; + unsigned long s = NSWindowStyleMaskBorderless; switch (_decorations) { case SystemDecorationsNone: diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h index 0e5c5869e7..cb5f86bdb9 100644 --- a/native/Avalonia.Native/src/OSX/WindowProtocol.h +++ b/native/Avalonia.Native/src/OSX/WindowProtocol.h @@ -11,7 +11,6 @@ @protocol AvnWindowProtocol -(void) pollModalSession: (NSModalSession _Nonnull) session; --(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; -(void) setEnabled: (bool) enable; -(void) showAppMenuOnly; From dc1b6a669b8a90bc71344ea6b6df28e2b489e7a0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 31 May 2022 13:50:01 +0100 Subject: [PATCH 31/96] [osx] make bringtofront work correctly for owned and modal windows. --- native/Avalonia.Native/src/OSX/AvnView.mm | 5 ----- native/Avalonia.Native/src/OSX/AvnWindow.mm | 9 +++------ native/Avalonia.Native/src/OSX/WindowImpl.mm | 9 ++++++++- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index bbb4d59adb..5436ad22f3 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -300,11 +300,6 @@ - (void)mouseDown:(NSEvent *)event { - if(_parent != nullptr) - { - _parent->BringToFront(); - } - _isLeftPressed = true; _lastMouseDownEvent = event; [self mouseEvent:event withType:LeftButtonDown]; diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 1445227cf5..60fdb26121 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -183,11 +183,6 @@ return self; } -- (void)mouseDown:(NSEvent *)event -{ - _parent->BringToFront(); -} - - (BOOL)windowShouldClose:(NSWindow *)sender { auto window = dynamic_cast(_parent.getRaw()); @@ -435,8 +430,10 @@ _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast([event timestamp] * 1000), AvnInputModifiersNone, point, delta); } + + _parent->BringToFront(); } - break; + break; case NSEventTypeMouseEntered: { diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 5333cb23c8..8330f4ed86 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -126,7 +126,14 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) { void WindowImpl::BringToFront() { - Activate(); + if(IsDialog()) + { + Activate(); + } + else + { + [Window orderFront:nullptr]; + } for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) { From f7daa81fbe8d684c13d0d10c8fb2727734e2e480 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 31 May 2022 14:10:33 +0100 Subject: [PATCH 32/96] dont create nspanel / nswindow at show. --- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 77f0f47934..d105f4cf38 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -36,8 +36,10 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; lastMinSize = NSSize { 0, 0 }; - Window = nullptr; lastMenu = nullptr; + + CreateNSWindow(false); + InitialiseNSWindow(); } HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) { @@ -88,7 +90,6 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { START_COM_CALL; @autoreleasepool { - CreateNSWindow(isDialog); InitialiseNSWindow(); if(hasPosition) @@ -585,6 +586,7 @@ void WindowBaseImpl::InitialiseNSWindow() { [Window setOpaque:false]; + [Window setHasShadow:true]; [Window invalidateShadow]; if (lastMenu != nullptr) { From 041fdb6bc9bcb06711c7715692e2d5771ad6a2eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 31 May 2022 14:11:19 +0100 Subject: [PATCH 33/96] call bring to front when window is made key. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 60fdb26121..52ee48317c 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -274,6 +274,11 @@ [super becomeKeyWindow]; } +- (void)windowDidBecomeKey:(NSNotification *)notification +{ + _parent->BringToFront(); +} + - (void)windowDidMiniaturize:(NSNotification *)notification { auto parent = dynamic_cast(_parent.operator->()); From 2ea49defb60cf0a8412dabde876a2d8c27e1d8c5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 31 May 2022 14:28:44 +0100 Subject: [PATCH 34/96] [osx] easily support using nspanel from windowbaseimpl. --- native/Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 +- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 62c0e2069d..4220811fc7 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -26,7 +26,7 @@ BEGIN_INTERFACE_MAP() virtual ~WindowBaseImpl(); - WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl); + WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, bool usePanel = false); virtual HRESULT ObtainNSWindowHandle(void **ret) override; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index d105f4cf38..e88c7f208c 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -21,7 +21,7 @@ WindowBaseImpl::~WindowBaseImpl() { Window = nullptr; } -WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) { +WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, bool usePanel) { _shown = false; _inResize = false; BaseEvents = events; @@ -38,7 +38,7 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) lastMenu = nullptr; - CreateNSWindow(false); + CreateNSWindow(usePanel); InitialiseNSWindow(); } From 76abbc8fbef53c161763e0387d46ace4b22819e0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 31 May 2022 16:34:22 +0100 Subject: [PATCH 35/96] ensure frameSize is refreshed when window is show. --- src/Avalonia.Controls/TopLevel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 57fb82485c..f2e8cdb1cf 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -484,7 +484,11 @@ namespace Avalonia.Controls /// Raises the event. /// /// The event args. - protected virtual void OnOpened(EventArgs e) => Opened?.Invoke(this, e); + protected virtual void OnOpened(EventArgs e) + { + FrameSize = PlatformImpl?.FrameSize; + Opened?.Invoke(this, e); + } /// /// Raises the event. From 67b6811ca8d18ed7e4063cb712577a260a0de232 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 31 May 2022 16:34:33 +0100 Subject: [PATCH 36/96] add comment to show why test isnt working. --- samples/IntegrationTestApp/ShowWindowTest.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 9b55864caa..f5f87c5c1c 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -25,7 +25,7 @@ namespace IntegrationTestApp this.GetControl("FrameSize").Text = $"{FrameSize}"; this.GetControl("Position").Text = $"{Position}"; this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; - this.GetControl("Scaling").Text = $"{((IRenderRoot)this).RenderScaling}"; + this.GetControl("Scaling").Text = $"{((IRenderRoot)this).RenderScaling}"; // TODO Use DesktopScaling from WindowImpl. if (Owner is not null) { From cef238d1950e7ec195f4d8f201181bde29b7f207 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 1 Jun 2022 10:10:43 +0100 Subject: [PATCH 37/96] dispatch bring to front parent when removing a child window. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 8330f4ed86..7776b2912d 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -100,6 +100,11 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) { if(_parent != nullptr) { _parent->_children.remove(this); + auto parent = _parent; + + dispatch_async(dispatch_get_main_queue(), ^{ + parent->BringToFront(); + }); } auto cparent = dynamic_cast(parent); From f017acae5685fe8b7c5f9fdf9cbd4308dea08c2e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 1 Jun 2022 10:15:54 +0100 Subject: [PATCH 38/96] dialogs should not be minimizable. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 7776b2912d..8520e3e470 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -590,7 +590,7 @@ NSWindowStyleMask WindowImpl::GetStyle() { break; } - if ([Window parentWindow] == nullptr) { + if (!IsDialog()) { s |= NSWindowStyleMaskMiniaturizable; } From 0d6e3a55f016bf521263735eac066d26bab37d47 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 1 Jun 2022 17:07:02 +0100 Subject: [PATCH 39/96] add test to show that osx chrome buttons are disabled when modal dialog is opened. --- .../WindowTests.cs | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 771faa1925..a82c46b927 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -22,6 +22,63 @@ namespace Avalonia.IntegrationTests.Appium tab.Click(); } + [PlatformFact(SkipOnWindows = true)] + public void OSX_Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown() + { + var mainWindowHandle = GetCurrentWindowHandleHack(); + + try + { + var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); + var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); + var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var showButton = _session.FindElementByAccessibilityId("ShowWindow"); + + var closeButton = + _session.FindElementByXPath( + "/XCUIElementTypeApplication/XCUIElementTypeWindow/XCUIElementTypeButton[1]"); + + var zoomButton = + _session.FindElementByXPath( + "/XCUIElementTypeApplication/XCUIElementTypeWindow/XCUIElementTypeButton[2]"); + + var miniturizeButton = + _session.FindElementByXPath( + "/XCUIElementTypeApplication/XCUIElementTypeWindow/XCUIElementTypeButton[3]"); + + + Assert.True(closeButton.Enabled); + Assert.True(zoomButton.Enabled); + Assert.True(miniturizeButton.Enabled); + + sizeTextBox.SendKeys("400, 400"); + + modeComboBox.Click(); + _session.FindElementByName(ShowWindowMode.Modal.ToString()).SendClick(); + + locationComboBox.Click(); + _session.FindElementByName(WindowStartupLocation.CenterOwner.ToString()).SendClick(); + + showButton.Click(); + + SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); + + Assert.False(closeButton.Enabled); + Assert.False(zoomButton.Enabled); + Assert.False(miniturizeButton.Enabled); + } + finally + { + try + { + var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); + closeButton.Click(); + SwitchToMainWindowHack(mainWindowHandle); + } + catch { } + } + } + [Theory] [MemberData(nameof(StartupLocationData))] public void StartupLocation(string? size, ShowWindowMode mode, WindowStartupLocation location) @@ -52,7 +109,7 @@ namespace Avalonia.IntegrationTests.Appium var position = PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text); var screenRect = PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text); var scaling = double.Parse(_session.FindElementByAccessibilityId("Scaling").Text); - + Assert.True(frameSize.Width >= clientSize.Width, "Expected frame width >= client width."); Assert.True(frameSize.Height > clientSize.Height, "Expected frame height > client height."); From b7397a7cdf710fe17d49ee3993196fefd734d74b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 1 Jun 2022 17:56:36 +0100 Subject: [PATCH 40/96] add a11y identifiers for windows. --- samples/IntegrationTestApp/MainWindow.axaml | 1 + samples/IntegrationTestApp/ShowWindowTest.axaml | 1 + 2 files changed, 2 insertions(+) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 5151d80932..82348691e9 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="IntegrationTestApp.MainWindow" + Name="MainWindow" Title="IntegrationTestApp"> diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index e87ae0f32c..2525bfc866 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -1,6 +1,7 @@ From a7713dcda9d0b40d9f1dbcbd21ce2d4cea94f9ef Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 1 Jun 2022 17:56:52 +0100 Subject: [PATCH 41/96] add a unit test for child window order osx. --- .../WindowTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index a82c46b927..1f5ac052ec 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -2,7 +2,9 @@ using System.Linq; using System.Runtime.InteropServices; using Avalonia.Controls; +using OpenQA.Selenium; using OpenQA.Selenium.Appium; +using SeleniumExtras.PageObjects; using Xunit; using Xunit.Sdk; @@ -21,6 +23,79 @@ namespace Avalonia.IntegrationTests.Appium var tab = tabs.FindElementByName("Window"); tab.Click(); } + + [PlatformFact(SkipOnWindows = true)] + public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent() + { + var mainWindowHandle = GetCurrentWindowHandleHack(); + + try + { + var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); + var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); + var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var showButton = _session.FindElementByAccessibilityId("ShowWindow"); + + var mainWindow = + _session.FindElementByAccessibilityId("MainWindow"); + + + + sizeTextBox.SendKeys("200, 100"); + + modeComboBox.Click(); + _session.FindElementByName(ShowWindowMode.Modal.ToString()).SendClick(); + + locationComboBox.Click(); + _session.FindElementByName(WindowStartupLocation.CenterOwner.ToString()).SendClick(); + + showButton.Click(); + + var secondaryWindow = _session.FindElementByAccessibilityId("SecondaryWindow"); + + mainWindow.Click(); + + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); + + int i = 0; + int mainWindowIndex = 0; + int secondaryWindowIndex = 0; + + foreach (var window in windows) + { + i++; + + var child = window.FindElementByXPath("XCUIElementTypeWindow"); + + switch (child.GetAttribute("identifier")) + { + case "MainWindow": + mainWindowIndex = i; + break; + + case "SecondaryWindow": + secondaryWindowIndex = i; + break; + } + + } + + Assert.Equal(1, secondaryWindowIndex); + Assert.Equal(2, mainWindowIndex); + + SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); + } + finally + { + try + { + var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); + closeButton.Click(); + SwitchToMainWindowHack(mainWindowHandle); + } + catch { } + } + } [PlatformFact(SkipOnWindows = true)] public void OSX_Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown() From a70dadea18bd0ff1fb9e0b1a6fa657659eb608a4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 1 Jun 2022 18:17:15 +0100 Subject: [PATCH 42/96] simplify test. --- .../WindowTests.cs | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 1f5ac052ec..8d6be4d199 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Controls; @@ -39,8 +40,6 @@ namespace Avalonia.IntegrationTests.Appium var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - - sizeTextBox.SendKeys("200, 100"); modeComboBox.Click(); @@ -51,37 +50,15 @@ namespace Avalonia.IntegrationTests.Appium showButton.Click(); - var secondaryWindow = _session.FindElementByAccessibilityId("SecondaryWindow"); - mainWindow.Click(); var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - int i = 0; - int mainWindowIndex = 0; - int secondaryWindowIndex = 0; + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); - foreach (var window in windows) - { - i++; - - var child = window.FindElementByXPath("XCUIElementTypeWindow"); - - switch (child.GetAttribute("identifier")) - { - case "MainWindow": - mainWindowIndex = i; - break; - - case "SecondaryWindow": - secondaryWindowIndex = i; - break; - } - - } - - Assert.Equal(1, secondaryWindowIndex); - Assert.Equal(2, mainWindowIndex); + Assert.Equal(0, secondaryWindowIndex); + Assert.Equal(1, mainWindowIndex); SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); } @@ -293,4 +270,13 @@ namespace Avalonia.IntegrationTests.Appium Modal } } + + static class Extensions + { + public static int GetWindowOrder(this IReadOnlyCollection elements, string identifier) + { + return elements.TakeWhile(x => + x.FindElementByXPath("XCUIElementTypeWindow")?.GetAttribute("identifier") != identifier).Count(); + } + } } From 1917def95927cb95d55a1d08ea14943cd5e03849 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 2 Jun 2022 14:15:02 +0100 Subject: [PATCH 43/96] full set of window tests for osx. --- .../ShowWindowTest.axaml.cs | 2 +- .../WindowTests.cs | 226 +++++++++++++----- 2 files changed, 165 insertions(+), 63 deletions(-) diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index f5f87c5c1c..3f45f1c5ad 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -25,7 +25,7 @@ namespace IntegrationTestApp this.GetControl("FrameSize").Text = $"{FrameSize}"; this.GetControl("Position").Text = $"{Position}"; this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; - this.GetControl("Scaling").Text = $"{((IRenderRoot)this).RenderScaling}"; // TODO Use DesktopScaling from WindowImpl. + this.GetControl("Scaling").Text = $"{PlatformImpl?.DesktopScaling}"; if (Owner is not null) { diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 8d6be4d199..b027c17ff3 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Runtime.InteropServices; using Avalonia.Controls; using OpenQA.Selenium; @@ -24,53 +25,117 @@ namespace Avalonia.IntegrationTests.Appium var tab = tabs.FindElementByName("Window"); tab.Click(); } + + private IDisposable OpenWindow(ShowWindowMode mode, WindowStartupLocation location, int width = 200, int height = 100) + { + var mainWindow = GetCurrentWindowHandleHack(); + var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); + var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); + var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var showButton = _session.FindElementByAccessibilityId("ShowWindow"); + + sizeTextBox.SendKeys($"{width}, {height}"); + + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + + showButton.Click(); + + return Disposable.Create(() => + { + try + { + SwitchToNewWindowHack(mainWindow); + var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); + closeButton.SendClick(); + } + catch { } + }); + } [PlatformFact(SkipOnWindows = true)] public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent() { var mainWindowHandle = GetCurrentWindowHandleHack(); + var mainWindow = + _session.FindElementByAccessibilityId("MainWindow"); + try { - var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); - var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); - var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); - var showButton = _session.FindElementByAccessibilityId("ShowWindow"); + using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + { + mainWindow.Click(); - var mainWindow = - _session.FindElementByAccessibilityId("MainWindow"); - - sizeTextBox.SendKeys("200, 100"); + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - modeComboBox.Click(); - _session.FindElementByName(ShowWindowMode.Modal.ToString()).SendClick(); + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); - locationComboBox.Click(); - _session.FindElementByName(WindowStartupLocation.CenterOwner.ToString()).SendClick(); + Assert.Equal(0, secondaryWindowIndex); + Assert.Equal(1, mainWindowIndex); + } + } + finally + { + SwitchToMainWindowHack(mainWindowHandle); + } + } + + [PlatformFact(SkipOnWindows = true)] + public void OSX_WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent() + { + var mainWindowHandle = GetCurrentWindowHandleHack(); + + var mainWindow = + _session.FindElementByAccessibilityId("MainWindow"); - showButton.Click(); + try + { + using (OpenWindow(ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) + { + mainWindow.Click(); + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); + + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); + + Assert.Equal(0, secondaryWindowIndex); + Assert.Equal(1, mainWindowIndex); + } + } + finally + { + SwitchToMainWindowHack(mainWindowHandle); + } + } + + [PlatformFact(SkipOnWindows = true)] + public void OSX_WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent() + { + var mainWindow = + _session.FindElementByAccessibilityId("MainWindow"); + + using (OpenWindow(ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner, 1400)) + { mainWindow.Click(); + var secondaryWindow = + _session.FindElementByAccessibilityId("SecondaryWindow"); + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); int mainWindowIndex = windows.GetWindowOrder("MainWindow"); int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); - - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); - SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); - } - finally - { - try - { - var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); - closeButton.Click(); - SwitchToMainWindowHack(mainWindowHandle); - } - catch { } + Assert.Equal(1, secondaryWindowIndex); + Assert.Equal(0, mainWindowIndex); + + secondaryWindow.SendClick(); } } @@ -81,53 +146,54 @@ namespace Avalonia.IntegrationTests.Appium try { - var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); - var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); - var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); - var showButton = _session.FindElementByAccessibilityId("ShowWindow"); + var window = _session.FindWindowOuter("MainWindow"); - var closeButton = - _session.FindElementByXPath( - "/XCUIElementTypeApplication/XCUIElementTypeWindow/XCUIElementTypeButton[1]"); - - var zoomButton = - _session.FindElementByXPath( - "/XCUIElementTypeApplication/XCUIElementTypeWindow/XCUIElementTypeButton[2]"); - - var miniturizeButton = - _session.FindElementByXPath( - "/XCUIElementTypeApplication/XCUIElementTypeWindow/XCUIElementTypeButton[3]"); - + var (closeButton, zoomButton, miniturizeButton) + = window.GetChromeButtons(); Assert.True(closeButton.Enabled); Assert.True(zoomButton.Enabled); Assert.True(miniturizeButton.Enabled); - - sizeTextBox.SendKeys("400, 400"); - modeComboBox.Click(); - _session.FindElementByName(ShowWindowMode.Modal.ToString()).SendClick(); - - locationComboBox.Click(); - _session.FindElementByName(WindowStartupLocation.CenterOwner.ToString()).SendClick(); + using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + { + SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); - showButton.Click(); - - SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); - - Assert.False(closeButton.Enabled); - Assert.False(zoomButton.Enabled); - Assert.False(miniturizeButton.Enabled); + Assert.False(closeButton.Enabled); + Assert.False(zoomButton.Enabled); + Assert.False(miniturizeButton.Enabled); + } } finally { - try + SwitchToMainWindowHack(mainWindowHandle); + } + } + + [PlatformFact(SkipOnWindows = true)] + public void OSX_Minimize_Button_Disabled_Modal_Dialog() + { + var mainWindowHandle = GetCurrentWindowHandleHack(); + + try + { + using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { - var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); - closeButton.Click(); - SwitchToMainWindowHack(mainWindowHandle); + var secondaryWindow = _session.FindWindowOuter("SecondaryWindow"); + + var (closeButton, zoomButton, miniturizeButton) + = secondaryWindow.GetChromeButtons(); + + Assert.True(closeButton.Enabled); + Assert.True(zoomButton.Enabled); + Assert.False(miniturizeButton.Enabled); + + SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); } - catch { } + } + finally + { + SwitchToMainWindowHack(mainWindowHandle); } } @@ -257,6 +323,13 @@ namespace Avalonia.IntegrationTests.Appium if (Math.Abs(expected.Y - actual.Y) > 10) throw new EqualException(expected, actual); } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + if (Math.Abs(expected.X - actual.X) > 15) + throw new EqualException(expected, actual); + if (Math.Abs(expected.Y - actual.Y) > 15) + throw new EqualException(expected, actual); + } else { Assert.Equal(expected, actual); @@ -278,5 +351,34 @@ namespace Avalonia.IntegrationTests.Appium return elements.TakeWhile(x => x.FindElementByXPath("XCUIElementTypeWindow")?.GetAttribute("identifier") != identifier).Count(); } + + public static AppiumWebElement? FindWindowInner(this AppiumDriver session, string identifier) + { + return session.FindElementsByXPath("XCUIElementTypeWindow") + .FirstOrDefault(x => x.GetAttribute("identifier") == identifier); + } + + public static AppiumWebElement? FindWindowOuter(this AppiumDriver session, string identifier) + { + var windows = session.FindElementsByXPath("XCUIElementTypeWindow"); + + var window = windows.FirstOrDefault(x=>x.FindElementsByXPath("XCUIElementTypeWindow").Any(x => x.GetAttribute("identifier") == identifier)); + + return window; + } + + public static (AppiumWebElement? closeButton, AppiumWebElement? zoomButton, AppiumWebElement? miniturizeButton) GetChromeButtons (this AppiumWebElement outerWindow) + { + var closeButton = + outerWindow.FindElementByXPath("XCUIElementTypeButton[1]"); + + var zoomButton = + outerWindow.FindElementByXPath("XCUIElementTypeButton[2]"); + + var miniturizeButton = + outerWindow.FindElementByXPath("XCUIElementTypeButton[3]"); + + return (closeButton, zoomButton, miniturizeButton); + } } } From 827692fa272aaa841d352d5aef3d5b5f9c5614dd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 2 Jun 2022 22:36:22 +0100 Subject: [PATCH 44/96] add test for osx modal dialog window order when clicking resize grip of parent. --- .../WindowTests.cs | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index b027c17ff3..59e705bd9b 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Runtime.InteropServices; +using System.Threading.Tasks; using Avalonia.Controls; using OpenQA.Selenium; using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Interactions; using SeleniumExtras.PageObjects; using Xunit; using Xunit.Sdk; @@ -50,7 +52,7 @@ namespace Avalonia.IntegrationTests.Appium { SwitchToNewWindowHack(mainWindow); var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); - closeButton.SendClick(); + closeButton.Click(); } catch { } }); @@ -85,6 +87,43 @@ namespace Avalonia.IntegrationTests.Appium } } + [PlatformFact(SkipOnWindows = true)] + public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() + { + var mainWindowHandle = GetCurrentWindowHandleHack(); + + var mainWindow = + _session.FindWindowOuter("MainWindow"); + + try + { + using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + { + new Actions(_session) + .MoveToElement(mainWindow, 100, 1) + .ClickAndHold() + .Perform(); + + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); + + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); + + new Actions(_session) + .MoveToElement(mainWindow, 100, 1) + .Release() + .Perform(); + + Assert.Equal(0, secondaryWindowIndex); + Assert.Equal(1, mainWindowIndex); + } + } + finally + { + SwitchToMainWindowHack(mainWindowHandle); + } + } + [PlatformFact(SkipOnWindows = true)] public void OSX_WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent() { From a0af269d36b6300e941b1d9196746c08668528ec Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 2 Jun 2022 23:02:37 +0100 Subject: [PATCH 45/96] reset app after most tests, add test for fullscreen mode with modal osx. --- .../WindowTests.cs | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 59e705bd9b..8bf429c2ed 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -84,6 +84,7 @@ namespace Avalonia.IntegrationTests.Appium finally { SwitchToMainWindowHack(mainWindowHandle); + _session.ResetApp(); } } @@ -121,6 +122,41 @@ namespace Avalonia.IntegrationTests.Appium finally { SwitchToMainWindowHack(mainWindowHandle); + _session.ResetApp(); + } + } + + [PlatformFact(SkipOnWindows = true)] + public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen() + { + var mainWindowHandle = GetCurrentWindowHandleHack(); + + var mainWindow = + _session.FindWindowOuter("MainWindow"); + + var buttons = mainWindow.GetChromeButtons(); + + buttons.zoomButton.Click(); + + Task.Delay(500).Wait(); + + try + { + using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + { + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); + + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); + + Assert.Equal(0, secondaryWindowIndex); + Assert.Equal(1, mainWindowIndex); + } + } + finally + { + SwitchToMainWindowHack(mainWindowHandle); + _session.ResetApp(); } } @@ -150,31 +186,39 @@ namespace Avalonia.IntegrationTests.Appium finally { SwitchToMainWindowHack(mainWindowHandle); + _session.ResetApp(); } } - + [PlatformFact(SkipOnWindows = true)] public void OSX_WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent() { var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - using (OpenWindow(ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner, 1400)) + try { - mainWindow.Click(); - - var secondaryWindow = - _session.FindElementByAccessibilityId("SecondaryWindow"); + using (OpenWindow(ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner, 1400)) + { + mainWindow.Click(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); + var secondaryWindow = + _session.FindElementByAccessibilityId("SecondaryWindow"); - int mainWindowIndex = windows.GetWindowOrder("MainWindow"); - int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - Assert.Equal(1, secondaryWindowIndex); - Assert.Equal(0, mainWindowIndex); - - secondaryWindow.SendClick(); + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); + + Assert.Equal(1, secondaryWindowIndex); + Assert.Equal(0, mainWindowIndex); + + secondaryWindow.SendClick(); + } + } + finally + { + _session.ResetApp(); } } @@ -206,6 +250,7 @@ namespace Avalonia.IntegrationTests.Appium finally { SwitchToMainWindowHack(mainWindowHandle); + _session.ResetApp(); } } @@ -233,6 +278,7 @@ namespace Avalonia.IntegrationTests.Appium finally { SwitchToMainWindowHack(mainWindowHandle); + _session.ResetApp(); } } From 860fcc524d4be5ad292ce09713f0dd9fd6ac7329 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 21 Jun 2022 13:46:35 +0200 Subject: [PATCH 46/96] Refactor how we show windows. Trying to make it a little less hacky. Only tested on Win32 so far. --- .../ElementExtensions.cs | 63 ++++++++ .../WindowTests.cs | 144 +++++++----------- 2 files changed, 114 insertions(+), 93 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index 3eb8646835..07fea24a93 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; using System.Runtime.InteropServices; +using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; +using Xunit; namespace Avalonia.IntegrationTests.Appium { @@ -43,6 +47,65 @@ namespace Avalonia.IntegrationTests.Appium } } + /// + /// Clicks a button which is expected to open a new window. + /// + /// The button to click. + /// + /// An object which when disposed will cause the newly opened window to close. + /// + public static IDisposable OpenWindowWithClick(this AppiumWebElement element) + { + var session = element.WrappedDriver; + var oldHandle = session.CurrentWindowHandle; + var oldHandles = session.WindowHandles.ToList(); + var oldChildWindows = session.FindElements(By.XPath("//Window")); + + element.Click(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var newHandle = session.WindowHandles.Except(oldHandles).SingleOrDefault(); + + if (newHandle is not null) + { + // A new top-level window was opened. We need to switch to it. + session.SwitchTo().Window(newHandle); + + return Disposable.Create(() => + { + session.Close(); + session.SwitchTo().Window(oldHandle); + }); + } + else + { + // If a new window handle hasn't been added to the session then it's likely + // that a child window was opened. These don't appear in session.WindowHandles + // so we have to use an XPath query to get hold of it. + var newChildWindows = session.FindElements(By.XPath("//Window")); + var childWindow = Assert.Single(newChildWindows.Except(oldChildWindows)); + + return Disposable.Create(() => + { + childWindow.SendKeys(Keys.Alt + Keys.F4 + Keys.Alt); + }); + } + } + else + { + var newHandle = session.CurrentWindowHandle; + + Assert.NotEqual(oldHandle, newHandle); + + return Disposable.Create(() => + { + session.Close(); + session.SwitchTo().Window(oldHandle); + }); + } + } + public static void SendClick(this AppiumWebElement element) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 8bf429c2ed..38d0d5ba1c 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -8,7 +8,6 @@ using Avalonia.Controls; using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; -using SeleniumExtras.PageObjects; using Xunit; using Xunit.Sdk; @@ -28,36 +27,33 @@ namespace Avalonia.IntegrationTests.Appium tab.Click(); } - private IDisposable OpenWindow(ShowWindowMode mode, WindowStartupLocation location, int width = 200, int height = 100) + [Theory] + [MemberData(nameof(StartupLocationData))] + public void StartupLocation(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) { - var mainWindow = GetCurrentWindowHandleHack(); - var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); - var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); - var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); - var showButton = _session.FindElementByAccessibilityId("ShowWindow"); - - sizeTextBox.SendKeys($"{width}, {height}"); + using var window = OpenWindow(size, mode, location); + var clientSize = Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text); + var frameSize = Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text); + var position = PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text); + var screenRect = PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text); + var scaling = double.Parse(_session.FindElementByAccessibilityId("Scaling").Text); - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); + Assert.True(frameSize.Width >= clientSize.Width, "Expected frame width >= client width."); + Assert.True(frameSize.Height > clientSize.Height, "Expected frame height > client height."); - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); + var frameRect = new PixelRect(position, PixelSize.FromSize(frameSize, scaling)); - showButton.Click(); - - return Disposable.Create(() => + switch (location) { - try - { - SwitchToNewWindowHack(mainWindow); - var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); - closeButton.Click(); - } - catch { } - }); + case WindowStartupLocation.CenterScreen: + { + var expected = screenRect.CenterRect(frameRect); + AssertCloseEnough(expected.Position, frameRect.Position); + break; + } + } } - + [PlatformFact(SkipOnWindows = true)] public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent() { @@ -68,7 +64,7 @@ namespace Avalonia.IntegrationTests.Appium try { - using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { mainWindow.Click(); @@ -98,7 +94,7 @@ namespace Avalonia.IntegrationTests.Appium try { - using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { new Actions(_session) .MoveToElement(mainWindow, 100, 1) @@ -142,7 +138,7 @@ namespace Avalonia.IntegrationTests.Appium try { - using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); @@ -170,7 +166,7 @@ namespace Avalonia.IntegrationTests.Appium try { - using (OpenWindow(ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) { mainWindow.Click(); @@ -198,7 +194,7 @@ namespace Avalonia.IntegrationTests.Appium try { - using (OpenWindow(ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner, 1400)) + using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner)) { mainWindow.Click(); @@ -238,7 +234,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.True(zoomButton.Enabled); Assert.True(miniturizeButton.Enabled); - using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); @@ -261,7 +257,7 @@ namespace Avalonia.IntegrationTests.Appium try { - using (OpenWindow(ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { var secondaryWindow = _session.FindWindowOuter("SecondaryWindow"); @@ -281,69 +277,11 @@ namespace Avalonia.IntegrationTests.Appium _session.ResetApp(); } } - - [Theory] - [MemberData(nameof(StartupLocationData))] - public void StartupLocation(string? size, ShowWindowMode mode, WindowStartupLocation location) - { - var mainWindowHandle = GetCurrentWindowHandleHack(); - - try - { - var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); - var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); - var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); - var showButton = _session.FindElementByAccessibilityId("ShowWindow"); - - if (size is not null) - sizeTextBox.SendKeys(size); - - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); - - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); - - showButton.Click(); - SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); - - var clientSize = Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text); - var frameSize = Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text); - var position = PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text); - var screenRect = PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text); - var scaling = double.Parse(_session.FindElementByAccessibilityId("Scaling").Text); - - Assert.True(frameSize.Width >= clientSize.Width, "Expected frame width >= client width."); - Assert.True(frameSize.Height > clientSize.Height, "Expected frame height > client height."); - - var frameRect = new PixelRect(position, PixelSize.FromSize(frameSize, scaling)); - - switch (location) - { - case WindowStartupLocation.CenterScreen: - { - var expected = screenRect.CenterRect(frameRect); - AssertCloseEnough(expected.Position, frameRect.Position); - break; - } - } - } - finally - { - try - { - var closeButton = _session.FindElementByAccessibilityId("CloseWindow"); - closeButton.Click(); - SwitchToMainWindowHack(mainWindowHandle); - } - catch { } - } - } - public static TheoryData StartupLocationData() + public static TheoryData StartupLocationData() { - var sizes = new[] { null, "400,300" }; - var data = new TheoryData(); + var sizes = new PixelSize?[] { null, new PixelSize(400, 300) }; + var data = new TheoryData(); foreach (var size in sizes) { @@ -421,6 +359,26 @@ namespace Avalonia.IntegrationTests.Appium } } + private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) + { + var mainWindow = GetCurrentWindowHandleHack(); + var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); + var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); + var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var showButton = _session.FindElementByAccessibilityId("ShowWindow"); + + if (size.HasValue) + sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); + + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + + return showButton.OpenWindowWithClick(); + } + public enum ShowWindowMode { NonOwned, From 8c3424baa4e0beeb51a4b8a65fd7055be8507cd1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 10:36:49 +0200 Subject: [PATCH 47/96] Refactor OSX window tests a bit. Try not to rely on `_session.ResetApp();` because restoring the application state in the tests tends to show up more errors (which this change has done: https://github.com/AvaloniaUI/Avalonia/issues/8335#issuecomment-1162804733) --- samples/IntegrationTestApp/MainWindow.axaml | 2 + .../IntegrationTestApp/MainWindow.axaml.cs | 15 ++ .../IntegrationTestApp/ShowWindowTest.axaml | 2 - .../ShowWindowTest.axaml.cs | 2 - .../ElementExtensions.cs | 42 +++- .../WindowTests.cs | 210 ++++++------------ 6 files changed, 116 insertions(+), 157 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 82348691e9..aa2191c26b 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -110,6 +110,8 @@ CenterOwner + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 580548a433..1aba10ec30 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; @@ -82,6 +83,16 @@ namespace IntegrationTestApp } } + private void SendToBack() + { + var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; + + foreach (var window in lifetime.Windows) + { + window.Activate(); + } + } + private void MenuClicked(object? sender, RoutedEventArgs e) { var clickedMenuItemTextBlock = this.FindControl("ClickedMenuItem"); @@ -102,6 +113,10 @@ namespace IntegrationTestApp this.FindControl("ClickedMenuItem").Text = "None"; if (source?.Name == "ShowWindow") ShowWindow(); + if (source?.Name == "SendToBack") + SendToBack(); + if (source?.Name == "ExitFullscreen") + WindowState = WindowState.Normal; } } } diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 2525bfc866..a263d8ab46 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -4,8 +4,6 @@ Name="SecondaryWindow" Title="Show Window Test"> - - diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 3f45f1c5ad..720f7b1c12 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -34,7 +34,5 @@ namespace IntegrationTestApp ownerRect.Text = $"{owner.Position}, {owner.FrameSize}"; } } - - private void CloseWindow_Click(object sender, RoutedEventArgs e) => Close(); } } diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index 07fea24a93..16d37e4beb 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -15,6 +15,19 @@ namespace Avalonia.IntegrationTests.Appium public static IReadOnlyList GetChildren(this AppiumWebElement element) => element.FindElementsByXPath("*/*"); + public static (AppiumWebElement close, AppiumWebElement minimize, AppiumWebElement maximize) GetChromeButtons(this AppiumWebElement window) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var closeButton = window.FindElementByXPath("//XCUIElementTypeButton[1]"); + var fullscreenButton = window.FindElementByXPath("//XCUIElementTypeButton[2]"); + var minimizeButton = window.FindElementByXPath("//XCUIElementTypeButton[3]"); + return (closeButton, minimizeButton, fullscreenButton); + } + + throw new NotSupportedException("GetChromeButtons not supported on this platform."); + } + public static string GetComboBoxValue(this AppiumWebElement element) { return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @@ -57,14 +70,15 @@ namespace Avalonia.IntegrationTests.Appium public static IDisposable OpenWindowWithClick(this AppiumWebElement element) { var session = element.WrappedDriver; - var oldHandle = session.CurrentWindowHandle; - var oldHandles = session.WindowHandles.ToList(); - var oldChildWindows = session.FindElements(By.XPath("//Window")); - - element.Click(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + var oldHandle = session.CurrentWindowHandle; + var oldHandles = session.WindowHandles.ToList(); + var oldChildWindows = session.FindElements(By.XPath("//Window")); + + element.Click(); + var newHandle = session.WindowHandles.Except(oldHandles).SingleOrDefault(); if (newHandle is not null) @@ -94,14 +108,22 @@ namespace Avalonia.IntegrationTests.Appium } else { - var newHandle = session.CurrentWindowHandle; - - Assert.NotEqual(oldHandle, newHandle); + var oldWindows = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow")); + var oldWindowTitles = oldWindows.ToDictionary(x => x.Text); + + element.Click(); + var newWindows = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow")); + var newWindowTitles = newWindows.ToDictionary(x => x.Text); + var newWindowTitle = Assert.Single(newWindowTitles.Keys.Except(oldWindowTitles.Keys)); + var newWindow = (AppiumWebElement)newWindowTitles[newWindowTitle]; + return Disposable.Create(() => { - session.Close(); - session.SwitchTo().Window(oldHandle); + // TODO: We should be able to use Cmd+W here but Avalonia apps don't seem to have this shortcut + // set up by default. + var (close, _, _) = newWindow.GetChromeButtons(); + close!.Click(); }); } } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 38d0d5ba1c..e887a2afe8 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -57,82 +57,56 @@ namespace Avalonia.IntegrationTests.Appium [PlatformFact(SkipOnWindows = true)] public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent() { - var mainWindowHandle = GetCurrentWindowHandleHack(); - - var mainWindow = - _session.FindElementByAccessibilityId("MainWindow"); + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - try + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { - using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) - { - mainWindow.Click(); + mainWindow.Click(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - int mainWindowIndex = windows.GetWindowOrder("MainWindow"); - int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); - } - } - finally - { - SwitchToMainWindowHack(mainWindowHandle); - _session.ResetApp(); + Assert.Equal(0, secondaryWindowIndex); + Assert.Equal(1, mainWindowIndex); } } [PlatformFact(SkipOnWindows = true)] public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { - var mainWindowHandle = GetCurrentWindowHandleHack(); - - var mainWindow = - _session.FindWindowOuter("MainWindow"); + var mainWindow = _session.FindWindowOuter("MainWindow"); - try + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { - using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) - { - new Actions(_session) - .MoveToElement(mainWindow, 100, 1) - .ClickAndHold() - .Perform(); + new Actions(_session) + .MoveToElement(mainWindow, 100, 1) + .ClickAndHold() + .Perform(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - int mainWindowIndex = windows.GetWindowOrder("MainWindow"); - int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); - new Actions(_session) - .MoveToElement(mainWindow, 100, 1) - .Release() - .Perform(); + new Actions(_session) + .MoveToElement(mainWindow, 100, 1) + .Release() + .Perform(); - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); - } - } - finally - { - SwitchToMainWindowHack(mainWindowHandle); - _session.ResetApp(); + Assert.Equal(0, secondaryWindowIndex); + Assert.Equal(1, mainWindowIndex); } } [PlatformFact(SkipOnWindows = true)] public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen() { - var mainWindowHandle = GetCurrentWindowHandleHack(); - - var mainWindow = - _session.FindWindowOuter("MainWindow"); - + var mainWindow = _session.FindWindowOuter("MainWindow"); var buttons = mainWindow.GetChromeButtons(); - buttons.zoomButton.Click(); + buttons.maximize.Click(); Task.Delay(500).Wait(); @@ -151,70 +125,51 @@ namespace Avalonia.IntegrationTests.Appium } finally { - SwitchToMainWindowHack(mainWindowHandle); - _session.ResetApp(); + _session.FindElementByAccessibilityId("ExitFullscreen").Click(); } } [PlatformFact(SkipOnWindows = true)] public void OSX_WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent() { - var mainWindowHandle = GetCurrentWindowHandleHack(); - - var mainWindow = - _session.FindElementByAccessibilityId("MainWindow"); + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - try + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) { - using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) - { - mainWindow.Click(); + mainWindow.Click(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - int mainWindowIndex = windows.GetWindowOrder("MainWindow"); - int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); - } - } - finally - { - SwitchToMainWindowHack(mainWindowHandle); - _session.ResetApp(); + Assert.Equal(0, secondaryWindowIndex); + Assert.Equal(1, mainWindowIndex); } } [PlatformFact(SkipOnWindows = true)] public void OSX_WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent() { - var mainWindow = - _session.FindElementByAccessibilityId("MainWindow"); + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - try + using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner)) { - using (OpenWindow(new PixelSize(1400, 100), ShowWindowMode.NonOwned, WindowStartupLocation.CenterOwner)) - { - mainWindow.Click(); + mainWindow.Click(); - var secondaryWindow = - _session.FindElementByAccessibilityId("SecondaryWindow"); + var secondaryWindow = + _session.FindElementByAccessibilityId("SecondaryWindow"); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); + var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - int mainWindowIndex = windows.GetWindowOrder("MainWindow"); - int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); + int mainWindowIndex = windows.GetWindowOrder("MainWindow"); + int secondaryWindowIndex = windows.GetWindowOrder("SecondaryWindow"); - Assert.Equal(1, secondaryWindowIndex); - Assert.Equal(0, mainWindowIndex); + Assert.Equal(1, secondaryWindowIndex); + Assert.Equal(0, mainWindowIndex); - secondaryWindow.SendClick(); - } - } - finally - { - _session.ResetApp(); + var sendToBack = _session.FindElementByAccessibilityId("SendToBack"); + sendToBack.Click(); } } @@ -222,31 +177,22 @@ namespace Avalonia.IntegrationTests.Appium public void OSX_Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown() { var mainWindowHandle = GetCurrentWindowHandleHack(); - - try - { - var window = _session.FindWindowOuter("MainWindow"); + var window = _session.FindWindowOuter("MainWindow"); - var (closeButton, zoomButton, miniturizeButton) - = window.GetChromeButtons(); + var (closeButton, zoomButton, miniturizeButton) + = window.GetChromeButtons(); - Assert.True(closeButton.Enabled); - Assert.True(zoomButton.Enabled); - Assert.True(miniturizeButton.Enabled); + Assert.True(closeButton.Enabled); + Assert.True(zoomButton.Enabled); + Assert.True(miniturizeButton.Enabled); - using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) - { - SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); - - Assert.False(closeButton.Enabled); - Assert.False(zoomButton.Enabled); - Assert.False(miniturizeButton.Enabled); - } - } - finally + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { - SwitchToMainWindowHack(mainWindowHandle); - _session.ResetApp(); + SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); + + Assert.False(closeButton.Enabled); + Assert.False(zoomButton.Enabled); + Assert.False(miniturizeButton.Enabled); } } @@ -255,26 +201,18 @@ namespace Avalonia.IntegrationTests.Appium { var mainWindowHandle = GetCurrentWindowHandleHack(); - try + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { - using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) - { - var secondaryWindow = _session.FindWindowOuter("SecondaryWindow"); + var secondaryWindow = _session.FindWindowOuter("SecondaryWindow"); - var (closeButton, zoomButton, miniturizeButton) - = secondaryWindow.GetChromeButtons(); - - Assert.True(closeButton.Enabled); - Assert.True(zoomButton.Enabled); - Assert.False(miniturizeButton.Enabled); + var (closeButton, zoomButton, miniaturizeButton) + = secondaryWindow.GetChromeButtons(); - SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); - } - } - finally - { - SwitchToMainWindowHack(mainWindowHandle); - _session.ResetApp(); + Assert.True(closeButton.Enabled); + Assert.True(zoomButton.Enabled); + Assert.False(miniaturizeButton.Enabled); + + SwitchToNewWindowHack(oldWindowHandle: mainWindowHandle); } } @@ -409,19 +347,5 @@ namespace Avalonia.IntegrationTests.Appium return window; } - - public static (AppiumWebElement? closeButton, AppiumWebElement? zoomButton, AppiumWebElement? miniturizeButton) GetChromeButtons (this AppiumWebElement outerWindow) - { - var closeButton = - outerWindow.FindElementByXPath("XCUIElementTypeButton[1]"); - - var zoomButton = - outerWindow.FindElementByXPath("XCUIElementTypeButton[2]"); - - var miniturizeButton = - outerWindow.FindElementByXPath("XCUIElementTypeButton[3]"); - - return (closeButton, zoomButton, miniturizeButton); - } } } From 8b9e675bd4a8ee174c78d54062c1c0b04a22b845 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 10:55:19 +0200 Subject: [PATCH 48/96] Refactor PlatformFact. To make it inclusive rather than exclusive. --- .../ButtonTests.cs | 2 +- .../ComboBoxTests.cs | 6 ++-- .../ListBoxTests.cs | 2 +- .../MenuTests.cs | 16 +++++----- .../NativeMenuTests.cs | 2 +- .../PlatformFactAttribute.cs | 32 +++++++++++++------ .../WindowTests.cs | 14 ++++---- 7 files changed, 43 insertions(+), 31 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs index 2ac859e091..6c630ae782 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs @@ -44,7 +44,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Button with TextBlock", button.Text); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void ButtonWithAcceleratorKey() { var button = _session.FindElementByAccessibilityId("ButtonWithAcceleratorKey"); diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index fad3e1eb9f..abdb4e2dd8 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -46,7 +46,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Item 0", comboBox.GetComboBoxValue()); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Can_Change_Selection_With_Keyboard() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); @@ -63,7 +63,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Item 1", comboBox.GetComboBoxValue()); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Can_Change_Selection_With_Keyboard_From_Unselected() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); @@ -80,7 +80,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Item 0", comboBox.GetComboBoxValue()); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Can_Cancel_Keyboard_Selection_With_Escape() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); diff --git a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs index 625742ac20..e2943b3349 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs @@ -61,7 +61,7 @@ namespace Avalonia.IntegrationTests.Appium } // appium-mac2-driver just hangs - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Can_Select_Range_By_Shift_Clicking() { var listBox = GetTarget(); diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index 98fb335061..d1d231466f 100644 --- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -57,7 +57,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Grandchild", clickedMenuItem.Text); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Select_Child_With_Alt_Arrow_Keys() { new Actions(_session) @@ -69,7 +69,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Child 1", clickedMenuItem.Text); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Select_Grandchild_With_Alt_Arrow_Keys() { new Actions(_session) @@ -81,7 +81,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Grandchild", clickedMenuItem.Text); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Select_Child_With_Alt_Access_Keys() { new Actions(_session) @@ -93,7 +93,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Child 1", clickedMenuItem.Text); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Select_Grandchild_With_Alt_Access_Keys() { new Actions(_session) @@ -105,7 +105,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Grandchild", clickedMenuItem.Text); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Select_Child_With_Click_Arrow_Keys() { var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); @@ -119,7 +119,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Child 1", clickedMenuItem.Text); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Select_Grandchild_With_Click_Arrow_Keys() { var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); @@ -133,7 +133,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Grandchild", clickedMenuItem.Text); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void Child_AcceleratorKey() { var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); @@ -145,7 +145,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Ctrl+O", childMenuItem.GetAttribute("AcceleratorKey")); } - [PlatformFact(SkipOnOSX = true)] + [PlatformFact(TestPlatforms.Windows)] public void PointerOver_Does_Not_Steal_Focus() { // Issue #7906 diff --git a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs index fde01f0e41..7858c4cc81 100644 --- a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs @@ -17,7 +17,7 @@ namespace Avalonia.IntegrationTests.Appium tab.Click(); } - [PlatformFact(SkipOnWindows = true)] + [PlatformFact(TestPlatforms.MacOS)] public void View_Menu_Select_Button_Tab() { var tabs = _session.FindElementByAccessibilityId("MainTabs"); diff --git a/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs index 60338b92c2..53ae5d924f 100644 --- a/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs +++ b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs @@ -5,21 +5,33 @@ using Xunit; namespace Avalonia.IntegrationTests.Appium { + [Flags] + internal enum TestPlatforms + { + Windows = 0x01, + MacOS = 0x02, + All = Windows | MacOS, + } + internal class PlatformFactAttribute : FactAttribute { + public PlatformFactAttribute(TestPlatforms platforms = TestPlatforms.All) => Platforms = platforms; + + public TestPlatforms Platforms { get; } + public override string? Skip { - get - { - if (SkipOnWindows && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return "Ignored on Windows"; - if (SkipOnOSX && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return "Ignored on MacOS"; - return null; - } + get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}"; set => throw new NotSupportedException(); } - public bool SkipOnOSX { get; set; } - public bool SkipOnWindows { get; set; } + + private bool IsSupported() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return Platforms.HasAnyFlag(TestPlatforms.Windows); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return Platforms.HasAnyFlag(TestPlatforms.MacOS); + return false; + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index e887a2afe8..b759dafbeb 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -54,7 +54,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(SkipOnWindows = true)] + [PlatformFact(TestPlatforms.MacOS)] public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent() { var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); @@ -73,7 +73,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(SkipOnWindows = true)] + [PlatformFact(TestPlatforms.MacOS)] public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { var mainWindow = _session.FindWindowOuter("MainWindow"); @@ -100,7 +100,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(SkipOnWindows = true)] + [PlatformFact(TestPlatforms.MacOS)] public void OSX_WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen() { var mainWindow = _session.FindWindowOuter("MainWindow"); @@ -129,7 +129,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(SkipOnWindows = true)] + [PlatformFact(TestPlatforms.MacOS)] public void OSX_WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent() { var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); @@ -148,7 +148,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(SkipOnWindows = true)] + [PlatformFact(TestPlatforms.MacOS)] public void OSX_WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent() { var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); @@ -173,7 +173,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(SkipOnWindows = true)] + [PlatformFact(TestPlatforms.MacOS)] public void OSX_Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown() { var mainWindowHandle = GetCurrentWindowHandleHack(); @@ -196,7 +196,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(SkipOnWindows = true)] + [PlatformFact(TestPlatforms.MacOS)] public void OSX_Minimize_Button_Disabled_Modal_Dialog() { var mainWindowHandle = GetCurrentWindowHandleHack(); From 0289a515b38984601d36186a260c44edf5acd8be Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Jun 2022 00:59:16 -0400 Subject: [PATCH 49/96] Add file picker interface definitions --- src/Avalonia.Base/Logging/LogArea.cs | 10 ++ .../Platform/Storage/FileIO/BclStorageFile.cs | 107 ++++++++++++++++++ .../Storage/FileIO/BclStorageFolder.cs | 88 ++++++++++++++ .../Storage/FileIO/BclStorageProvider.cs | 35 ++++++ .../Storage/FileIO/StorageProviderHelpers.cs | 40 +++++++ .../Platform/Storage/FilePickerFileType.cs | 44 +++++++ .../Platform/Storage/FilePickerFileTypes.cs | 48 ++++++++ .../Platform/Storage/FilePickerOpenOptions.cs | 29 +++++ .../Platform/Storage/FilePickerSaveOptions.cs | 39 +++++++ .../Storage/FolderPickerOpenOptions.cs | 22 ++++ .../Platform/Storage/IStorageBookmarkItem.cs | 21 ++++ .../Platform/Storage/IStorageFile.cs | 32 ++++++ .../Platform/Storage/IStorageFolder.cs | 12 ++ .../Platform/Storage/IStorageItem.cs | 53 +++++++++ .../Platform/Storage/IStorageProvider.cs | 56 +++++++++ .../Platform/Storage/StorageItemProperties.cs | 43 +++++++ .../Dialogs/IStorageProviderFactory.cs | 12 ++ .../{ => Dialogs}/ISystemDialogImpl.cs | 2 + .../Platform/Dialogs/SystemDialogImpl.cs | 74 ++++++++++++ .../ITopLevelImplWithStorageProvider.cs | 11 ++ src/Avalonia.Controls/SystemDialog.cs | 57 ++++++++++ src/Avalonia.Controls/TopLevel.cs | 9 +- 22 files changed, 843 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageFile.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageFolder.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageItem.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageProvider.cs create mode 100644 src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs create mode 100644 src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs rename src/Avalonia.Controls/Platform/{ => Dialogs}/ISystemDialogImpl.cs (88%) create mode 100644 src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs create mode 100644 src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index c049f9e763..98ef6d2530 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -44,5 +44,15 @@ namespace Avalonia.Logging /// The log event comes from X11Platform. /// public const string X11Platform = nameof(X11Platform); + + /// + /// The log event comes from AndroidPlatform. + /// + public const string AndroidPlatform = nameof(AndroidPlatform); + + /// + /// The log event comes from IOSPlatform. + /// + public const string IOSPlatform = nameof(IOSPlatform); } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs new file mode 100644 index 0000000000..5af02219ce --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Security; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage.FileIO; + +[Unstable] +public class BclStorageFile : IStorageBookmarkFile +{ + private readonly FileInfo _fileInfo; + + public BclStorageFile(FileInfo fileInfo) + { + _fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); + } + + public bool CanOpenRead => true; + + public bool CanOpenWrite => true; + + public string Name => _fileInfo.Name; + + public virtual bool CanBookmark => true; + + public Task GetBasicPropertiesAsync() + { + var props = new StorageItemProperties(); + if (_fileInfo.Exists) + { + props = new StorageItemProperties( + (ulong)_fileInfo.Length, + _fileInfo.CreationTimeUtc, + _fileInfo.LastAccessTimeUtc); + } + return Task.FromResult(props); + } + + public Task GetParentAsync() + { + if (_fileInfo.Directory is { } directory) + { + return Task.FromResult(new BclStorageFolder(directory)); + } + return Task.FromResult(null); + } + + public Task OpenRead() + { + return Task.FromResult(_fileInfo.OpenRead()); + } + + public Task OpenWrite() + { + return Task.FromResult(_fileInfo.OpenWrite()); + } + + public virtual Task SaveBookmark() + { + return Task.FromResult(_fileInfo.FullName); + } + + public Task ReleaseBookmark() + { + // No-op + return Task.CompletedTask; + } + + public bool TryGetUri([NotNullWhen(true)] out Uri? uri) + { + try + { + if (_fileInfo.Directory is not null) + { + uri = Path.IsPathRooted(_fileInfo.FullName) ? + new Uri(new Uri("file://"), _fileInfo.FullName) : + new Uri(_fileInfo.FullName, UriKind.Relative); + return true; + } + + uri = null; + return false; + } + catch (SecurityException) + { + uri = null; + return false; + } + } + + protected virtual void Dispose(bool disposing) + { + } + + ~BclStorageFile() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs new file mode 100644 index 0000000000..7267017eaf --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -0,0 +1,88 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Security; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage.FileIO; + +[Unstable] +public class BclStorageFolder : IStorageBookmarkFolder +{ + private readonly DirectoryInfo _directoryInfo; + + public BclStorageFolder(DirectoryInfo directoryInfo) + { + _directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); + if (!_directoryInfo.Exists) + { + throw new ArgumentException("Directory must exist", nameof(directoryInfo)); + } + } + + public string Name => _directoryInfo.Name; + + public bool CanBookmark => true; + + public Task GetBasicPropertiesAsync() + { + var props = new StorageItemProperties( + null, + _directoryInfo.CreationTimeUtc, + _directoryInfo.LastAccessTimeUtc); + return Task.FromResult(props); + } + + public Task GetParentAsync() + { + if (_directoryInfo.Parent is { } directory) + { + return Task.FromResult(new BclStorageFolder(directory)); + } + return Task.FromResult(null); + } + + public virtual Task SaveBookmark() + { + return Task.FromResult(_directoryInfo.FullName); + } + + public Task ReleaseBookmark() + { + // No-op + return Task.CompletedTask; + } + + public bool TryGetUri([NotNullWhen(true)] out Uri? uri) + { + try + { + uri = Path.IsPathRooted(_directoryInfo.FullName) ? + new Uri(new Uri("file://"), _directoryInfo.FullName) : + new Uri(_directoryInfo.FullName, UriKind.Relative); + + return true; + } + catch (SecurityException) + { + uri = null; + return false; + } + } + + protected virtual void Dispose(bool disposing) + { + } + + ~BclStorageFolder() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs new file mode 100644 index 0000000000..469388021e --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage.FileIO; + +[Unstable] +public abstract class BclStorageProvider : IStorageProvider +{ + public abstract bool CanOpen { get; } + public abstract Task> OpenFilePickerAsync(FilePickerOpenOptions options); + + public abstract bool CanSave { get; } + public abstract Task SaveFilePickerAsync(FilePickerSaveOptions options); + + public abstract bool CanPickFolder { get; } + public abstract Task> OpenFolderPickerAsync(FolderPickerOpenOptions options); + + public virtual Task OpenFileBookmarkAsync(string bookmark) + { + var file = new FileInfo(bookmark); + return file.Exists + ? Task.FromResult(new BclStorageFile(file)) + : Task.FromResult(null); + } + + public virtual Task OpenFolderBookmarkAsync(string bookmark) + { + var folder = new DirectoryInfo(bookmark); + return folder.Exists + ? Task.FromResult(new BclStorageFolder(folder)) + : Task.FromResult(null); + } +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs new file mode 100644 index 0000000000..f90d0a5a2f --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using System.Linq; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage.FileIO; + +[Unstable] +public static class StorageProviderHelpers +{ + public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter) + { + var name = Path.GetFileName(path); + if (name != null && !Path.HasExtension(name)) + { + if (filter?.Patterns?.Count > 0) + { + if (defaultExtension != null + && filter.Patterns.Contains(defaultExtension)) + { + return Path.ChangeExtension(path, defaultExtension.TrimStart('.')); + } + + var ext = filter.Patterns.FirstOrDefault(x => x != "*.*"); + ext = ext?.Split(new[] { "*." }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault(); + if (ext != null) + { + return Path.ChangeExtension(path, ext); + } + } + + if (defaultExtension != null) + { + return Path.ChangeExtension(path, defaultExtension); + } + } + + return path; + } +} diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs new file mode 100644 index 0000000000..98848ac9f7 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace Avalonia.Platform.Storage; + +/// +/// Represents a name mapped to the associated file types (extensions). +/// +public class FilePickerFileType +{ + public FilePickerFileType(string name) + { + Name = name; + } + + /// + /// File type name. + /// + public string Name { get; } + + /// + /// List of extensions in GLOB format. I.e. "*.png" or "*.*". + /// + /// + /// Used on Windows and Linux systems. + /// + public IReadOnlyList? Patterns { get; set; } + + /// + /// List of extensions in MIME format. + /// + /// + /// Used on Android, Browser and Linux systems. + /// + public IReadOnlyList? MimeTypes { get; set; } + + /// + /// List of extensions in Apple uniform format. + /// + /// + /// Used only on Apple devices. + /// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers. + /// + public IReadOnlyList? AppleUniformTypeIdentifiers { get; set; } +} diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs new file mode 100644 index 0000000000..5da037999a --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs @@ -0,0 +1,48 @@ +namespace Avalonia.Platform.Storage; + +/// +/// Dictionary of well known file types. +/// +public static class FilePickerFileTypes +{ + public static FilePickerFileType All { get; } = new("All") + { + Patterns = new[] { "*.*" }, + MimeTypes = new[] { "*/*" } + }; + + public static FilePickerFileType TextPlain { get; } = new("Plain Text") + { + Patterns = new[] { "*.txt" }, + AppleUniformTypeIdentifiers = new[] { "public.plain-text" }, + MimeTypes = new[] { "text/plain" } + }; + + public static FilePickerFileType ImageAll { get; } = new("All Images") + { + Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" }, + AppleUniformTypeIdentifiers = new[] { "public.image" }, + MimeTypes = new[] { "image/*" } + }; + + public static FilePickerFileType ImageJpg { get; } = new("JPEG image") + { + Patterns = new[] { "*.jpg", "*.jpeg" }, + AppleUniformTypeIdentifiers = new[] { "public.jpeg" }, + MimeTypes = new[] { "image/jpeg" } + }; + + public static FilePickerFileType ImagePng { get; } = new("PNG image") + { + Patterns = new[] { "*.png" }, + AppleUniformTypeIdentifiers = new[] { "public.png" }, + MimeTypes = new[] { "image/png" } + }; + + public static FilePickerFileType Pdf { get; } = new("PDF document") + { + Patterns = new[] { "*.pdf" }, + AppleUniformTypeIdentifiers = new[] { "com.adobe.pdf" }, + MimeTypes = new[] { "application/pdf" } + }; +} diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs new file mode 100644 index 0000000000..1f9202b0e7 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Avalonia.Platform.Storage; + +/// +/// Options class for method. +/// +public class FilePickerOpenOptions +{ + /// + /// Gets or sets the text that appears in the title bar of a file dialog. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// + public IStorageFolder? SuggestedStartLocation { get; set; } + + /// + /// Gets or sets an option indicating whether open picker allows users to select multiple files. + /// + public bool AllowMultiple { get; set; } + + /// + /// Gets or sets the collection of file types that the file open picker displays. + /// + public IReadOnlyList? FileTypeFilter { get; set; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs new file mode 100644 index 0000000000..0f4d690f7a --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace Avalonia.Platform.Storage; + +/// +/// Options class for method. +/// +public class FilePickerSaveOptions +{ + /// + /// Gets or sets the text that appears in the title bar of a file dialog. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the file name that the file save picker suggests to the user. + /// + public string? SuggestedFileName { get; set; } + + /// + /// Gets or sets the default extension to be used to save the file. + /// + public string? DefaultExtension { get; set; } + + /// + /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// + public IStorageFolder? SuggestedStartLocation { get; set; } + + /// + /// Gets or sets the collection of valid file types that the user can choose to assign to a file. + /// + public IReadOnlyList? FileTypeChoices { get; set; } + + /// + /// Gets or sets a value indicating whether file open picker displays a warning if the user specifies the name of a file that already exists. + /// + public bool? ShowOverwritePrompt { get; set; } +} diff --git a/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs new file mode 100644 index 0000000000..de90da30b2 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs @@ -0,0 +1,22 @@ +namespace Avalonia.Platform.Storage; + +/// +/// Options class for method. +/// +public class FolderPickerOpenOptions +{ + /// + /// Gets or sets the text that appears in the title bar of a folder dialog. + /// + public string? Title { get; set; } + + /// + /// Gets or sets an option indicating whether open picker allows users to select multiple folders. + /// + public bool AllowMultiple { get; set; } + + /// + /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// + public IStorageFolder? SuggestedStartLocation { get; set; } +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs new file mode 100644 index 0000000000..65811b7fbd --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +[NotClientImplementable] +public interface IStorageBookmarkItem : IStorageItem +{ + Task ReleaseBookmark(); +} + +[NotClientImplementable] +public interface IStorageBookmarkFile : IStorageFile, IStorageBookmarkItem +{ +} + +[NotClientImplementable] +public interface IStorageBookmarkFolder : IStorageFolder, IStorageBookmarkItem +{ + +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs new file mode 100644 index 0000000000..2f12514e50 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +/// +/// Represents a file. Provides information about the file and its contents, and ways to manipulate them. +/// +[NotClientImplementable] +public interface IStorageFile : IStorageItem +{ + /// + /// Returns true, if file is readable. + /// + bool CanOpenRead { get; } + + /// + /// Opens a stream for read access. + /// + Task OpenRead(); + + /// + /// Returns true, if file is writeable. + /// + bool CanOpenWrite { get; } + + /// + /// Opens stream for writing to the file. + /// + Task OpenWrite(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs new file mode 100644 index 0000000000..83b316bc3b --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -0,0 +1,12 @@ +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +/// +/// Manipulates folders and their contents, and provides information about them. +/// +[NotClientImplementable] +public interface IStorageFolder : IStorageItem +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs new file mode 100644 index 0000000000..078311a286 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -0,0 +1,53 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +/// +/// Manipulates storage items (files and folders) and their contents, and provides information about them +/// +/// +/// This interface inherits . It's recommended to dispose when it's not used anymore. +/// +[NotClientImplementable] +public interface IStorageItem : IDisposable +{ + /// + /// Gets the name of the item including the file name extension if there is one. + /// + string Name { get; } + + /// + /// Gets the full file-system path of the item, if the item has a path. + /// + /// + /// Android backend might return file path with "content:" scheme. + /// Browser and iOS backends might return relative uris. + /// + bool TryGetUri([NotNullWhen(true)] out Uri? uri); + + /// + /// Gets the basic properties of the current item. + /// + Task GetBasicPropertiesAsync(); + + /// + /// Returns true is item can be bookmarked and reused later. + /// + bool CanBookmark { get; } + + /// + /// Saves items to a bookmark. + /// + /// + /// Returns identifier of a bookmark. Can be null if OS denied request. + /// + Task SaveBookmark(); + + /// + /// Gets the parent folder of the current storage item. + /// + Task GetParentAsync(); +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs new file mode 100644 index 0000000000..32fb148790 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +[NotClientImplementable] +public interface IStorageProvider +{ + /// + /// Returns true if it's possible to open file picker on the current platform. + /// + bool CanOpen { get; } + + /// + /// Opens file picker dialog. + /// + /// Array of selected or empty collection if user canceled the dialog. + Task> OpenFilePickerAsync(FilePickerOpenOptions options); + + /// + /// Returns true if it's possible to open save file picker on the current platform. + /// + bool CanSave { get; } + + /// + /// Opens save file picker dialog. + /// + /// Saved or null if user canceled the dialog. + Task SaveFilePickerAsync(FilePickerSaveOptions options); + + /// + /// Returns true if it's possible to open folder picker on the current platform. + /// + bool CanPickFolder { get; } + + /// + /// Opens folder picker dialog. + /// + /// Array of selected or empty collection if user canceled the dialog. + Task> OpenFolderPickerAsync(FolderPickerOpenOptions options); + + /// + /// Open from the bookmark ID. + /// + /// Bookmark ID. + /// Bookmarked file or null if OS denied request. + Task OpenFileBookmarkAsync(string bookmark); + + /// + /// Open from the bookmark ID. + /// + /// Bookmark ID. + /// Bookmarked folder or null if OS denied request. + Task OpenFolderBookmarkAsync(string bookmark); +} diff --git a/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs new file mode 100644 index 0000000000..41b9bfa941 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs @@ -0,0 +1,43 @@ +using System; + +namespace Avalonia.Platform.Storage; + +/// +/// Provides access to the content-related properties of an item (like a file or folder). +/// +public class StorageItemProperties +{ + public StorageItemProperties( + ulong? size = null, + DateTimeOffset? dateCreated = null, + DateTimeOffset? dateModified = null) + { + Size = size; + DateCreated = dateCreated; + DateModified = dateModified; + } + + /// + /// Gets the size of the file in bytes. + /// + /// + /// Can be null if property is not available. + /// + public ulong? Size { get; } + + /// + /// Gets the date and time that the current folder was created. + /// + /// + /// Can be null if property is not available. + /// + public DateTimeOffset? DateCreated { get; } + + /// + /// Gets the date and time of the last time the file was modified. + /// + /// + /// Can be null if property is not available. + /// + public DateTimeOffset? DateModified { get; } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs b/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs new file mode 100644 index 0000000000..3eee8e848e --- /dev/null +++ b/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs @@ -0,0 +1,12 @@ +#nullable enable +using Avalonia.Platform.Storage; + +namespace Avalonia.Controls.Platform; + +/// +/// Factory allows to register custom storage provider instead of native implementation. +/// +public interface IStorageProviderFactory +{ + IStorageProvider CreateProvider(TopLevel topLevel); +} diff --git a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs similarity index 88% rename from src/Avalonia.Controls/Platform/ISystemDialogImpl.cs rename to src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs index 715eda5cfa..996fff6775 100644 --- a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Avalonia.Metadata; @@ -6,6 +7,7 @@ namespace Avalonia.Controls.Platform /// /// Defines a platform-specific system dialog implementation. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] [Unstable] public interface ISystemDialogImpl { diff --git a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs new file mode 100644 index 0000000000..2775c53803 --- /dev/null +++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +#nullable enable + +namespace Avalonia.Controls.Platform +{ + /// + /// Defines a platform-specific system dialog implementation. + /// + [Obsolete] + internal class SystemDialogImpl : ISystemDialogImpl + { + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) + { + if (dialog is OpenFileDialog openDialog) + { + var filePicker = parent.StorageProvider; + if (!filePicker.CanOpen) + { + return null; + } + + var options = openDialog.ToFilePickerOpenOptions(); + + var files = await filePicker.OpenFilePickerAsync(options); + return files + .Select(file => file.TryGetUri(out var fullPath) + ? fullPath.LocalPath + : file.Name) + .ToArray(); + } + else if (dialog is SaveFileDialog saveDialog) + { + var filePicker = parent.StorageProvider; + if (!filePicker.CanSave) + { + return null; + } + + var options = saveDialog.ToFilePickerSaveOptions(); + + var file = await filePicker.SaveFilePickerAsync(options); + if (file is null) + { + return null; + } + + var filePath = file.TryGetUri(out var fullPath) + ? fullPath.LocalPath + : file.Name; + return new[] { filePath }; + } + return null; + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) + { + var filePicker = parent.StorageProvider; + if (!filePicker.CanPickFolder) + { + return null; + } + + var options = dialog.ToFolderPickerOpenOptions(); + + var folders = await filePicker.OpenFolderPickerAsync(options); + return folders + .Select(f => f.TryGetUri(out var uri) ? uri.LocalPath : null) + .FirstOrDefault(u => u is not null); + } + } +} diff --git a/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs b/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs new file mode 100644 index 0000000000..b42040f3c3 --- /dev/null +++ b/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs @@ -0,0 +1,11 @@ +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Platform.Storage; + +namespace Avalonia.Controls.Platform; + +[Unstable] +public interface ITopLevelImplWithStorageProvider : ITopLevelImpl +{ + public IStorageProvider StorageProvider { get; } +} diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index 093f10be51..f3fb4d9a6d 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -3,12 +3,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; +using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; namespace Avalonia.Controls { /// /// Base class for system file dialogs. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public abstract class FileDialog : FileSystemDialog { /// @@ -26,6 +29,7 @@ namespace Avalonia.Controls /// /// Base class for system file and directory dialogs. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public abstract class FileSystemDialog : SystemDialog { [Obsolete("Use Directory")] @@ -45,6 +49,7 @@ namespace Avalonia.Controls /// /// Represents a system dialog that prompts the user to select a location for saving a file. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public class SaveFileDialog : FileDialog { /// @@ -73,11 +78,27 @@ namespace Avalonia.Controls return (await service.ShowFileDialogAsync(this, parent) ?? Array.Empty()).FirstOrDefault(); } + + public FilePickerSaveOptions ToFilePickerSaveOptions() + { + return new FilePickerSaveOptions + { + SuggestedFileName = InitialFileName, + DefaultExtension = DefaultExtension, + FileTypeChoices = Filters?.Select(f => new FilePickerFileType(f.Name!) { Patterns = f.Extensions.Select(e => $"*.{e}").ToArray() }).ToArray(), + Title = Title, + SuggestedStartLocation = InitialDirectory is { } directory + ? new BclStorageFolder(new System.IO.DirectoryInfo(directory)) + : null, + ShowOverwritePrompt = ShowOverwritePrompt + }; + } } /// /// Represents a system dialog that allows the user to select one or more files to open. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public class OpenFileDialog : FileDialog { /// @@ -100,11 +121,25 @@ namespace Avalonia.Controls var service = AvaloniaLocator.Current.GetRequiredService(); return service.ShowFileDialogAsync(this, parent); } + + public FilePickerOpenOptions ToFilePickerOpenOptions() + { + return new FilePickerOpenOptions + { + AllowMultiple = AllowMultiple, + FileTypeFilter = Filters?.Select(f => new FilePickerFileType(f.Name!) { Patterns = f.Extensions.Select(e => $"*.{e}").ToArray() }).ToArray(), + Title = Title, + SuggestedStartLocation = InitialDirectory is { } directory + ? new BclStorageFolder(new System.IO.DirectoryInfo(directory)) + : null + }; + } } /// /// Represents a system dialog that allows the user to select a directory. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public class OpenFolderDialog : FileSystemDialog { [Obsolete("Use Directory")] @@ -129,14 +164,35 @@ namespace Avalonia.Controls var service = AvaloniaLocator.Current.GetRequiredService(); return service.ShowFolderDialogAsync(this, parent); } + + public FolderPickerOpenOptions ToFolderPickerOpenOptions() + { + return new FolderPickerOpenOptions + { + Title = Title, + SuggestedStartLocation = InitialDirectory is { } directory + ? new BclStorageFolder(new System.IO.DirectoryInfo(directory)) + : null + }; + } } /// /// Base class for system dialogs. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public abstract class SystemDialog { + static SystemDialog() + { + if (AvaloniaLocator.Current.GetService() is null) + { + // Register default implementation. + AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + } + } + /// /// Gets or sets the dialog title. /// @@ -146,6 +202,7 @@ namespace Avalonia.Controls /// /// Represents a filter in an or an . /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public class FileDialogFilter { /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 57fb82485c..d09b824958 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -11,6 +11,7 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; @@ -93,7 +94,8 @@ namespace Avalonia.Controls private ILayoutManager? _layoutManager; private Border? _transparencyFallbackBorder; private TargetWeakEventSubscriber? _resourcesChangesSubscriber; - + private IStorageProvider? _storageProvider; + /// /// Initializes static members of the class. /// @@ -319,6 +321,11 @@ namespace Avalonia.Controls double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1; IStyleHost IStyleHost.StylingParent => _globalStyles!; + + public IStorageProvider StorageProvider => _storageProvider + ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) + ?? (PlatformImpl as ITopLevelImplWithStorageProvider)?.StorageProvider + ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget(); From e717cce7e822f25cc4290150597410489fcbdc26 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Jun 2022 01:00:07 -0400 Subject: [PATCH 50/96] Update headless implementations, managed and samples --- samples/ControlCatalog/ControlCatalog.csproj | 2 +- samples/ControlCatalog/MainView.xaml | 3 + samples/ControlCatalog/MainView.xaml.cs | 5 - samples/ControlCatalog/Pages/DialogsPage.xaml | 76 ++++-- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 234 ++++++++++++++++-- .../Pages/NumericUpDownPage.xaml | 6 +- .../Pages/NumericUpDownPage.xaml.cs | 13 +- .../Remote/PreviewerWindowImpl.cs | 6 +- .../Remote/PreviewerWindowingPlatform.cs | 1 - src/Avalonia.DesignerSupport/Remote/Stubs.cs | 32 ++- src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 4 - .../ManagedFileChooserFilterViewModel.cs | 35 +-- .../ManagedFileChooserViewModel.cs | 97 +++++--- .../ManagedFileDialogExtensions.cs | 142 ++--------- .../ManagedStorageProvider.cs | 147 +++++++++++ .../AvaloniaHeadlessPlatform.cs | 1 - .../HeadlessPlatformStubs.cs | 36 ++- src/Avalonia.Headless/HeadlessWindowImpl.cs | 6 +- 18 files changed, 576 insertions(+), 270 deletions(-) create mode 100644 src/Avalonia.Dialogs/ManagedStorageProvider.cs diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 903c849834..8358fb3cd4 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + netstandard2.0;net6.0 true enable diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index d8dc3bad2d..b6ce59f15b 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -69,6 +69,9 @@ + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index d675324d9f..47d11738bc 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -24,11 +24,6 @@ namespace ControlCatalog { IList tabItems = ((IList)sideBar.Items); tabItems.Add(new TabItem() - { - Header = "Dialogs", - Content = new DialogsPage() - }); - tabItems.Add(new TabItem() { Header = "Screens", Content = new ScreenPage() diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index 8a835867b3..cc23ef796a 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -1,29 +1,57 @@ - - - Use filters - - - - - + + - - + - - - - - - - + + + + + + + + + + + + + + Use filters + + + Force managed dialog + Open multiple + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index efa30c2741..f7b6db1255 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -1,13 +1,21 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Dialogs; using Avalonia.Layout; using Avalonia.Markup.Xaml; -#pragma warning disable 4014 +using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; + +#pragma warning disable CS0618 // Type or member is obsolete +#nullable enable + namespace ControlCatalog.Pages { public class DialogsPage : UserControl @@ -18,13 +26,16 @@ namespace ControlCatalog.Pages var results = this.Get("PickerLastResults"); var resultsVisible = this.Get("PickerLastResultsVisible"); + var bookmarkContainer = this.Get("BookmarkContainer"); + var openedFileContent = this.Get("OpenedFileContent"); + var openMultiple = this.Get("OpenMultiple"); - string? lastSelectedDirectory = null; + IStorageFolder? lastSelectedDirectory = null; - List? GetFilters() + List GetFilters() { if (this.Get("UseFilters").IsChecked != true) - return null; + return new List(); return new List { new FileDialogFilter @@ -39,12 +50,23 @@ namespace ControlCatalog.Pages }; } + List? GetFileTypes() + { + if (this.Get("UseFilters").IsChecked != true) + return null; + return new List + { + FilePickerFileTypes.All, + FilePickerFileTypes.TextPlain + }; + } + this.Get