Browse Source

Merge branch 'master' into blazor-skia-raster-backend

pull/8417/head
Sentient Lobster 4 years ago
committed by GitHub
parent
commit
955d8782a2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .editorconfig
  2. 1
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  3. 2
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  4. 1
      samples/ControlCatalog.NetCore/Program.cs
  5. 2
      samples/ControlCatalog/MainView.xaml
  6. 235
      samples/ControlCatalog/Pages/PointerCanvas.cs
  7. 109
      samples/ControlCatalog/Pages/PointerContactsTab.cs
  8. 322
      samples/ControlCatalog/Pages/PointersPage.cs
  9. 66
      samples/ControlCatalog/Pages/PointersPage.xaml
  10. 78
      samples/ControlCatalog/Pages/PointersPage.xaml.cs
  11. 119
      src/Avalonia.Base/Controls/ResourceDictionary.cs
  12. 5
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  13. 7
      src/Avalonia.Base/Input/IKeyboardDevice.cs
  14. 10
      src/Avalonia.Base/Input/IPenDevice.cs
  15. 43
      src/Avalonia.Base/Input/IPointer.cs
  16. 174
      src/Avalonia.Base/Input/PenDevice.cs
  17. 22
      src/Avalonia.Base/Input/PointerEventArgs.cs
  18. 125
      src/Avalonia.Base/Input/PointerPoint.cs
  19. 19
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  20. 17
      src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs
  21. 13
      src/Avalonia.Base/Input/TouchDevice.cs
  22. 1
      src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs
  23. 4
      src/Avalonia.Base/ValueStore.cs
  24. 4
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  25. 1
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  26. 17
      src/Avalonia.Controls/Primitives/Popup.cs
  27. 10
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  28. 2
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  29. 5
      src/Avalonia.Controls/ToolTip.cs
  30. 108
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  31. 28
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
  32. 6
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  33. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
  34. 12
      src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml
  35. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  36. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  37. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  38. 12
      src/Markup/Avalonia.Markup.Xaml/IAddChild.cs
  39. 12
      src/Shared/RawEventGrouping.cs
  40. 3
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  41. 13
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  42. 217
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  43. 1
      src/Windows/Avalonia.Win32/Win32Platform.cs
  44. 429
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  45. 24
      src/Windows/Avalonia.Win32/WindowImpl.cs
  46. 30
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  47. 24
      tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
  48. 63
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  49. 105
      tests/Avalonia.LeakTests/ControlTests.cs
  50. 127
      tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs
  51. 51
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

2
.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

1
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) {

2
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();
}

1
samples/ControlCatalog.NetCore/Program.cs

@ -115,7 +115,6 @@ namespace ControlCatalog.NetCore
})
.With(new Win32PlatformOptions
{
EnableMultitouch = true
})
.UseSkia()
.AfterSetup(builder =>

2
samples/ControlCatalog/MainView.xaml

@ -109,7 +109,7 @@
<TabItem Header="OpenGL">
<pages:OpenGlPage />
</TabItem>
<TabItem Header="Pointers (Touch)">
<TabItem Header="Pointers">
<pages:PointersPage />
</TabItem>
<TabItem Header="ProgressBar">

235
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<int, PointerPoints> _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<PointerCanvas, int> ThreadSleepProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, int>(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<PointerCanvas, bool> DrawOnlyPointsProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, bool>(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<PointerCanvas, string?> StatusProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, string?>(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);
}
}

109
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<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
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);
}
}
}

322
samples/ControlCatalog/Pages/PointersPage.cs

@ -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<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
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<int, PointerPoints> _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);
}
}
}
}

66
samples/ControlCatalog/Pages/PointersPage.xaml

@ -0,0 +1,66 @@
<UserControl x:Class="ControlCatalog.Pages.PointersPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ControlCatalog.Pages">
<TabControl>
<TabItem Header="Contacts">
<local:PointerContactsTab />
</TabItem>
<TabItem Header="IntermediatePoints">
<Panel TextElement.Foreground="Black">
<local:PointerCanvas x:Name="IntermediatePointsCanvas"
DrawOnlyPoints="True"
Status="{Binding #Status1TextBlock.Text, Mode=OneWayToSource}"
ThreadSleep="{Binding #ThreadSleepSlider.Value}" />
<Border Width="300"
Height="60"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<StackPanel Background="LightYellow">
<TextBlock Text="{Binding #ThreadSleepSlider.Value, StringFormat='Thread sleep: {0} / 500'}" />
<Slider x:Name="ThreadSleepSlider"
Value="50"
Maximum="500"
Minimum="0" />
</StackPanel>
</Border>
<TextBlock x:Name="Status1TextBlock"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
</Panel>
</TabItem>
<TabItem Header="Pressure">
<Panel TextElement.Foreground="Black">
<local:PointerCanvas x:Name="PressureCanvas"
DrawOnlyPoints="False"
Status="{Binding #Status2TextBlock.Text, Mode=OneWayToSource}"
ThreadSleep="0" />
<TextBlock x:Name="Status2TextBlock"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
</Panel>
</TabItem>
<TabItem Header="Capture">
<WrapPanel>
<Border Name="BorderCapture1"
MinWidth="250"
MinHeight="170"
Margin="5"
Padding="50"
Background="{DynamicResource SystemAccentColor}"
ToolTip.Placement="Bottom">
<TextBlock>Capture 1</TextBlock>
</Border>
<Border Name="BorderCapture2"
MinWidth="250"
MinHeight="170"
Margin="5"
Padding="50"
Background="{DynamicResource SystemAccentColor}"
ToolTip.Placement="Bottom">
<TextBlock>Capture 2</TextBlock>
</Border>
</WrapPanel>
</TabItem>
</TabControl>
</UserControl>

78
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<Border>("BorderCapture1");
var border2 = this.Get<Border>("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);
}
}

119
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
{
/// <summary>
/// An indexed dictionary of resources.
/// </summary>
public class ResourceDictionary : AvaloniaDictionary<object, object?>, IResourceDictionary
public class ResourceDictionary : IResourceDictionary
{
private Dictionary<object, object?>? _inner;
private IResourceHost? _owner;
private AvaloniaList<IResourceProvider>? _mergedDictionaries;
/// <summary>
/// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
/// </summary>
public ResourceDictionary()
{
CollectionChanged += OnCollectionChanged;
}
public ResourceDictionary() { }
/// <summary>
/// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
/// </summary>
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<object> Keys => (ICollection<object>?)_inner?.Keys ?? Array.Empty<object>();
public ICollection<object?> Values => (ICollection<object?>?)_inner?.Values ?? Array.Empty<object?>();
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<KeyValuePair<object, object?>>.IsReadOnly => false;
private Dictionary<object, object?> 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<KeyValuePair<object, object?>>.Add(KeyValuePair<object, object?> item)
{
Add(item.Key, item.Value);
}
bool ICollection<KeyValuePair<object, object?>>.Contains(KeyValuePair<object, object?> item)
{
return (_inner as ICollection<KeyValuePair<object, object?>>)?.Contains(item) ?? false;
}
void ICollection<KeyValuePair<object, object?>>.CopyTo(KeyValuePair<object, object?>[] array, int arrayIndex)
{
(_inner as ICollection<KeyValuePair<object, object?>>)?.CopyTo(array, arrayIndex);
}
bool ICollection<KeyValuePair<object, object?>>.Remove(KeyValuePair<object, object?> item)
{
if ((_inner as ICollection<KeyValuePair<object, object?>>)?.Remove(item) == true)
{
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
return true;
}
return false;
}
public IEnumerator<KeyValuePair<object, object?>> GetEnumerator()
{
return _inner?.GetEnumerator() ?? Enumerable.Empty<KeyValuePair<object, object?>>().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);
}
}
}

5
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;

7
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]

10
src/Avalonia.Base/Input/IPenDevice.cs

@ -0,0 +1,10 @@
namespace Avalonia.Input
{
/// <summary>
/// Represents a pen/stylus device.
/// </summary>
public interface IPenDevice : IPointerDevice
{
}
}

43
src/Avalonia.Base/Input/IPointer.cs

@ -2,20 +2,59 @@ using Avalonia.Metadata;
namespace Avalonia.Input
{
/// <summary>
/// Identifies specific pointer generated by input device.
/// </summary>
/// <remarks>
/// Some devices, for instance, touchscreen might generate a pointer on each physical contact.
/// </remarks>
[NotClientImplementable]
public interface IPointer
{
/// <summary>
/// Gets a unique identifier for the input pointer.
/// </summary>
int Id { get; }
/// <summary>
/// Captures pointer input to the specified control.
/// </summary>
/// <param name="control">The control.</param>
/// <remarks>
/// 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 <see cref="Captured"/> property.
/// </remarks>
void Capture(IInputElement? control);
/// <summary>
/// Gets the control that is currently capturing by the pointer, if any.
/// </summary>
/// <remarks>
/// 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
/// <see cref="Capture"/> method.
/// </remarks>
IInputElement? Captured { get; }
/// <summary>
/// Gets the pointer device type.
/// </summary>
PointerType Type { get; }
/// <summary>
/// Gets a value that indicates whether the input is from the primary pointer when multiple pointers are registered.
/// </summary>
bool IsPrimary { get; }
}
/// <summary>
/// Enumerates pointer device types.
/// </summary>
public enum PointerType
{
Mouse,
Touch
Touch,
Pen
}
}

174
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
{
/// <summary>
/// Represents a pen/stylus device.
/// </summary>
public class PenDevice : IPenDevice, IDisposable
{
private readonly Dictionary<long, Pointer> _pointers = new();
private readonly Dictionary<long, PixelPoint> _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<IPlatformSettings>();
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<IReadOnlyList<RawPointerPoint>?>? 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;
}
}
}

22
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -67,7 +67,14 @@ namespace Avalonia.Input
public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
}
/// <summary>
/// Gets specific pointer generated by input device.
/// </summary>
public IPointer Pointer { get; }
/// <summary>
/// Gets the time when the input occurred.
/// </summary>
public ulong Timestamp { get; }
private IPointerDevice? _device;
@ -91,7 +98,10 @@ namespace Avalonia.Input
return mods;
}
}
/// <summary>
/// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated.
/// </summary>
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;
}
/// <summary>
/// Gets the pointer position relative to a control.
/// </summary>
/// <param name="relativeTo">The control.</param>
/// <returns>The pointer position in the control's coordinates.</returns>
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);

125
src/Avalonia.Base/Input/PointerPoint.cs

@ -1,5 +1,10 @@
using Avalonia.Input.Raw;
namespace Avalonia.Input
{
/// <summary>
/// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact.
/// </summary>
public sealed class PointerPoint
{
public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties)
@ -8,25 +13,109 @@ namespace Avalonia.Input
Position = position;
Properties = properties;
}
/// <summary>
/// Gets specific pointer generated by input device.
/// </summary>
public IPointer Pointer { get; }
/// <summary>
/// Gets extended information about the input pointer.
/// </summary>
public PointerPointProperties Properties { get; }
/// <summary>
/// Gets the location of the pointer input in client coordinates.
/// </summary>
public Point Position { get; }
}
/// <summary>
/// Provides extended properties for a PointerPoint object.
/// </summary>
public sealed class PointerPointProperties
{
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device.
/// </summary>
public bool IsLeftButtonPressed { get; }
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the tertiary action mode of an input device.
/// </summary>
public bool IsMiddleButtonPressed { get; }
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the secondary action mode (if supported) of an input device.
/// </summary>
public bool IsRightButtonPressed { get; }
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the first extended mouse button (XButton1).
/// </summary>
public bool IsXButton1Pressed { get; }
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the second extended mouse button (XButton2).
/// </summary>
public bool IsXButton2Pressed { get; }
/// <summary>
/// Gets a value that indicates whether the barrel button of the pen/stylus device is pressed.
/// </summary>
public bool IsBarrelButtonPressed { get; }
/// <summary>
/// Gets a value that indicates whether the input is from a pen eraser.
/// </summary>
public bool IsEraser { get; }
/// <summary>
/// Gets a value that indicates whether the digitizer pen is inverted.
/// </summary>
public bool IsInverted { get; }
/// <summary>
/// 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).
/// </summary>
/// <returns>
/// A value between 0.0 and 359.0 in degrees of rotation. The default value is 0.0.
/// </returns>
public float Twist { get; }
/// <summary>
/// Gets a value that indicates the force that the pointer device (typically a pen/stylus) exerts on the surface of the digitizer.
/// </summary>
/// <returns>
/// A value from 0 to 1.0. The default value is 0.5.
/// </returns>
public float Pressure { get; } = 0.5f;
/// <summary>
/// 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).
/// </summary>
/// <returns>
/// 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.
/// </returns>
public float XTilt { get; }
/// <summary>
/// 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).
/// </summary>
/// <returns>
/// 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.
/// </returns>
public float YTilt { get; }
/// <summary>
/// Gets the kind of pointer state change.
/// </summary>
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();
}

19
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

@ -56,11 +56,12 @@ namespace Avalonia.Input.Raw
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
Point = new RawPointerPoint();
Position = position;
Type = type;
InputModifiers = inputModifiers;
}
/// <summary>
/// Initializes a new instance of the <see cref="RawPointerEventArgs"/> class.
/// </summary>
@ -87,6 +88,11 @@ namespace Avalonia.Input.Raw
InputModifiers = inputModifiers;
}
/// <summary>
/// Gets the raw pointer identifier.
/// </summary>
public long RawPointerId { get; set; }
/// <summary>
/// Gets the pointer properties and position, in client DIPs.
/// </summary>
@ -130,10 +136,17 @@ namespace Avalonia.Input.Raw
/// Pointer position, in client DIPs.
/// </summary>
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;
}
}
}

17
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; }
}
}

13
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;
}

1
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;
}

4
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)

4
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;
}

1
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)

17
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);
}
}

10
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.
/// </summary>
/// <param name="control">The control.</param>
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);
}
}
}

2
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;
}
}

5
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);

108
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<PropertyInfo> 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)

28
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();

6
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -30,7 +30,11 @@
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,*">
<Grid ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto">
<Button Grid.Column="0" Grid.RowSpan="2" Content="^" Command="{Binding ApplyParentProperty}" />
<Button Grid.Column="0" Grid.RowSpan="2" Content="^"
IsEnabled="{Binding CanNavigateToParentProperty}"
Margin="0 0 4 0"
ToolTip.Tip="Navigate to parent property"
Command="{Binding NavigateToParentProperty}" />
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding SelectedEntityName}" FontWeight="Bold" />
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding SelectedEntityType}" FontStyle="Italic" />
</Grid>

2
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs

@ -25,7 +25,7 @@ namespace Avalonia.Diagnostics.Views
{
if (sender is DataGrid grid && grid.DataContext is ControlDetailsViewModel controlDetails)
{
controlDetails.ApplySelectedProperty();
controlDetails.NavigateToSelectedProperty();
}
}

12
src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml

@ -19,16 +19,4 @@
</FocusAdornerTemplate>
</Setter>
</Style>
<!-- DottedLine FocusAdorner -->
<Style Selector="Window.DottedLineFocusAdorner :is(Control)">
<Setter Property="FocusAdorner">
<FocusAdornerTemplate>
<Rectangle Stroke="{StaticResource SystemControlFocusVisualPrimaryBrush}"
StrokeThickness="1"
StrokeDashArray="1,2"
Margin="1" />
</FocusAdornerTemplate>
</Setter>
</Style>
</Styles>

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@ -41,7 +41,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
},
WhitespaceSignificantCollectionAttributes =
{
typeSystem.GetType("Avalonia.Metadata.WhitespaceSignificantCollectionAttribute")
typeSystem.GetType("Avalonia.Metadata.WhitespaceSignificantCollectionAttribute")
},
TrimSurroundingWhitespaceAttributes =
{
@ -56,7 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
XmlNamespaceInfoProvider =
typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlXmlNamespaceInfoProvider"),
DeferredContentPropertyAttributes = {typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute")},
DeferredContentPropertyAttributes = { typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute") },
DeferredContentExecutorCustomizationDefaultTypeParameter = typeSystem.GetType("Avalonia.Controls.IControl"),
DeferredContentExecutorCustomizationTypeParameterDeferredContentAttributePropertyNames = new List<string>
{
@ -70,6 +70,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
},
InnerServiceProviderFactoryMethod =
runtimeHelpers.FindMethod(m => m.Name == "CreateInnerServiceProviderV1"),
IAddChild = typeSystem.GetType("Avalonia.Markup.Xaml.IAddChild"),
IAddChildOfT = typeSystem.GetType("Avalonia.Markup.Xaml.IAddChild`1")
};
rv.CustomAttributeResolver = new AttributeResolver(typeSystem, rv);

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit daaac590e078967b78045f74c38ef046d00d8582
Subproject commit a4e6be2d1407abec4f35fcb208848830ce513ead

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -15,6 +15,7 @@
<Compile Include="Converters\FontFamilyTypeConverter.cs" />
<Compile Include="Converters\TimeSpanTypeConverter.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="IAddChild.cs" />
<Compile Include="MarkupExtension.cs" />
<Compile Include="MarkupExtensions\CompiledBindingExtension.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\ArrayElementPlugin.cs" />

12
src/Markup/Avalonia.Markup.Xaml/IAddChild.cs

@ -0,0 +1,12 @@
namespace Avalonia.Markup.Xaml
{
public interface IAddChild
{
void AddChild(object child);
}
public interface IAddChild<T> : IAddChild
{
void AddChild(T child);
}
}

12
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<RawInputEventArgs> _eventCallback;
private readonly Queue<RawInputEventArgs> _inputQueue = new();
private readonly Action _dispatchFromQueue;
readonly Dictionary<long, RawTouchEventArgs> _lastTouchPoints = new();
readonly Dictionary<long, RawPointerEventArgs> _lastTouchPoints = new();
RawInputEventArgs? _lastEvent;
public RawEventGrouper(Action<RawInputEventArgs> 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);
}

3
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor

@ -5,7 +5,8 @@
@onpointerdown="OnPointerDown"
@onpointerup="OnPointerUp"
@onpointermove="OnPointerMove"
@onpointercancel="OnPointerCancel">
@onpointercancel="OnPointerCancel"
@onfocus="OnFocus">
<canvas id="htmlCanvas" @ref="_htmlCanvas" @attributes="AdditionalAttributes"/>

13
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@ -38,6 +38,7 @@ namespace Avalonia.Web.Blazor
private bool _initialised;
private bool _useGL;
private bool _inputElementFocused;
[Inject] private IJSRuntime Js { get; set; } = null!;
@ -222,6 +223,16 @@ namespace Avalonia.Web.Blazor
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, e.Key, GetModifiers(e));
}
private void OnFocus(FocusEventArgs e)
{
// if focus has unexpectedly moved from the input element to the container element,
// shift it back to the input element
if (_inputElementFocused && _inputHelper is not null)
{
_inputHelper.Focus();
}
}
private void OnInput(ChangeEventArgs e)
{
if (e.Value != null)
@ -399,10 +410,12 @@ namespace Avalonia.Web.Blazor
if (active)
{
_inputHelper.Show();
_inputElementFocused = true;
_inputHelper.Focus();
}
else
{
_inputElementFocused = false;
_inputHelper.Hide();
}
}

217
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -225,20 +225,17 @@ namespace Avalonia.Win32.Interop
[Flags]
public enum ModifierKeys
{
MK_CONTROL = 0x0008,
MK_NONE = 0x0000,
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
}
@ -514,6 +511,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 PointerInputType
{
PT_NONE = 0x00000000,
PT_POINTER = 0x00000001,
PT_TOUCH = 0x00000002,
PT_PEN = 0x00000003,
PT_MOUSE = 0x00000004,
PT_TOUCHPAD = 0x00000005
}
public enum WindowsMessage : uint
{
WM_NULL = 0x0000,
@ -689,6 +713,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,
DM_POINTERHITTEST = 0x0250,
WM_IME_SETCONTEXT = 0x0281,
WM_IME_NOTIFY = 0x0282,
WM_IME_CONTROL = 0x0283,
@ -844,6 +887,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 ulong PerformanceCount;
public PointerButtonChangeType ButtonChangeType;
}
[StructLayout(LayoutKind.Sequential)]
public struct RGBQUAD
{
@ -911,6 +1082,36 @@ 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);
[DllImport("user32.dll", SetLastError = true)]
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 GetPointerInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_INFO[] pointerInfos);
[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 GetPointerPenInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_PEN_INFO[] penInfos);
[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 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,
MonitorEnumDelegate lpfnEnum, IntPtr dwData);

1
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -68,6 +68,7 @@ namespace Avalonia
/// <remarks>
/// 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.
/// </remarks>
[Obsolete("Multitouch is always enabled on supported Windows versions")]
public bool? EnableMultitouch { get; set; } = true;
/// <summary>

429
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;
@ -26,8 +28,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:
{
@ -82,7 +84,7 @@ namespace Avalonia.Win32
case WindowsMessage.WM_DESTROY:
{
UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null);
// We need to release IMM context and state to avoid leaks.
if (Imm32InputMethod.Current.HWND == _hwnd)
{
@ -108,9 +110,9 @@ namespace Avalonia.Win32
var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam);
_scaling = dpi / 96.0;
ScalingChanged?.Invoke(_scaling);
using (SetResizeReason(PlatformResizeReason.DpiChange))
{
{
SetWindowPos(hWnd,
IntPtr.Zero,
newDisplayRect.left,
@ -178,6 +180,10 @@ namespace Avalonia.Win32
case WindowsMessage.WM_MBUTTONDOWN:
case WindowsMessage.WM_XBUTTONDOWN:
{
if (IsMouseInPointerEnabled)
{
break;
}
shouldTakeFocus = ShouldTakeFocusOnClick;
if (ShouldIgnoreTouchEmulatedMessage())
{
@ -188,7 +194,7 @@ namespace Avalonia.Win32
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
message switch
{
WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown,
WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown,
@ -207,6 +213,10 @@ namespace Avalonia.Win32
case WindowsMessage.WM_MBUTTONUP:
case WindowsMessage.WM_XBUTTONUP:
{
if (IsMouseInPointerEnabled)
{
break;
}
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
@ -216,7 +226,7 @@ namespace Avalonia.Win32
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
message switch
{
WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp,
WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp,
@ -231,11 +241,19 @@ namespace Avalonia.Win32
}
// Mouse capture is lost
case WindowsMessage.WM_CANCELMODE:
_mouseDevice.Capture(null);
if (!IsMouseInPointerEnabled)
{
_mouseDevice.Capture(null);
}
break;
case WindowsMessage.WM_MOUSEMOVE:
{
if (IsMouseInPointerEnabled)
{
break;
}
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
@ -259,42 +277,58 @@ namespace Avalonia.Win32
timestamp,
_owner,
RawPointerEventType.Move,
DipFromLParam(lParam), GetMouseModifiers(wParam));
DipFromLParam(lParam),
GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEWHEEL:
{
if (IsMouseInPointerEnabled)
{
break;
}
e = new RawMouseWheelEventArgs(
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam));
new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta),
GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEHWHEEL:
{
if (IsMouseInPointerEnabled)
{
break;
}
e = new RawMouseWheelEventArgs(
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam));
new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0),
GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSELEAVE:
{
if (IsMouseInPointerEnabled)
{
break;
}
_trackingMouse = false;
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.LeaveWindow,
new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers);
new Point(-1, -1),
WindowsKeyboardDevice.Instance.Modifiers);
break;
}
@ -303,11 +337,15 @@ namespace Avalonia.Win32
case WindowsMessage.WM_NCMBUTTONDOWN:
case WindowsMessage.WM_NCXBUTTONDOWN:
{
if (IsMouseInPointerEnabled)
{
break;
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
message switch
{
WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType
.NonClientLeftButtonDown,
@ -323,6 +361,10 @@ namespace Avalonia.Win32
}
case WindowsMessage.WM_TOUCH:
{
if (_wmPointerEnabled)
{
break;
}
var touchInputCount = wParam.ToInt32();
var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount];
@ -348,6 +390,120 @@ namespace Avalonia.Win32
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_NCPOINTERDOWN:
case WindowsMessage.WM_NCPOINTERUP:
case WindowsMessage.WM_POINTERDOWN:
case WindowsMessage.WM_POINTERUP:
case WindowsMessage.WM_POINTERUPDATE:
{
if (!_wmPointerEnabled)
{
break;
}
GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp);
var eventType = GetEventType(message, info);
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:
{
if (!_wmPointerEnabled)
{
break;
}
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:
{
if (!_wmPointerEnabled)
{
break;
}
GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp);
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.Position, delta, modifiers)
{
RawPointerId = info.pointerId
};
break;
}
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_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_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:
{
//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.
//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:
@ -446,7 +602,7 @@ namespace Avalonia.Win32
case WindowsMessage.WM_GETMINMAXINFO:
{
MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
_maxTrackSize = mmi.ptMaxTrackSize;
if (_minSize.Width > 0)
@ -530,7 +686,7 @@ namespace Avalonia.Win32
if (_managedDrag.PreprocessInputEvent(ref e))
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
#endif
if(shouldTakeFocus)
{
SetFocus(_hwnd);
@ -540,7 +696,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
@ -549,6 +705,11 @@ namespace Avalonia.Win32
_ignoreWmChar = e.Handled;
}
if (s_intermediatePointsPooledList.Count > 0)
{
s_intermediatePointsPooledList.Dispose();
}
if (e.Handled)
{
return IntPtr.Zero;
@ -561,6 +722,196 @@ namespace Avalonia.Win32
}
}
private unsafe Lazy<IReadOnlyList<RawPointerPoint>> CreateLazyIntermediatePoints(POINTER_INFO info)
{
var historyCount = Math.Min((int)info.historyCount, MaxPointerHistorySize);
if (historyCount > 1)
{
return new Lazy<IReadOnlyList<RawPointerPoint>>(() =>
{
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))
{
for (int i = historyCount - 1; i >= 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))
{
for (int i = historyCount - 1; i >= 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))
{
for (int i = historyCount - 1; i >= 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 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);
modifiers = default;
switch (type)
{
case PointerInputType.PT_PEN:
device = _penDevice;
GetPointerPenInfo(pointerId, out var penInfo);
info = penInfo.pointerInfo;
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;
}
if (info.dwTime != 0)
{
timestamp = info.dwTime;
}
modifiers |= GetInputModifiers(info.pointerFlags);
}
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.tiltY
};
}
private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info)
{
var isTouch = info.pointerType == PointerInputType.PT_TOUCH;
if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED))
{
return isTouch ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow;
}
var eventType = ToEventType(info.ButtonChangeType, isTouch);
if (eventType == RawPointerEventType.LeftButtonDown &&
message == WindowsMessage.WM_NCPOINTERDOWN)
{
eventType = RawPointerEventType.NonClientLeftButtonDown;
}
return eventType;
}
private static RawPointerEventType ToEventType(PointerButtonChangeType type, bool isTouch)
{
return type switch
{
PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN when isTouch => RawPointerEventType.TouchBegin,
PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN when !isTouch => RawPointerEventType.LeftButtonDown,
PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown,
PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_DOWN => RawPointerEventType.MiddleButtonDown,
PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_DOWN => RawPointerEventType.XButton1Down,
PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_DOWN => RawPointerEventType.XButton2Down,
PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP when isTouch => RawPointerEventType.TouchEnd,
PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP when !isTouch => RawPointerEventType.LeftButtonUp,
PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_UP => RawPointerEventType.RightButtonUp,
PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_UP => RawPointerEventType.MiddleButtonUp,
PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up,
PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up,
_ when isTouch => RawPointerEventType.TouchUpdate,
_ => RawPointerEventType.Move
};
}
private void UpdateInputMethod(IntPtr hkl)
{
// note: for non-ime language, also create it so that emoji panel tracks cursor
@ -568,11 +919,11 @@ namespace Avalonia.Win32
if (langid == _langid && Imm32InputMethod.Current.HWND == Hwnd)
{
return;
}
}
_langid = langid;
Imm32InputMethod.Current.SetLanguageAndWindow(this, Hwnd, hkl);
}
private static int ToInt32(IntPtr ptr)
@ -597,10 +948,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
@ -613,6 +961,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))
@ -642,5 +995,37 @@ namespace Avalonia.Win32
return modifiers;
}
private static RawInputModifiers GetInputModifiers(PointerFlags flags)
{
var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_FIRSTBUTTON))
{
modifiers |= RawInputModifiers.LeftMouseButton;
}
if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_SECONDBUTTON))
{
modifiers |= RawInputModifiers.RightMouseButton;
}
if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_THIRDBUTTON))
{
modifiers |= RawInputModifiers.MiddleMouseButton;
}
if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_FOURTHBUTTON))
{
modifiers |= RawInputModifiers.XButton1MouseButton;
}
if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_FIFTHBUTTON))
{
modifiers |= RawInputModifiers.XButton2MouseButton;
}
return modifiers;
}
}
}

24
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -22,6 +22,7 @@ using Avalonia.Win32.OpenGl;
using Avalonia.Win32.WinRT;
using Avalonia.Win32.WinRT.Composition;
using static Avalonia.Win32.Interop.UnmanagedMethods;
using Avalonia.Collections.Pooled;
using Avalonia.Metadata;
namespace Avalonia.Win32
@ -69,18 +70,19 @@ 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;
private readonly bool _wmPointerEnabled;
private Win32NativeControlHost _nativeControlHost;
private WndProc _wndProcDelegate;
private string _className;
private IntPtr _hwnd;
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;
@ -97,10 +99,17 @@ namespace Avalonia.Win32
private uint _langid;
private bool _ignoreWmChar;
private const int MaxPointerHistorySize = 512;
private static readonly PooledList<RawPointerPoint> s_intermediatePointsPooledList = new();
private static readonly POINTER_TOUCH_INFO[] s_historyTouchInfos = new POINTER_TOUCH_INFO[MaxPointerHistorySize];
private static readonly POINTER_PEN_INFO[] s_historyPenInfos = new POINTER_PEN_INFO[MaxPointerHistorySize];
private static readonly POINTER_INFO[] s_historyInfos = new POINTER_INFO[MaxPointerHistorySize];
public WindowImpl()
{
_touchDevice = new TouchDevice();
_mouseDevice = new WindowsMouseDevice();
_penDevice = new PenDevice();
#if USE_MANAGED_DRAG
_managedDrag = new ManagedWindowResizeDragHelper(this, capture =>
@ -129,6 +138,8 @@ namespace Avalonia.Win32
egl.Display is AngleWin32EglDisplay angleDisplay &&
angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11;
_wmPointerEnabled = Win32Platform.WindowsVersion >= PlatformConstants.Windows8;
CreateWindow();
_framebuffer = new FramebufferManager(_hwnd);
UpdateInputMethod(GetKeyboardLayout(0));
@ -283,6 +294,8 @@ namespace Avalonia.Win32
protected IntPtr Hwnd => _hwnd;
private bool IsMouseInPointerEnabled => _wmPointerEnabled && IsMouseInPointerEnabled();
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{
TransparencyLevel = EnableBlur(transparencyLevel);
@ -815,12 +828,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)
{

30
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@ -611,6 +611,36 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("foo", notifications[1].NewValue);
}
[Fact]
public void Can_Run_Empty_Batch_Update_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
var notifications = new List<AvaloniaPropertyChangedEventArgs>();
target.Foo = "foo";
target.Bar = "bar";
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.ClearValue(TestClass.BarProperty);
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.BarProperty)
{
target.BeginBatchUpdate();
target.EndBatchUpdate();
}
++raised;
};
target.EndBatchUpdate();
Assert.Null(target.Foo);
Assert.Null(target.Bar);
Assert.Equal(2, raised);
}
public class TestClass : AvaloniaObject
{
public static readonly StyledProperty<string> FooProperty =

24
tests/Avalonia.Base.UnitTests/Input/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
});
}
}
}

63
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@ -594,6 +594,69 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
}
}
[Fact]
public void Should_Update_When_Control_Moved_Causing_Layout_Change()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Decorator moveFrom;
Decorator moveTo;
Canvas moveMe;
var tree = new TestRoot
{
Width = 100,
Height = 100,
Child = new DockPanel
{
Children =
{
(moveFrom = new Decorator
{
Child = moveMe = new Canvas
{
Width = 100,
Height = 100,
},
}),
(moveTo = new Decorator()),
}
}
};
tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize));
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
var moveFromNode = (VisualNode)scene.FindNode(moveFrom);
var moveToNode = (VisualNode)scene.FindNode(moveTo);
Assert.Equal(1, moveFromNode.Children.Count);
Assert.Same(moveMe, moveFromNode.Children[0].Visual);
Assert.Empty(moveToNode.Children);
moveFrom.Child = null;
moveTo.Child = moveMe;
tree.LayoutManager.ExecuteLayoutPass();
scene = scene.CloneScene();
moveFromNode = (VisualNode)scene.FindNode(moveFrom);
moveToNode = (VisualNode)scene.FindNode(moveTo);
moveFromNode.SortChildren(scene);
moveToNode.SortChildren(scene);
sceneBuilder.Update(scene, moveFrom);
sceneBuilder.Update(scene, moveTo);
sceneBuilder.Update(scene, moveMe);
Assert.Empty(moveFromNode.Children);
Assert.Equal(1, moveToNode.Children.Count);
Assert.Same(moveMe, moveToNode.Children[0].Visual);
}
}
[Fact]
public void Should_Update_When_Control_Made_Invisible()
{

105
tests/Avalonia.LeakTests/ControlTests.cs

@ -7,6 +7,7 @@ using System.Reactive.Disposables;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
@ -877,6 +878,110 @@ namespace Avalonia.LeakTests
}
}
[Fact]
public void ToolTip_Is_Freed()
{
using (Start())
{
Func<Window> run = () =>
{
var window = new Window();
var source = new Button
{
Template = new FuncControlTemplate<Button>((parent, _) =>
new Decorator
{
[ToolTip.TipProperty] = new TextBlock
{
[~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
}
}),
};
window.Content = source;
window.Show();
var templateChild = (Decorator)source.GetVisualChildren().Single();
ToolTip.SetIsOpen(templateChild, true);
ToolTip.SetIsOpen(templateChild, false);
// Detach the button from the logical tree, so there is no reference to it
window.Content = null;
// Mock keep reference on a Popup via InvocationsCollection. So let's clear it before.
Mock.Get(window.PlatformImpl).Invocations.Clear();
return window;
};
var result = run();
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
dotMemory.Check(memory =>
{
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBlock>()).ObjectsCount);
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ToolTip>()).ObjectsCount);
});
}
}
[Fact]
public void Flyout_Is_Freed()
{
using (Start())
{
Func<Window> run = () =>
{
var window = new Window();
var source = new Button
{
Template = new FuncControlTemplate<Button>((parent, _) =>
new Button
{
Flyout = new Flyout
{
Content = new TextBlock
{
[~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
}
}
}),
};
window.Content = source;
window.Show();
var templateChild = (Button)source.GetVisualChildren().Single();
templateChild.Flyout!.ShowAt(templateChild);
templateChild.Flyout!.Hide();
// Detach the button from the logical tree, so there is no reference to it
window.Content = null;
// Mock keep reference on a Popup via InvocationsCollection. So let's clear it before.
Mock.Get(window.PlatformImpl).Invocations.Clear();
return window;
};
var result = run();
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
dotMemory.Check(memory =>
{
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBlock>()).ObjectsCount);
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Flyout>()).ObjectsCount);
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Popup>()).ObjectsCount);
});
}
}
private FuncControlTemplate CreateWindowTemplate()
{
return new FuncControlTemplate<Window>((parent, scope) =>

127
tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs

@ -3,9 +3,11 @@ using System.Globalization;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
@ -90,6 +92,131 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("bar", source.Content);
}
[Fact]
public void Should_Work_Inside_Of_Tooltip()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var source = new Button
{
Template = new FuncControlTemplate<Button>((parent, _) =>
new Decorator
{
[ToolTip.TipProperty] = new TextBlock
{
[~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
}
}),
};
window.Content = source;
window.Show();
try
{
var templateChild = (Decorator)source.GetVisualChildren().Single();
ToolTip.SetIsOpen(templateChild, true);
var target = (TextBlock)ToolTip.GetTip(templateChild)!;
Assert.Null(target.Text);
source.Content = "foo";
Assert.Equal("foo", target.Text);
source.Content = "bar";
Assert.Equal("bar", target.Text);
}
finally
{
window.Close();
}
}
}
[Fact]
public void Should_Work_Inside_Of_Popup()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var source = new Button
{
Template = new FuncControlTemplate<Button>((parent, _) =>
new Popup
{
Child = new TextBlock
{
[~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
}
}),
};
window.Content = source;
window.Show();
try
{
var popup = (Popup)source.GetVisualChildren().Single();
popup.IsOpen = true;
var target = (TextBlock)popup.Child!;
target[~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty);
Assert.Null(target.Text);
source.Content = "foo";
Assert.Equal("foo", target.Text);
source.Content = "bar";
Assert.Equal("bar", target.Text);
}
finally
{
window.Close();
}
}
}
[Fact]
public void Should_Work_Inside_Of_Flyout()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var source = new Button
{
Template = new FuncControlTemplate<Button>((parent, _) =>
new Button
{
Flyout = new Flyout
{
Content = new TextBlock
{
[~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
}
}
}),
};
window.Content = source;
window.Show();
try
{
var templateChild = (Button)source.GetVisualChildren().Single();
templateChild.Flyout!.ShowAt(templateChild);
var target = (TextBlock)((Flyout)templateChild.Flyout).Content!;
target[~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty);
Assert.Null(target.Text);
source.Content = "foo";
Assert.Equal("foo", target.Text);
source.Content = "bar";
Assert.Equal("bar", target.Text);
}
finally
{
window.Close();
}
}
}
private class PrefixConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)

51
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@ -958,6 +958,29 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("Foo", toolTip.Content);
}
[Fact]
public void AddChild_Child_Is_Set()
{
var xaml = @"<ObjectWithAddChild xmlns='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml'>Foo</ObjectWithAddChild>";
var target = AvaloniaRuntimeXamlLoader.Parse<ObjectWithAddChild>(xaml);
Assert.NotNull(target);
Assert.Equal("Foo", target.Child);
}
[Fact]
public void AddChildOfT_Child_Is_Set()
{
var xaml = @"<ObjectWithAddChildOfT xmlns='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml'>Foo</ObjectWithAddChildOfT>";
var target = AvaloniaRuntimeXamlLoader.Parse<ObjectWithAddChildOfT>(xaml);
Assert.NotNull(target);
Assert.Null(target.Child);
Assert.Equal("Foo", target.Text);
}
private class SelectedItemsViewModel : INotifyPropertyChanged
{
public string[] Items { get; set; }
@ -977,6 +1000,34 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
}
public class ObjectWithAddChild : IAddChild
{
public object Child { get; set; }
void IAddChild.AddChild(object child)
{
Child = child;
}
}
public class ObjectWithAddChildOfT : IAddChild<string>
{
public string Text { get; set; }
public object Child { get; set; }
void IAddChild.AddChild(object child)
{
Child = child;
}
void IAddChild<string>.AddChild(string child)
{
Text = child;
}
}
public class BasicTestsAttachedPropertyHolder
{
public static AvaloniaProperty<string> FooProperty =

Loading…
Cancel
Save