diff --git a/.editorconfig b/.editorconfig index cb589a5ce1..30edee1633 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,7 +21,7 @@ csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true -trim_trailing_whitespace = true +# trim_trailing_whitespace = true # Indentation preferences csharp_indent_block_contents = true diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 45d78b3926..77f53332cd 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -48,7 +48,6 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, [Window setContentMaxSize:lastMaxSize]; [Window setOpaque:false]; - [Window setHasShadow:true]; } HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) { diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 85a89955f4..95f61422cb 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -24,6 +24,8 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase _lastTitle = @""; _parent = nullptr; WindowEvents = events; + + [Window setHasShadow:true]; OnInitialiseNSWindow(); } diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index fd080cfc5b..13751b56b5 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -115,7 +115,6 @@ namespace ControlCatalog.NetCore }) .With(new Win32PlatformOptions { - EnableMultitouch = true }) .UseSkia() .AfterSetup(builder => diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index d8dc3bad2d..7676de54a6 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -109,7 +109,7 @@ - + diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs new file mode 100644 index 0000000000..5843b13a0c --- /dev/null +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -0,0 +1,235 @@ +#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; + private PointerUpdateKind? _lastNonOtherUpdateKind; + 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.TotalMilliseconds > 250) + { + Status = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)} +PointerUpdateKind: {_lastProperties?.PointerUpdateKind} +Last PointerUpdateKind != Other: {_lastNonOtherUpdateKind} +IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed} +IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed} +IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed} +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(); + + var lastPointer = e.GetCurrentPoint(this); + _lastProperties = lastPointer.Properties; + + if (_lastProperties.PointerUpdateKind != PointerUpdateKind.Other) + { + _lastNonOtherUpdateKind = _lastProperties.PointerUpdateKind; + } + + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) + { + _pointers.Remove(e.Pointer.Id); + return; + } + + 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); + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _lastProperties = null; + base.OnPointerCaptureLost(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 0377993d2c..0000000000 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ /dev/null @@ -1,322 +0,0 @@ -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() } - } - }; - } - - - 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), - 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 - } - }; - } - - class PointerCanvas : Control - { - private readonly Slider _slider; - private readonly TextBlock _status; - private int _events; - private Stopwatch _stopwatch = Stopwatch.StartNew(); - private Dictionary _pointers = new(); - class PointerPoints - { - struct CanvasPoint - { - public IBrush Brush; - public Point Point; - public double Radius; - } - - readonly CanvasPoint[] _points = new CanvasPoint[1000]; - int _index; - - public void Render(DrawingContext context) - { - - CanvasPoint? prev = null; - for (var c = 0; c < _points.Length; c++) - { - 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; - 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); - } - } - } - } - - public PointerCanvas(Slider slider, TextBlock status) - { - _slider = slider; - _status = status; - DispatcherTimer.Run(() => - { - if (_stopwatch.Elapsed.TotalSeconds > 1) - { - _status.Text = "Events per second: " + (_events / _stopwatch.Elapsed.TotalSeconds); - _stopwatch.Restart(); - _events = 0; - } - - return this.GetVisualRoot() != null; - }, TimeSpan.FromMilliseconds(10)); - } - - - void HandleEvent(PointerEventArgs e) - { - _events++; - Thread.Sleep((int)_slider.Value); - InvalidateVisual(); - - if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) - { - _pointers.Remove(e.Pointer.Id); - return; - } - - 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); - 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..c39106f29e --- /dev/null +++ b/samples/ControlCatalog/Pages/PointersPage.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..6fc468e37f --- /dev/null +++ b/samples/ControlCatalog/Pages/PointersPage.xaml.cs @@ -0,0 +1,78 @@ +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.PointerEntered += Border_PointerUpdated; + border1.PointerExited += Border_PointerUpdated; + + border2.PointerPressed += Border_PointerPressed; + border2.PointerReleased += Border_PointerReleased; + border2.PointerCaptureLost += Border_PointerCaptureLost; + border2.PointerMoved += Border_PointerUpdated; + border2.PointerEntered += Border_PointerUpdated; + border2.PointerExited += Border_PointerUpdated; + } + + private void Border_PointerUpdated(object sender, PointerEventArgs e) + { + var textBlock = (TextBlock)((Border)sender).Child; + var position = e.GetPosition((Border)sender); + textBlock.Text = @$"Type: {e.Pointer.Type} +Captured: {e.Pointer.Captured == sender} +PointerId: {e.Pointer.Id} +Position: {(int)position.X} {(int)position.Y}"; + e.Handled = true; + } + + private void Border_PointerCaptureLost(object sender, PointerCaptureLostEventArgs e) + { + var textBlock = (TextBlock)((Border)sender).Child; + textBlock.Text = @$"Type: {e.Pointer.Type} +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); + } +} diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 3af14daa83..77863e5101 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -1,38 +1,45 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; -using Avalonia.Metadata; - -#nullable enable namespace Avalonia.Controls { /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary + public class ResourceDictionary : IResourceDictionary { + private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; /// /// Initializes a new instance of the class. /// - public ResourceDictionary() - { - CollectionChanged += OnCollectionChanged; - } + public ResourceDictionary() { } /// /// Initializes a new instance of the class. /// - public ResourceDictionary(IResourceHost owner) - : this() + public ResourceDictionary(IResourceHost owner) => Owner = owner; + + public int Count => _inner?.Count ?? 0; + + public object? this[object key] { - Owner = owner; + get => _inner?[key]; + set + { + Inner[key] = value; + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } } + public ICollection Keys => (ICollection?)_inner?.Keys ?? Array.Empty(); + public ICollection Values => (ICollection?)_inner?.Values ?? Array.Empty(); + public IResourceHost? Owner { get => _owner; @@ -80,7 +87,7 @@ namespace Avalonia.Controls { get { - if (Count > 0) + if (_inner?.Count > 0) { return true; } @@ -100,11 +107,43 @@ namespace Avalonia.Controls } } + bool ICollection>.IsReadOnly => false; + + private Dictionary Inner => _inner ??= new(); + public event EventHandler? OwnerChanged; + public void Add(object key, object? value) + { + Inner.Add(key, value); + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + + public void Clear() + { + if (_inner?.Count > 0) + { + _inner.Clear(); + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } + + public bool ContainsKey(object key) => _inner?.ContainsKey(key) ?? false; + + public bool Remove(object key) + { + if (_inner?.Remove(key) == true) + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + return true; + } + + return false; + } + public bool TryGetResource(object key, out object? value) { - if (TryGetValue(key, out value)) + if (_inner is not null && _inner.TryGetValue(key, out value)) { return true; } @@ -120,9 +159,52 @@ namespace Avalonia.Controls } } + value = null; return false; } + public bool TryGetValue(object key, out object? value) + { + if (_inner is not null) + return _inner.TryGetValue(key, out value); + value = null; + return false; + } + + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return (_inner as ICollection>)?.Contains(item) ?? false; + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + (_inner as ICollection>)?.CopyTo(array, arrayIndex); + } + + bool ICollection>.Remove(KeyValuePair item) + { + if ((_inner as ICollection>)?.Remove(item) == true) + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + return true; + } + + return false; + } + + public IEnumerator> GetEnumerator() + { + return _inner?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + void IResourceProvider.AddOwner(IResourceHost owner) { owner = owner ?? throw new ArgumentNullException(nameof(owner)); @@ -134,7 +216,7 @@ namespace Avalonia.Controls Owner = owner; - var hasResources = Count > 0; + var hasResources = _inner?.Count > 0; if (_mergedDictionaries is object) { @@ -159,7 +241,7 @@ namespace Avalonia.Controls { Owner = null; - var hasResources = Count > 0; + var hasResources = _inner?.Count > 0; if (_mergedDictionaries is object) { @@ -176,10 +258,5 @@ namespace Avalonia.Controls } } } - - private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); - } } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index f8cedb636f..889b7e3b82 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/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; @@ -101,7 +102,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.Base/Input/IKeyboardDevice.cs b/src/Avalonia.Base/Input/IKeyboardDevice.cs index c8db8bf16f..80aebc02bc 100644 --- a/src/Avalonia.Base/Input/IKeyboardDevice.cs +++ b/src/Avalonia.Base/Input/IKeyboardDevice.cs @@ -43,12 +43,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 } [NotClientImplementable] diff --git a/src/Avalonia.Base/Input/IPenDevice.cs b/src/Avalonia.Base/Input/IPenDevice.cs new file mode 100644 index 0000000000..1cc0fcf76d --- /dev/null +++ b/src/Avalonia.Base/Input/IPenDevice.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Input +{ + /// + /// Represents a pen/stylus device. + /// + public interface IPenDevice : IPointerDevice + { + + } +} diff --git a/src/Avalonia.Base/Input/IPointer.cs b/src/Avalonia.Base/Input/IPointer.cs index 66aeacadc9..52605bb6ae 100644 --- a/src/Avalonia.Base/Input/IPointer.cs +++ b/src/Avalonia.Base/Input/IPointer.cs @@ -2,20 +2,59 @@ using Avalonia.Metadata; namespace Avalonia.Input { + /// + /// Identifies specific pointer generated by input device. + /// + /// + /// Some devices, for instance, touchscreen might generate a pointer on each physical contact. + /// [NotClientImplementable] 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, - Touch + Touch, + Pen } } diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs new file mode 100644 index 0000000000..d22b48562c --- /dev/null +++ b/src/Avalonia.Base/Input/PenDevice.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + /// + /// Represents a pen/stylus device. + /// + 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 bool _disposed; + + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawPointerEventArgs margs) + ProcessRawEvent(margs); + } + + private void ProcessRawEvent(RawPointerEventArgs e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + if (!_pointers.TryGetValue(e.RawPointerId, out var pointer)) + { + if (e.Type == RawPointerEventType.LeftButtonUp + || e.Type == RawPointerEventType.TouchEnd) + return; + + _pointers[e.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(), + PointerType.Pen, _pointers.Count == 0); + } + + _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(); + + bool shouldReleasePointer = false; + switch (e.Type) + { + case RawPointerEventType.LeaveWindow: + shouldReleasePointer = true; + break; + case RawPointerEventType.LeftButtonDown: + e.Handled = PenDown(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); + break; + case RawPointerEventType.LeftButtonUp: + e.Handled = PenUp(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); + break; + case RawPointerEventType.Move: + e.Handled = PenMove(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult, e.IntermediatePoints); + break; + } + + if (shouldReleasePointer) + { + pointer.Dispose(); + _pointers.Remove(e.RawPointerId); + _lastPositions.Remove(e.RawPointerId); + } + } + + private bool PenDown(Pointer pointer, ulong timestamp, + IInputElement root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest) + { + var source = pointer.Captured ?? hitTest; + + if (source != null) + { + pointer.Capture(source); + 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 PenMove(Pointer pointer, ulong timestamp, + IInputRoot root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest, + Lazy?>? intermediatePoints) + { + var source = pointer.Captured ?? hitTest; + + if (source is not null) + { + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, pointer, root, + p, timestamp, properties, inputModifiers, intermediatePoints); + + source.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool PenUp(Pointer pointer, ulong timestamp, + IInputElement root, Point p, PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest) + { + var source = pointer.Captured ?? hitTest; + + if (source is not null) + { + var e = new PointerReleasedEventArgs(source, pointer, root, p, timestamp, properties, inputModifiers, + _lastMouseDownButton); + + source?.RaiseEvent(e); + pointer.Capture(null); + return e.Handled; + } + + return false; + } + + public void Dispose() + { + if (_disposed) + return; + var values = _pointers.Values.ToList(); + _pointers.Clear(); + _disposed = true; + foreach (var p in values) + p.Dispose(); + } + + [Obsolete] + IInputElement? IPointerDevice.Captured => _pointers.Values + .FirstOrDefault(p => p.IsPrimary)?.Captured; + + [Obsolete] + void IPointerDevice.Capture(IInputElement? control) => _pointers.Values + .FirstOrDefault(p => p.IsPrimary)?.Capture(control); + + [Obsolete] + Point IPointerDevice.GetPosition(IVisual relativeTo) => new Point(-1, -1); + + public IPointer? TryGetPointer(RawPointerEventArgs ev) + { + return _pointers.TryGetValue(ev.RawPointerId, out var pointer) + ? pointer + : null; + } + } +} diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 5495802920..058c2f9cc1 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/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")] @@ -130,7 +145,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.Base/Input/PointerPoint.cs b/src/Avalonia.Base/Input/PointerPoint.cs index 9f8285a8e1..71145b5cb0 100644 --- a/src/Avalonia.Base/Input/PointerPoint.cs +++ b/src/Avalonia.Base/Input/PointerPoint.cs @@ -1,5 +1,10 @@ +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) @@ -8,25 +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; } + + /// + /// 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; } + + /// + /// 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; @@ -36,10 +125,13 @@ 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 - + if (kind == PointerUpdateKind.LeftButtonPressed) IsLeftButtonPressed = true; if (kind == PointerUpdateKind.LeftButtonReleased) @@ -62,6 +154,33 @@ namespace Avalonia.Input IsXButton2Pressed = 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; + } + + internal PointerPointProperties(PointerPointProperties basedOn, 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.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs index 8b9d7c161d..0e4e0ed3e2 100644 --- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Base/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. /// @@ -87,6 +88,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. /// @@ -130,10 +136,17 @@ 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; + Pressure = 0.5f; } } } diff --git a/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs index 020b40e55b..6706a45f48 100644 --- a/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs +++ b/src/Avalonia.Base/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.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index 54dcc4051e..e914d860fd 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/src/Avalonia.Base/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/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index 5dc426ab06..e4d5a1ca68 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -158,6 +158,7 @@ namespace Avalonia.Rendering.SceneGraph if (result != null && result.Parent != parent) { Deindex(scene, result); + ((VisualNode?)result.Parent)?.RemoveChild(result); result = null; } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 69c644dff9..bf29e0b0ac 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -462,10 +462,6 @@ namespace Avalonia values.Remove(entry.property); } } - else - { - throw new AvaloniaInternalException("Value could not be found at the end of batch update."); - } // If a new batch update was started while ending this one, abort. if (_batchUpdateCount > 0) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 2a51f45896..f9685b3682 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; } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 4801fa69f0..1504d2b25f 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -223,6 +223,7 @@ namespace Avalonia.Controls.Primitives { Popup.PlacementTarget = Target = placementTarget; ((ISetLogicalParent)Popup).SetParent(placementTarget); + Popup.SetValue(StyledElement.TemplatedParentProperty, placementTarget.TemplatedParent); } if (Popup.Child == null) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 95e5e25c42..1501d97470 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -860,22 +860,7 @@ namespace Avalonia.Controls.Primitives { if (control != null) { - var templatedParent = TemplatedParent; - - if (control.TemplatedParent == null) - { - control.SetValue(TemplatedParentProperty, templatedParent); - } - - control.ApplyTemplate(); - - if (!(control is IPresenter) && control.TemplatedParent == templatedParent) - { - foreach (IControl child in control.VisualChildren) - { - SetTemplatedParentAndApplyChildTemplates(child); - } - } + TemplatedControl.ApplyTemplatedParent(control, TemplatedParent); } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..4403bfce51 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -285,7 +285,7 @@ namespace Avalonia.Controls.Primitives Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(this, "Creating control template"); var (child, nameScope) = template.Build(this); - ApplyTemplatedParent(child); + ApplyTemplatedParent(child, this); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); @@ -387,18 +387,18 @@ namespace Avalonia.Controls.Primitives /// Sets the TemplatedParent property for the created template children. /// /// The control. - private void ApplyTemplatedParent(IControl control) + internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent) { - control.SetValue(TemplatedParentProperty, this); + control.SetValue(TemplatedParentProperty, templatedParent); var children = control.LogicalChildren; var count = children.Count; for (var i = 0; i < count; i++) { - if (children[i] is IControl child) + if (children[i] is IStyledElement child) { - ApplyTemplatedParent(child); + ApplyTemplatedParent(child, templatedParent); } } } diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index d92ffb0d1a..d0715e402d 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -168,7 +168,7 @@ namespace Avalonia.Controls.Selection { if (_writableSelectedItems is INotifyCollectionChanged incc) { - incc.CollectionChanged += OnSelectedItemsCollectionChanged; + incc.CollectionChanged -= OnSelectedItemsCollectionChanged; } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 91c93c87c8..bb18bf4c64 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -271,8 +271,9 @@ namespace Avalonia.Controls _popupHost = OverlayPopupHost.CreatePopupHost(control, null); _popupHost.SetChild(this); ((ISetLogicalParent)_popupHost).SetParent(control); - - _popupHost.ConfigurePosition(control, GetPlacement(control), + ApplyTemplatedParent(this, control.TemplatedParent); + + _popupHost.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); WindowManagerAddShadowHintChanged(_popupHost, false); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index e383c160e3..f8e2e0544f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -113,6 +113,8 @@ namespace Avalonia.Diagnostics.ViewModels } } + public bool CanNavigateToParentProperty => _selectedEntitiesStack.Count >= 1; + private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value) { if (value is StaticResourceExtension staticResource) @@ -415,7 +417,14 @@ namespace Avalonia.Diagnostics.ViewModels } } - public void ApplySelectedProperty() + private static IEnumerable GetAllPublicProperties(Type type) + { + return type + .GetProperties() + .Concat(type.GetInterfaces().SelectMany(i => i.GetProperties())); + } + + public void NavigateToSelectedProperty() { var selectedProperty = SelectedProperty; var selectedEntity = SelectedEntity; @@ -423,72 +432,103 @@ namespace Avalonia.Diagnostics.ViewModels if (selectedEntity == null || selectedProperty == null || selectedProperty.PropertyType == typeof(string) - || selectedProperty.PropertyType.IsValueType - ) + || selectedProperty.PropertyType.IsValueType) return; - object? property; - if (selectedProperty.Key is AvaloniaProperty avaloniaProperty) + object? property = null; + + switch (selectedProperty) { - property = (_selectedEntity as IControl)?.GetValue(avaloniaProperty); + case AvaloniaPropertyViewModel avaloniaProperty: + + property = (_selectedEntity as IControl)?.GetValue(avaloniaProperty.Property); + + break; + + case ClrPropertyViewModel clrProperty: + { + property = GetAllPublicProperties(selectedEntity.GetType()) + .FirstOrDefault(pi => clrProperty.Property == pi)? + .GetValue(selectedEntity); + + break; + } } - else + + if (property == null) + return; + + _selectedEntitiesStack.Push((Name:selectedEntityName!, Entry:selectedEntity)); + + var propertyName = selectedProperty.Name; + + //Strip out interface names + if (propertyName.LastIndexOf('.') is var p && p != -1) { - property = selectedEntity.GetType().GetProperties() - .FirstOrDefault(pi => pi.Name == selectedProperty.Name - && pi.DeclaringType == selectedProperty.DeclaringType - && pi.PropertyType.Name == selectedProperty.PropertyType.Name) - ?.GetValue(selectedEntity); + propertyName = propertyName.Substring(p + 1); } - if (property == null) return; - _selectedEntitiesStack.Push((Name:selectedEntityName!,Entry:selectedEntity)); - NavigateToProperty(property, selectedProperty.Name); + + NavigateToProperty(property, selectedEntityName + "." + propertyName); + + RaisePropertyChanged(nameof(CanNavigateToParentProperty)); } - public void ApplyParentProperty() + public void NavigateToParentProperty() { - if (_selectedEntitiesStack.Any()) + if (_selectedEntitiesStack.Count > 0) { var property = _selectedEntitiesStack.Pop(); NavigateToProperty(property.Entry, property.Name); + + RaisePropertyChanged(nameof(CanNavigateToParentProperty)); } } - protected void NavigateToProperty(object o, string? entityName) + protected void NavigateToProperty(object o, string? entityName) { var oldSelectedEntity = SelectedEntity; - if (oldSelectedEntity is IAvaloniaObject ao1) - { - ao1.PropertyChanged -= ControlPropertyChanged; - } - else if (oldSelectedEntity is INotifyPropertyChanged inpc1) + + switch (oldSelectedEntity) { - inpc1.PropertyChanged -= ControlPropertyChanged; + case IAvaloniaObject ao1: + ao1.PropertyChanged -= ControlPropertyChanged; + break; + + case INotifyPropertyChanged inpc1: + inpc1.PropertyChanged -= ControlPropertyChanged; + break; } - + SelectedEntity = o; SelectedEntityName = entityName; SelectedEntityType = o.ToString(); + var properties = GetAvaloniaProperties(o) .Concat(GetClrProperties(o, _showImplementedInterfaces)) .OrderBy(x => x, PropertyComparer.Instance) .ThenBy(x => x.Name) .ToArray(); - _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToArray()); + _propertyIndex = properties + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.ToArray()); + + TreePage.PropertiesFilter.FilterString = string.Empty; var view = new DataGridCollectionView(properties); view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); view.Filter = FilterProperty; PropertiesView = view; - if (o is IAvaloniaObject ao2) + switch (o) { - ao2.PropertyChanged += ControlPropertyChanged; - } - else if (o is INotifyPropertyChanged inpc2) - { - inpc2.PropertyChanged += ControlPropertyChanged; + case IAvaloniaObject ao2: + ao2.PropertyChanged += ControlPropertyChanged; + break; + + case INotifyPropertyChanged inpc2: + inpc2.PropertyChanged += ControlPropertyChanged; + break; } } @@ -498,7 +538,9 @@ namespace Avalonia.Diagnostics.ViewModels if (SelectedEntity != _avaloniaObject) { - NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); + NavigateToProperty( + _avaloniaObject, + (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); } if (PropertiesView is null) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index 0c0c005122..a453fef212 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -30,20 +30,28 @@ namespace Avalonia.Diagnostics.ViewModels if (control is AvaloniaObject ao) { - MarginThickness = ao.GetValue(Layoutable.MarginProperty); - - if (HasPadding) + try { - PaddingThickness = ao.GetValue(Decorator.PaddingProperty); - } + _updatingFromControl = true; + MarginThickness = ao.GetValue(Layoutable.MarginProperty); + + if (HasPadding) + { + PaddingThickness = ao.GetValue(Decorator.PaddingProperty); + } - if (HasBorder) + if (HasBorder) + { + BorderThickness = ao.GetValue(Border.BorderThicknessProperty); + } + + HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty); + VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty); + } + finally { - BorderThickness = ao.GetValue(Border.BorderThicknessProperty); + _updatingFromControl = false; } - - HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty); - VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty); } UpdateSize(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index cc392853be..a426b387f7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -30,7 +30,11 @@ -