committed by
GitHub
743 changed files with 43474 additions and 21265 deletions
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,67 @@ |
|||||
|
# Starter pipeline |
||||
|
# Start with a minimal pipeline that you can customize to build and deploy your code. |
||||
|
# Add steps that build, run tests, deploy, and more: |
||||
|
# https://aka.ms/yaml |
||||
|
|
||||
|
trigger: |
||||
|
- master |
||||
|
|
||||
|
jobs: |
||||
|
- job: Mac |
||||
|
pool: |
||||
|
name: 'AvaloniaMacPool' |
||||
|
|
||||
|
steps: |
||||
|
- script: system_profiler SPDisplaysDataType |grep Resolution |
||||
|
|
||||
|
- script: | |
||||
|
pkill node |
||||
|
appium & |
||||
|
pkill IntegrationTestApp |
||||
|
./build.sh CompileNative |
||||
|
rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") |
||||
|
pkill IntegrationTestApp |
||||
|
./samples/IntegrationTestApp/bundle.sh |
||||
|
open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app |
||||
|
pkill IntegrationTestApp |
||||
|
|
||||
|
- task: DotNetCoreCLI@2 |
||||
|
inputs: |
||||
|
command: 'test' |
||||
|
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' |
||||
|
|
||||
|
- script: | |
||||
|
pkill IntegrationTestApp |
||||
|
pkill node |
||||
|
|
||||
|
|
||||
|
- job: Windows |
||||
|
pool: |
||||
|
vmImage: 'windows-2022' |
||||
|
|
||||
|
steps: |
||||
|
- task: UseDotNet@2 |
||||
|
displayName: 'Use .NET Core SDK 6.0.202' |
||||
|
inputs: |
||||
|
version: 6.0.202 |
||||
|
|
||||
|
- task: Windows Application Driver@0 |
||||
|
inputs: |
||||
|
OperationType: 'Start' |
||||
|
AgentResolution: '4K' |
||||
|
displayName: 'Start WinAppDriver' |
||||
|
|
||||
|
- task: DotNetCoreCLI@2 |
||||
|
inputs: |
||||
|
command: 'build' |
||||
|
projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj' |
||||
|
|
||||
|
- task: DotNetCoreCLI@2 |
||||
|
inputs: |
||||
|
command: 'test' |
||||
|
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' |
||||
|
|
||||
|
- task: Windows Application Driver@0 |
||||
|
inputs: |
||||
|
OperationType: 'Stop' |
||||
|
displayName: 'Stop WinAppDriver' |
||||
@ -1,14 +1,10 @@ |
|||||
using System; |
namespace ControlCatalog.Models |
||||
using System.Collections.Generic; |
|
||||
using System.Text; |
|
||||
|
|
||||
namespace ControlCatalog.Models |
|
||||
{ |
{ |
||||
public enum CatalogTheme |
public enum CatalogTheme |
||||
{ |
{ |
||||
FluentLight, |
FluentLight, |
||||
FluentDark, |
FluentDark, |
||||
DefaultLight, |
SimpleLight, |
||||
DefaultDark |
SimpleDark |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,45 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:pages="clr-namespace:ControlCatalog.Pages" |
||||
|
x:Class="ControlCatalog.Pages.CompositionPage"> |
||||
|
<StackPanel> |
||||
|
<TextBlock Classes="h1">Implicit animations</TextBlock> |
||||
|
<Grid ColumnDefinitions="*,10,40" Margin="0 0 40 0"> |
||||
|
<ItemsControl x:Name="Items"> |
||||
|
<ItemsControl.ItemsPanel> |
||||
|
<ItemsPanelTemplate> |
||||
|
<WrapPanel/> |
||||
|
</ItemsPanelTemplate> |
||||
|
</ItemsControl.ItemsPanel> |
||||
|
<ItemsControl.DataTemplates> |
||||
|
<DataTemplate DataType="pages:CompositionPageColorItem"> |
||||
|
<Border |
||||
|
pages:CompositionPage.EnableAnimations="True" |
||||
|
Padding="10" BorderBrush="Gray" BorderThickness="2" |
||||
|
Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10"> |
||||
|
<TextBlock Text="{Binding ColorHexValue}"/> |
||||
|
</Border> |
||||
|
</DataTemplate> |
||||
|
</ItemsControl.DataTemplates> |
||||
|
</ItemsControl> |
||||
|
<GridSplitter Margin="2" BorderThickness="1" BorderBrush="Gray" |
||||
|
Background="#e0e0e0" Grid.Column="1" |
||||
|
ResizeDirection="Columns" ResizeBehavior="PreviousAndNext" |
||||
|
/> |
||||
|
<Border Grid.Column="2"> |
||||
|
<LayoutTransformControl |
||||
|
HorizontalAlignment="Center" |
||||
|
|
||||
|
MinWidth="30"> |
||||
|
<LayoutTransformControl.LayoutTransform> |
||||
|
<RotateTransform Angle="90"/> |
||||
|
</LayoutTransformControl.LayoutTransform> |
||||
|
|
||||
|
<TextBlock>Resize me</TextBlock> |
||||
|
</LayoutTransformControl> |
||||
|
</Border> |
||||
|
</Grid> |
||||
|
|
||||
|
|
||||
|
</StackPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,153 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Numerics; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Markup.Xaml; |
||||
|
using Avalonia.Markup.Xaml.Templates; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Rendering.Composition; |
||||
|
using Avalonia.Rendering.Composition.Animations; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class CompositionPage : UserControl |
||||
|
{ |
||||
|
private ImplicitAnimationCollection _implicitAnimations; |
||||
|
|
||||
|
public CompositionPage() |
||||
|
{ |
||||
|
AvaloniaXamlLoader.Load(this); |
||||
|
} |
||||
|
|
||||
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
||||
|
{ |
||||
|
base.OnAttachedToVisualTree(e); |
||||
|
this.FindControl<ItemsControl>("Items").Items = CreateColorItems(); |
||||
|
} |
||||
|
|
||||
|
private List<CompositionPageColorItem> CreateColorItems() |
||||
|
{ |
||||
|
var list = new List<CompositionPageColorItem>(); |
||||
|
|
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 185, 0))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 231, 72, 86))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 120, 215))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 153, 188))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 122, 117, 116))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 118, 118, 118))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 141, 0))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 232, 17, 35))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 99, 177))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 45, 125, 154))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 93, 90, 88))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 76, 74, 72))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 247, 99, 12))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 234, 0, 94))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 142, 140, 216))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 183, 195))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 104, 118, 138))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 105, 121, 126))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 202, 80, 16))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 195, 0, 82))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 107, 105, 214))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 3, 131, 135))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 81, 92, 107))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 74, 84, 89))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 218, 59, 1))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 227, 0, 140))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 135, 100, 184))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 178, 148))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 86, 124, 115))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 100, 124, 100))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 239, 105, 80))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 191, 0, 119))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 116, 77, 169))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 1, 133, 116))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 72, 104, 96))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 82, 94, 84))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 209, 52, 56))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 194, 57, 179))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 177, 70, 194))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 204, 106))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 73, 130, 5))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 132, 117, 69))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 67, 67))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 154, 0, 137))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 136, 23, 152))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 137, 62))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 124, 16))); |
||||
|
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 126, 115, 95))); |
||||
|
|
||||
|
return list; |
||||
|
} |
||||
|
|
||||
|
private void EnsureImplicitAnimations() |
||||
|
{ |
||||
|
if (_implicitAnimations == null) |
||||
|
{ |
||||
|
var compositor = ElementComposition.GetElementVisual(this)!.Compositor; |
||||
|
|
||||
|
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); |
||||
|
offsetAnimation.Target = "Offset"; |
||||
|
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); |
||||
|
offsetAnimation.Duration = TimeSpan.FromMilliseconds(400); |
||||
|
|
||||
|
var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); |
||||
|
rotationAnimation.Target = "RotationAngle"; |
||||
|
rotationAnimation.InsertKeyFrame(.5f, 0.160f); |
||||
|
rotationAnimation.InsertKeyFrame(1f, 0f); |
||||
|
rotationAnimation.Duration = TimeSpan.FromMilliseconds(400); |
||||
|
|
||||
|
var animationGroup = compositor.CreateAnimationGroup(); |
||||
|
animationGroup.Add(offsetAnimation); |
||||
|
animationGroup.Add(rotationAnimation); |
||||
|
|
||||
|
_implicitAnimations = compositor.CreateImplicitAnimationCollection(); |
||||
|
_implicitAnimations["Offset"] = animationGroup; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void SetEnableAnimations(Border border, bool value) |
||||
|
{ |
||||
|
|
||||
|
var page = border.FindAncestorOfType<CompositionPage>(); |
||||
|
if (page == null) |
||||
|
{ |
||||
|
border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); }; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (ElementComposition.GetElementVisual(page) == null) |
||||
|
return; |
||||
|
|
||||
|
page.EnsureImplicitAnimations(); |
||||
|
ElementComposition.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = |
||||
|
page._implicitAnimations; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class CompositionPageColorItem |
||||
|
{ |
||||
|
public Color Color { get; private set; } |
||||
|
|
||||
|
public SolidColorBrush ColorBrush |
||||
|
{ |
||||
|
get { return new SolidColorBrush(Color); } |
||||
|
} |
||||
|
|
||||
|
public String ColorHexValue |
||||
|
{ |
||||
|
get { return Color.ToString().Substring(3).ToUpperInvariant(); } |
||||
|
} |
||||
|
|
||||
|
public CompositionPageColorItem(Color color) |
||||
|
{ |
||||
|
Color = color; |
||||
|
} |
||||
|
} |
||||
@ -1,29 +1,57 @@ |
|||||
<UserControl xmlns="https://github.com/avaloniaui" |
<UserControl x:Class="ControlCatalog.Pages.DialogsPage" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
xmlns="https://github.com/avaloniaui" |
||||
x:Class="ControlCatalog.Pages.DialogsPage"> |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
<StackPanel Orientation="Vertical" Spacing="4" Margin="4"> |
<StackPanel Margin="4" |
||||
<CheckBox Name="UseFilters">Use filters</CheckBox> |
Orientation="Vertical" |
||||
<Button Name="OpenFile">_Open File</Button> |
Spacing="4"> |
||||
<Button Name="OpenMultipleFiles">Open _Multiple File</Button> |
|
||||
<Button Name="SaveFile">_Save File</Button> |
|
||||
<Button Name="SelectFolder">Select Fo_lder</Button> |
|
||||
<Button Name="OpenBoth">Select _Both</Button> |
|
||||
|
|
||||
<TextBlock x:Name="PickerLastResultsVisible" |
<TextBlock Text="Windows:" /> |
||||
Classes="h2" |
|
||||
IsVisible="False" |
|
||||
Text="Last picker results:" /> |
|
||||
<ItemsPresenter x:Name="PickerLastResults" /> |
|
||||
|
|
||||
<TextBlock Margin="0, 8, 0, 0" |
<Expander Header="Window dialogs"> |
||||
Classes="h1" |
<StackPanel Spacing="4"> |
||||
Text="Window dialogs" /> |
<Button Name="DecoratedWindow">Decorated _window</Button> |
||||
<Button Name="DecoratedWindow">Decorated _window</Button> |
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button> |
||||
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button> |
<Button Name="Dialog" ToolTip.Tip="Shows a dialog">_Dialog</Button> |
||||
<Button Name="Dialog" ToolTip.Tip="Shows a dialog">_Dialog</Button> |
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button> |
||||
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button> |
<Button Name="OwnedWindow">Own_ed window</Button> |
||||
<Button Name="OwnedWindow">Own_ed window</Button> |
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button> |
||||
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button> |
</StackPanel> |
||||
|
</Expander> |
||||
|
|
||||
|
<TextBlock Margin="0,20,0,0" Text="Pickers:" /> |
||||
|
|
||||
|
<CheckBox Name="UseFilters">Use filters</CheckBox> |
||||
|
<Expander Header="FilePicker API"> |
||||
|
<StackPanel Spacing="4"> |
||||
|
<CheckBox Name="ForceManaged">Force managed dialog</CheckBox> |
||||
|
<CheckBox Name="OpenMultiple">Open multiple</CheckBox> |
||||
|
<Button Name="OpenFolderPicker">Select Fo_lder</Button> |
||||
|
<Button Name="OpenFilePicker">_Open File</Button> |
||||
|
<Button Name="SaveFilePicker">_Save File</Button> |
||||
|
<Button Name="OpenFileFromBookmark">Open File Bookmark</Button> |
||||
|
<Button Name="OpenFolderFromBookmark">Open Folder Bookmark</Button> |
||||
|
</StackPanel> |
||||
|
</Expander> |
||||
|
<Expander Header="Legacy OpenFileDialog"> |
||||
|
<StackPanel Spacing="4"> |
||||
|
<Button Name="OpenFile">_Open File</Button> |
||||
|
<Button Name="OpenMultipleFiles">Open _Multiple File</Button> |
||||
|
<Button Name="SaveFile">_Save File</Button> |
||||
|
<Button Name="SelectFolder">Select Fo_lder</Button> |
||||
|
<Button Name="OpenBoth">Select _Both</Button> |
||||
|
</StackPanel> |
||||
|
</Expander> |
||||
|
|
||||
|
<TextBlock x:Name="PickerLastResultsVisible" |
||||
|
Classes="h2" |
||||
|
IsVisible="False" |
||||
|
Text="Last picker results:" /> |
||||
|
<ItemsPresenter x:Name="PickerLastResults" /> |
||||
|
|
||||
|
<TextBox Name="BookmarkContainer" Watermark="Bookmark" /> |
||||
|
<TextBox Name="OpenedFileContent" |
||||
|
MaxLines="10" |
||||
|
Watermark="Picked file content" /> |
||||
|
|
||||
</StackPanel> |
</StackPanel> |
||||
</UserControl> |
</UserControl> |
||||
|
|||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
@ -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> |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using Avalonia; |
||||
|
using MiniMvvm; |
||||
|
|
||||
|
namespace ControlCatalog.ViewModels |
||||
|
{ |
||||
|
public class ExpanderPageViewModel : ViewModelBase |
||||
|
{ |
||||
|
private object _cornerRadius = AvaloniaProperty.UnsetValue; |
||||
|
private bool _rounded; |
||||
|
|
||||
|
public object CornerRadius |
||||
|
{ |
||||
|
get => _cornerRadius; |
||||
|
private set => RaiseAndSetIfChanged(ref _cornerRadius, value); |
||||
|
} |
||||
|
|
||||
|
public bool Rounded |
||||
|
{ |
||||
|
get => _rounded; |
||||
|
set |
||||
|
{ |
||||
|
if (RaiseAndSetIfChanged(ref _rounded, value)) |
||||
|
CornerRadius = _rounded ? new CornerRadius(25) : AvaloniaProperty.UnsetValue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,6 +1,5 @@ |
|||||
<Application xmlns="https://github.com/avaloniaui"> |
<Application xmlns="https://github.com/avaloniaui"> |
||||
<Application.Styles> |
<Application.Styles> |
||||
<StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/> |
<SimpleTheme Mode="Light" /> |
||||
<StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/> |
|
||||
</Application.Styles> |
</Application.Styles> |
||||
</Application> |
</Application> |
||||
|
|||||
@ -1,6 +1,5 @@ |
|||||
<Application xmlns="https://github.com/avaloniaui"> |
<Application xmlns="https://github.com/avaloniaui"> |
||||
<Application.Styles> |
<Application.Styles> |
||||
<StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/> |
<SimpleTheme Mode="Light" /> |
||||
<StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/> |
|
||||
</Application.Styles> |
</Application.Styles> |
||||
</Application> |
</Application> |
||||
|
|||||
@ -1,9 +1,7 @@ |
|||||
<Application |
<Application xmlns="https://github.com/avaloniaui" |
||||
xmlns="https://github.com/avaloniaui" |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
x:Class="VirtualizationDemo.App"> |
||||
x:Class="VirtualizationDemo.App"> |
<Application.Styles> |
||||
<Application.Styles> |
<SimpleTheme /> |
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/> |
</Application.Styles> |
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/> |
</Application> |
||||
</Application.Styles> |
|
||||
</Application> |
|
||||
|
|||||
@ -1,6 +1,5 @@ |
|||||
<Application xmlns="https://github.com/avaloniaui"> |
<Application xmlns="https://github.com/avaloniaui"> |
||||
<Application.Styles> |
<Application.Styles> |
||||
<StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/> |
<SimpleTheme Mode="Light" /> |
||||
<StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/> |
|
||||
</Application.Styles> |
</Application.Styles> |
||||
</Application> |
</Application> |
||||
|
|||||
@ -0,0 +1,269 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Android.Content; |
||||
|
using Android.Provider; |
||||
|
using Avalonia.Logging; |
||||
|
using Avalonia.Platform.Storage; |
||||
|
using Java.Lang; |
||||
|
using AndroidUri = Android.Net.Uri; |
||||
|
using Exception = System.Exception; |
||||
|
using JavaFile = Java.IO.File; |
||||
|
|
||||
|
namespace Avalonia.Android.Platform.Storage; |
||||
|
|
||||
|
internal abstract class AndroidStorageItem : IStorageBookmarkItem |
||||
|
{ |
||||
|
private Context? _context; |
||||
|
|
||||
|
protected AndroidStorageItem(Context context, AndroidUri uri) |
||||
|
{ |
||||
|
_context = context; |
||||
|
Uri = uri; |
||||
|
} |
||||
|
|
||||
|
internal AndroidUri Uri { get; } |
||||
|
|
||||
|
protected Context Context => _context ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); |
||||
|
|
||||
|
public string Name => GetColumnValue(Context, Uri, MediaStore.IMediaColumns.DisplayName) |
||||
|
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty; |
||||
|
|
||||
|
public bool CanBookmark => true; |
||||
|
|
||||
|
public Task<string?> SaveBookmarkAsync() |
||||
|
{ |
||||
|
Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); |
||||
|
return Task.FromResult(Uri.ToString()); |
||||
|
} |
||||
|
|
||||
|
public Task ReleaseBookmarkAsync() |
||||
|
{ |
||||
|
Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) |
||||
|
{ |
||||
|
uri = new Uri(Uri.ToString()!); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public abstract Task<StorageItemProperties> GetBasicPropertiesAsync(); |
||||
|
|
||||
|
protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var projection = new[] { column }; |
||||
|
using var cursor = context.ContentResolver!.Query(contentUri, projection, selection, selectionArgs, null); |
||||
|
if (cursor?.MoveToFirst() == true) |
||||
|
{ |
||||
|
var columnIndex = cursor.GetColumnIndex(column); |
||||
|
if (columnIndex != -1) |
||||
|
return cursor.GetString(columnIndex); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "File metadata reader failed: '{Exception}'", ex); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public Task<IStorageFolder?> GetParentAsync() |
||||
|
{ |
||||
|
using var javaFile = new JavaFile(Uri.Path!); |
||||
|
|
||||
|
// Java file represents files AND directories. Don't be confused.
|
||||
|
if (javaFile.ParentFile is {} parentFile |
||||
|
&& AndroidUri.FromFile(parentFile) is {} androidUri) |
||||
|
{ |
||||
|
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Context, androidUri)); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult<IStorageFolder?>(null); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_context = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder |
||||
|
{ |
||||
|
public AndroidStorageFolder(Context context, AndroidUri uri) : base(context, uri) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public override Task<StorageItemProperties> GetBasicPropertiesAsync() |
||||
|
{ |
||||
|
return Task.FromResult(new StorageItemProperties()); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync() |
||||
|
{ |
||||
|
using var javaFile = new JavaFile(Uri.Path!); |
||||
|
|
||||
|
// Java file represents files AND directories. Don't be confused.
|
||||
|
var files = await javaFile.ListFilesAsync().ConfigureAwait(false); |
||||
|
if (files is null) |
||||
|
{ |
||||
|
return Array.Empty<IStorageItem>(); |
||||
|
} |
||||
|
|
||||
|
return files |
||||
|
.Select(f => (file: f, uri: AndroidUri.FromFile(f))) |
||||
|
.Where(t => t.uri is not null) |
||||
|
.Select(t => t.file switch |
||||
|
{ |
||||
|
{ IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!), |
||||
|
{ IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!), |
||||
|
_ => null |
||||
|
}) |
||||
|
.Where(i => i is not null) |
||||
|
.ToArray()!; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile |
||||
|
{ |
||||
|
public AndroidStorageFile(Context context, AndroidUri uri) : base(context, uri) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public bool CanOpenRead => true; |
||||
|
|
||||
|
public bool CanOpenWrite => true; |
||||
|
|
||||
|
public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false) |
||||
|
?? throw new InvalidOperationException("Failed to open content stream")); |
||||
|
|
||||
|
public Task<Stream> OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true) |
||||
|
?? throw new InvalidOperationException("Failed to open content stream")); |
||||
|
|
||||
|
private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) |
||||
|
{ |
||||
|
var isVirtual = IsVirtualFile(context, uri); |
||||
|
if (isVirtual) |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "Content URI was virtual: '{Uri}'", uri); |
||||
|
return GetVirtualFileStream(context, uri, isOutput); |
||||
|
} |
||||
|
|
||||
|
return isOutput |
||||
|
? context.ContentResolver?.OpenOutputStream(uri) |
||||
|
: context.ContentResolver?.OpenInputStream(uri); |
||||
|
} |
||||
|
|
||||
|
private bool IsVirtualFile(Context context, AndroidUri uri) |
||||
|
{ |
||||
|
if (!DocumentsContract.IsDocumentUri(context, uri)) |
||||
|
return false; |
||||
|
|
||||
|
var value = GetColumnValue(context, uri, DocumentsContract.Document.ColumnFlags); |
||||
|
if (!string.IsNullOrEmpty(value) && int.TryParse(value, out var flagsInt)) |
||||
|
{ |
||||
|
var flags = (DocumentContractFlags)flagsInt; |
||||
|
return flags.HasFlag(DocumentContractFlags.VirtualDocument); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private Stream? GetVirtualFileStream(Context context, AndroidUri uri, bool isOutput) |
||||
|
{ |
||||
|
var mimeTypes = context.ContentResolver?.GetStreamTypes(uri, FilePickerFileTypes.All.MimeTypes![0]); |
||||
|
if (mimeTypes?.Length >= 1) |
||||
|
{ |
||||
|
var mimeType = mimeTypes[0]; |
||||
|
var asset = context.ContentResolver! |
||||
|
.OpenTypedAssetFileDescriptor(uri, mimeType, null); |
||||
|
|
||||
|
var stream = isOutput |
||||
|
? asset?.CreateOutputStream() |
||||
|
: asset?.CreateInputStream(); |
||||
|
|
||||
|
return stream; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public override Task<StorageItemProperties> GetBasicPropertiesAsync() |
||||
|
{ |
||||
|
ulong? size = null; |
||||
|
DateTimeOffset? itemDate = null; |
||||
|
DateTimeOffset? dateModified = null; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var projection = new[] |
||||
|
{ |
||||
|
MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded, |
||||
|
MediaStore.IMediaColumns.DateModified |
||||
|
}; |
||||
|
using var cursor = Context.ContentResolver!.Query(Uri, projection, null, null, null); |
||||
|
|
||||
|
if (cursor?.MoveToFirst() == true) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.Size); |
||||
|
if (columnIndex != -1) |
||||
|
{ |
||||
|
size = (ulong)cursor.GetLong(columnIndex); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? |
||||
|
.Log(this, "File Size metadata reader failed: '{Exception}'", ex); |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateAdded); |
||||
|
if (columnIndex != -1) |
||||
|
{ |
||||
|
var longValue = cursor.GetLong(columnIndex); |
||||
|
itemDate = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null; |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? |
||||
|
.Log(this, "File DateAdded metadata reader failed: '{Exception}'", ex); |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateModified); |
||||
|
if (columnIndex != -1) |
||||
|
{ |
||||
|
var longValue = cursor.GetLong(columnIndex); |
||||
|
dateModified = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null; |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? |
||||
|
.Log(this, "File DateAdded metadata reader failed: '{Exception}'", ex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
catch (UnsupportedOperationException) |
||||
|
{ |
||||
|
// It's not possible to get parameters of some files/folders.
|
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(new StorageItemProperties(size, itemDate, dateModified)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,177 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Android.App; |
||||
|
using Android.Content; |
||||
|
using Android.Provider; |
||||
|
using Avalonia.Platform.Storage; |
||||
|
using AndroidUri = Android.Net.Uri; |
||||
|
|
||||
|
namespace Avalonia.Android.Platform.Storage; |
||||
|
|
||||
|
internal class AndroidStorageProvider : IStorageProvider |
||||
|
{ |
||||
|
private readonly AvaloniaActivity _activity; |
||||
|
private int _lastRequestCode = 20000; |
||||
|
|
||||
|
public AndroidStorageProvider(AvaloniaActivity activity) |
||||
|
{ |
||||
|
_activity = activity; |
||||
|
} |
||||
|
|
||||
|
public bool CanOpen => OperatingSystem.IsAndroidVersionAtLeast(19); |
||||
|
|
||||
|
public bool CanSave => OperatingSystem.IsAndroidVersionAtLeast(19); |
||||
|
|
||||
|
public bool CanPickFolder => OperatingSystem.IsAndroidVersionAtLeast(21); |
||||
|
|
||||
|
public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) |
||||
|
{ |
||||
|
var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark)); |
||||
|
return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri)); |
||||
|
} |
||||
|
|
||||
|
public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark) |
||||
|
{ |
||||
|
var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark)); |
||||
|
return Task.FromResult<IStorageBookmarkFile?>(new AndroidStorageFile(_activity, uri)); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
||||
|
{ |
||||
|
var mimeTypes = options.FileTypeFilter?.Where(t => t != FilePickerFileTypes.All) |
||||
|
.SelectMany(f => f.MimeTypes ?? Array.Empty<string>()).Distinct().ToArray() ?? Array.Empty<string>(); |
||||
|
|
||||
|
var intent = new Intent(Intent.ActionOpenDocument) |
||||
|
.AddCategory(Intent.CategoryOpenable) |
||||
|
.PutExtra(Intent.ExtraAllowMultiple, options.AllowMultiple) |
||||
|
.SetType(FilePickerFileTypes.All.MimeTypes![0]); |
||||
|
if (mimeTypes.Length > 0) |
||||
|
{ |
||||
|
intent = intent.PutExtra(Intent.ExtraMimeTypes, mimeTypes); |
||||
|
} |
||||
|
|
||||
|
if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri) |
||||
|
{ |
||||
|
intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri); |
||||
|
} |
||||
|
|
||||
|
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select file"); |
||||
|
|
||||
|
var uris = await StartActivity(pickerIntent, false); |
||||
|
return uris.Select(u => new AndroidStorageFile(_activity, u)).ToArray(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
||||
|
{ |
||||
|
var mimeTypes = options.FileTypeChoices?.Where(t => t != FilePickerFileTypes.All) |
||||
|
.SelectMany(f => f.MimeTypes ?? Array.Empty<string>()).Distinct().ToArray() ?? Array.Empty<string>(); |
||||
|
|
||||
|
var intent = new Intent(Intent.ActionCreateDocument) |
||||
|
.AddCategory(Intent.CategoryOpenable) |
||||
|
.SetType(FilePickerFileTypes.All.MimeTypes![0]); |
||||
|
if (mimeTypes.Length > 0) |
||||
|
{ |
||||
|
intent = intent.PutExtra(Intent.ExtraMimeTypes, mimeTypes); |
||||
|
} |
||||
|
|
||||
|
if (options.SuggestedFileName is { } fileName) |
||||
|
{ |
||||
|
if (options.DefaultExtension is { } ext) |
||||
|
{ |
||||
|
fileName += ext.StartsWith('.') ? ext : "." + ext; |
||||
|
} |
||||
|
intent = intent.PutExtra(Intent.ExtraTitle, fileName); |
||||
|
} |
||||
|
|
||||
|
if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri) |
||||
|
{ |
||||
|
intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri); |
||||
|
} |
||||
|
|
||||
|
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Save file"); |
||||
|
|
||||
|
var uris = await StartActivity(pickerIntent, true); |
||||
|
return uris.Select(u => new AndroidStorageFile(_activity, u)).FirstOrDefault(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
||||
|
{ |
||||
|
var intent = new Intent(Intent.ActionOpenDocumentTree) |
||||
|
.PutExtra(Intent.ExtraAllowMultiple, options.AllowMultiple); |
||||
|
if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri) |
||||
|
{ |
||||
|
intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri); |
||||
|
} |
||||
|
|
||||
|
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder"); |
||||
|
|
||||
|
var uris = await StartActivity(pickerIntent, false); |
||||
|
return uris.Select(u => new AndroidStorageFolder(_activity, u)).ToArray(); |
||||
|
} |
||||
|
|
||||
|
private async Task<List<AndroidUri>> StartActivity(Intent? pickerIntent, bool singleResult) |
||||
|
{ |
||||
|
var resultList = new List<AndroidUri>(1); |
||||
|
var tcs = new TaskCompletionSource<Intent?>(); |
||||
|
var currentRequestCode = _lastRequestCode++; |
||||
|
|
||||
|
_activity.ActivityResult += OnActivityResult; |
||||
|
_activity.StartActivityForResult(pickerIntent, currentRequestCode); |
||||
|
|
||||
|
var result = await tcs.Task; |
||||
|
|
||||
|
if (result != null) |
||||
|
{ |
||||
|
// ClipData first to avoid issue with multiple files selection.
|
||||
|
if (!singleResult && result.ClipData is { } clipData) |
||||
|
{ |
||||
|
for (var i = 0; i < clipData.ItemCount; i++) |
||||
|
{ |
||||
|
var uri = clipData.GetItemAt(i)?.Uri; |
||||
|
if (uri != null) |
||||
|
{ |
||||
|
resultList.Add(uri); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else if (result.Data is { } uri) |
||||
|
{ |
||||
|
resultList.Add(uri); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (result?.HasExtra("error") == true) |
||||
|
{ |
||||
|
throw new Exception(result.GetStringExtra("error")); |
||||
|
} |
||||
|
|
||||
|
return resultList; |
||||
|
|
||||
|
void OnActivityResult(int requestCode, Result resultCode, Intent data) |
||||
|
{ |
||||
|
if (currentRequestCode != requestCode) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_activity.ActivityResult -= OnActivityResult; |
||||
|
|
||||
|
_ = tcs.TrySetResult(resultCode == Result.Ok ? data : null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static AndroidUri? TryGetInitialUri(IStorageFolder? folder) |
||||
|
{ |
||||
|
if (OperatingSystem.IsAndroidVersionAtLeast(26) |
||||
|
&& (folder as AndroidStorageItem)?.Uri is { } uri) |
||||
|
{ |
||||
|
return uri; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
@ -1,20 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Controls.Platform; |
|
||||
|
|
||||
namespace Avalonia.Android |
|
||||
{ |
|
||||
internal class SystemDialogImpl : ISystemDialogImpl |
|
||||
{ |
|
||||
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
|
|
||||
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) |
|
||||
{ |
|
||||
throw new NotImplementedException(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,306 @@ |
|||||
|
// ReSharper disable InconsistentNaming
|
||||
|
// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc
|
||||
|
|
||||
|
using System; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
// ReSharper disable CompareOfFloatsByEqualityOperator
|
||||
|
// ReSharper disable CommentTypo
|
||||
|
// ReSharper disable MemberCanBePrivate.Global
|
||||
|
// ReSharper disable TooWideLocalVariableScope
|
||||
|
// ReSharper disable UnusedMember.Global
|
||||
|
#pragma warning disable 649
|
||||
|
|
||||
|
namespace Avalonia.Animation.Easings |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a cubic bezier curve and can compute Y coordinate for a given X
|
||||
|
/// </summary>
|
||||
|
internal unsafe struct CubicBezier |
||||
|
{ |
||||
|
const int CUBIC_BEZIER_SPLINE_SAMPLES = 11; |
||||
|
double ax_; |
||||
|
double bx_; |
||||
|
double cx_; |
||||
|
|
||||
|
double ay_; |
||||
|
double by_; |
||||
|
double cy_; |
||||
|
|
||||
|
double start_gradient_; |
||||
|
double end_gradient_; |
||||
|
|
||||
|
double range_min_; |
||||
|
double range_max_; |
||||
|
private bool monotonically_increasing_; |
||||
|
|
||||
|
fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES]; |
||||
|
|
||||
|
public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this() |
||||
|
{ |
||||
|
InitCoefficients(p1x, p1y, p2x, p2y); |
||||
|
InitGradients(p1x, p1y, p2x, p2y); |
||||
|
InitRange(p1y, p2y); |
||||
|
InitSpline(); |
||||
|
} |
||||
|
|
||||
|
public readonly double SampleCurveX(double t) |
||||
|
{ |
||||
|
// `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
|
||||
|
return ((ax_ * t + bx_) * t + cx_) * t; |
||||
|
} |
||||
|
|
||||
|
readonly double SampleCurveY(double t) |
||||
|
{ |
||||
|
return ((ay_ * t + by_) * t + cy_) * t; |
||||
|
} |
||||
|
|
||||
|
readonly double SampleCurveDerivativeX(double t) |
||||
|
{ |
||||
|
return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_; |
||||
|
} |
||||
|
|
||||
|
readonly double SampleCurveDerivativeY(double t) |
||||
|
{ |
||||
|
return (3.0 * ay_ * t + 2.0 * by_) * t + cy_; |
||||
|
} |
||||
|
|
||||
|
public readonly double SolveWithEpsilon(double x, double epsilon) |
||||
|
{ |
||||
|
if (x < 0.0) |
||||
|
return 0.0 + start_gradient_ * x; |
||||
|
if (x > 1.0) |
||||
|
return 1.0 + end_gradient_ * (x - 1.0); |
||||
|
return SampleCurveY(SolveCurveX(x, epsilon)); |
||||
|
} |
||||
|
|
||||
|
void InitCoefficients(double p1x, |
||||
|
double p1y, |
||||
|
double p2x, |
||||
|
double p2y) |
||||
|
{ |
||||
|
// Calculate the polynomial coefficients, implicit first and last control
|
||||
|
// points are (0,0) and (1,1).
|
||||
|
cx_ = 3.0 * p1x; |
||||
|
bx_ = 3.0 * (p2x - p1x) - cx_; |
||||
|
ax_ = 1.0 - cx_ - bx_; |
||||
|
|
||||
|
cy_ = 3.0 * p1y; |
||||
|
by_ = 3.0 * (p2y - p1y) - cy_; |
||||
|
ay_ = 1.0 - cy_ - by_; |
||||
|
|
||||
|
#if DEBUG
|
||||
|
// Bezier curves with x-coordinates outside the range [0,1] for internal
|
||||
|
// control points may have multiple values for t for a given value of x.
|
||||
|
// In this case, calls to SolveCurveX may produce ambiguous results.
|
||||
|
monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1; |
||||
|
#endif
|
||||
|
} |
||||
|
|
||||
|
void InitGradients(double p1x, |
||||
|
double p1y, |
||||
|
double p2x, |
||||
|
double p2y) |
||||
|
{ |
||||
|
// End-point gradients are used to calculate timing function results
|
||||
|
// outside the range [0, 1].
|
||||
|
//
|
||||
|
// There are four possibilities for the gradient at each end:
|
||||
|
// (1) the closest control point is not horizontally coincident with regard to
|
||||
|
// (0, 0) or (1, 1). In this case the line between the end point and
|
||||
|
// the control point is tangent to the bezier at the end point.
|
||||
|
// (2) the closest control point is coincident with the end point. In
|
||||
|
// this case the line between the end point and the far control
|
||||
|
// point is tangent to the bezier at the end point.
|
||||
|
// (3) both internal control points are coincident with an endpoint. There
|
||||
|
// are two special case that fall into this category:
|
||||
|
// CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are
|
||||
|
// equivalent to linear.
|
||||
|
// (4) the closest control point is horizontally coincident with the end
|
||||
|
// point, but vertically distinct. In this case the gradient at the
|
||||
|
// end point is Infinite. However, this causes issues when
|
||||
|
// interpolating. As a result, we break down to a simple case of
|
||||
|
// 0 gradient under these conditions.
|
||||
|
|
||||
|
if (p1x > 0) |
||||
|
start_gradient_ = p1y / p1x; |
||||
|
else if (p1y == 0 && p2x > 0) |
||||
|
start_gradient_ = p2y / p2x; |
||||
|
else if (p1y == 0 && p2y == 0) |
||||
|
start_gradient_ = 1; |
||||
|
else |
||||
|
start_gradient_ = 0; |
||||
|
|
||||
|
if (p2x < 1) |
||||
|
end_gradient_ = (p2y - 1) / (p2x - 1); |
||||
|
else if (p2y == 1 && p1x < 1) |
||||
|
end_gradient_ = (p1y - 1) / (p1x - 1); |
||||
|
else if (p2y == 1 && p1y == 1) |
||||
|
end_gradient_ = 1; |
||||
|
else |
||||
|
end_gradient_ = 0; |
||||
|
} |
||||
|
|
||||
|
const double kBezierEpsilon = 1e-7; |
||||
|
|
||||
|
void InitRange(double p1y, double p2y) |
||||
|
{ |
||||
|
range_min_ = 0; |
||||
|
range_max_ = 1; |
||||
|
if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1) |
||||
|
return; |
||||
|
|
||||
|
double epsilon = kBezierEpsilon; |
||||
|
|
||||
|
// Represent the function's derivative in the form at^2 + bt + c
|
||||
|
// as in sampleCurveDerivativeY.
|
||||
|
// (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros
|
||||
|
// but does not actually give the slope of the curve.)
|
||||
|
double a = 3.0 * ay_; |
||||
|
double b = 2.0 * by_; |
||||
|
double c = cy_; |
||||
|
|
||||
|
// Check if the derivative is constant.
|
||||
|
if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon) |
||||
|
return; |
||||
|
|
||||
|
// Zeros of the function's derivative.
|
||||
|
double t1; |
||||
|
double t2 = 0; |
||||
|
|
||||
|
if (Math.Abs(a) < epsilon) |
||||
|
{ |
||||
|
// The function's derivative is linear.
|
||||
|
t1 = -c / b; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// The function's derivative is a quadratic. We find the zeros of this
|
||||
|
// quadratic using the quadratic formula.
|
||||
|
double discriminant = b * b - 4 * a * c; |
||||
|
if (discriminant < 0) |
||||
|
return; |
||||
|
double discriminant_sqrt = Math.Sqrt(discriminant); |
||||
|
t1 = (-b + discriminant_sqrt) / (2 * a); |
||||
|
t2 = (-b - discriminant_sqrt) / (2 * a); |
||||
|
} |
||||
|
|
||||
|
double sol1 = 0; |
||||
|
double sol2 = 0; |
||||
|
|
||||
|
// If the solution is in the range [0,1] then we include it, otherwise we
|
||||
|
// ignore it.
|
||||
|
|
||||
|
// An interesting fact about these beziers is that they are only
|
||||
|
// actually evaluated in [0,1]. After that we take the tangent at that point
|
||||
|
// and linearly project it out.
|
||||
|
if (0 < t1 && t1 < 1) |
||||
|
sol1 = SampleCurveY(t1); |
||||
|
|
||||
|
if (0 < t2 && t2 < 1) |
||||
|
sol2 = SampleCurveY(t2); |
||||
|
|
||||
|
range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2); |
||||
|
range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2); |
||||
|
} |
||||
|
|
||||
|
void InitSpline() |
||||
|
{ |
||||
|
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); |
||||
|
for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) |
||||
|
{ |
||||
|
spline_samples_[i] = SampleCurveX(i * delta_t); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const int kMaxNewtonIterations = 4; |
||||
|
|
||||
|
|
||||
|
public readonly double SolveCurveX(double x, double epsilon) |
||||
|
{ |
||||
|
if (x < 0 || x > 1) |
||||
|
throw new ArgumentException(); |
||||
|
|
||||
|
double t0 = 0; |
||||
|
double t1 = 0; |
||||
|
double t2 = x; |
||||
|
double x2 = 0; |
||||
|
double d2; |
||||
|
int i; |
||||
|
|
||||
|
#if DEBUG
|
||||
|
if (!monotonically_increasing_) |
||||
|
throw new InvalidOperationException(); |
||||
|
#endif
|
||||
|
|
||||
|
// Linear interpolation of spline curve for initial guess.
|
||||
|
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); |
||||
|
for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) |
||||
|
{ |
||||
|
if (x <= spline_samples_[i]) |
||||
|
{ |
||||
|
t1 = delta_t * i; |
||||
|
t0 = t1 - delta_t; |
||||
|
t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) / |
||||
|
(spline_samples_[i] - spline_samples_[i - 1]); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Perform a few iterations of Newton's method -- normally very fast.
|
||||
|
// See https://en.wikipedia.org/wiki/Newton%27s_method.
|
||||
|
double newton_epsilon = Math.Min(kBezierEpsilon, epsilon); |
||||
|
for (i = 0; i < kMaxNewtonIterations; i++) |
||||
|
{ |
||||
|
x2 = SampleCurveX(t2) - x; |
||||
|
if (Math.Abs(x2) < newton_epsilon) |
||||
|
return t2; |
||||
|
d2 = SampleCurveDerivativeX(t2); |
||||
|
if (Math.Abs(d2) < kBezierEpsilon) |
||||
|
break; |
||||
|
t2 = t2 - x2 / d2; |
||||
|
} |
||||
|
|
||||
|
if (Math.Abs(x2) < epsilon) |
||||
|
return t2; |
||||
|
|
||||
|
// Fall back to the bisection method for reliability.
|
||||
|
while (t0 < t1) |
||||
|
{ |
||||
|
x2 = SampleCurveX(t2); |
||||
|
if (Math.Abs(x2 - x) < epsilon) |
||||
|
return t2; |
||||
|
if (x > x2) |
||||
|
t0 = t2; |
||||
|
else |
||||
|
t1 = t2; |
||||
|
t2 = (t1 + t0) * .5; |
||||
|
} |
||||
|
|
||||
|
// Failure.
|
||||
|
return t2; |
||||
|
} |
||||
|
|
||||
|
public readonly double Solve(double x) |
||||
|
{ |
||||
|
return SolveWithEpsilon(x, kBezierEpsilon); |
||||
|
} |
||||
|
|
||||
|
public readonly double SlopeWithEpsilon(double x, double epsilon) |
||||
|
{ |
||||
|
x = MathUtilities.Clamp(x, 0.0, 1.0); |
||||
|
double t = SolveCurveX(x, epsilon); |
||||
|
double dx = SampleCurveDerivativeX(t); |
||||
|
double dy = SampleCurveDerivativeY(t); |
||||
|
return dy / dx; |
||||
|
} |
||||
|
|
||||
|
public readonly double Slope(double x) |
||||
|
{ |
||||
|
return SlopeWithEpsilon(x, kBezierEpsilon); |
||||
|
} |
||||
|
|
||||
|
public readonly double RangeMin => range_min_; |
||||
|
public readonly double RangeMax => range_max_; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Animation.Easings; |
||||
|
|
||||
|
public class CubicBezierEasing : IEasing |
||||
|
{ |
||||
|
private CubicBezier _bezier; |
||||
|
//cubic-bezier(0.25, 0.1, 0.25, 1.0)
|
||||
|
internal CubicBezierEasing(Point controlPoint1, Point controlPoint2) |
||||
|
{ |
||||
|
ControlPoint1 = controlPoint1; |
||||
|
ControlPoint2 = controlPoint2; |
||||
|
if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) |
||||
|
throw new ArgumentException(); |
||||
|
_bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); |
||||
|
} |
||||
|
|
||||
|
public Point ControlPoint2 { get; set; } |
||||
|
public Point ControlPoint1 { get; set; } |
||||
|
|
||||
|
internal static IEasing Ease { get; } = new CubicBezierEasing(new Point(0.25, 0.1), new Point(0.25, 1)); |
||||
|
|
||||
|
double IEasing.Ease(double progress) |
||||
|
{ |
||||
|
return _bezier.Solve(progress); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Internal interface for listening to changes in <see cref="Classes"/> in a more
|
||||
|
/// performant manner than subscribing to CollectionChanged.
|
||||
|
/// </summary>
|
||||
|
internal interface IClassesChangedListener |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Notifies the listener that the <see cref="Classes"/> collection has changed.
|
||||
|
/// </summary>
|
||||
|
void Changed(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
namespace Avalonia.Input |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a pen/stylus device.
|
||||
|
/// </summary>
|
||||
|
public interface IPenDevice : IPointerDevice |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,157 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Input |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a pen/stylus device.
|
||||
|
/// </summary>
|
||||
|
public class PenDevice : IPenDevice, IDisposable |
||||
|
{ |
||||
|
private readonly Dictionary<long, Pointer> _pointers = 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); |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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(); |
||||
|
} |
||||
|
|
||||
|
public IPointer? TryGetPointer(RawPointerEventArgs ev) |
||||
|
{ |
||||
|
return _pointers.TryGetValue(ev.RawPointerId, out var pointer) |
||||
|
? pointer |
||||
|
: null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,15 +1,23 @@ |
|||||
|
using System; |
||||
|
|
||||
namespace Avalonia.Input.Raw |
namespace Avalonia.Input.Raw |
||||
{ |
{ |
||||
public class RawTouchEventArgs : RawPointerEventArgs |
public class RawTouchEventArgs : RawPointerEventArgs |
||||
{ |
{ |
||||
public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, |
public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, |
||||
RawPointerEventType type, Point position, RawInputModifiers inputModifiers, |
RawPointerEventType type, Point position, RawInputModifiers inputModifiers, |
||||
long touchPointId) |
long rawPointerId) |
||||
: base(device, timestamp, root, type, position, inputModifiers) |
: base(device, timestamp, root, type, position, inputModifiers) |
||||
{ |
{ |
||||
TouchPointId = touchPointId; |
RawPointerId = rawPointerId; |
||||
} |
} |
||||
|
|
||||
public long TouchPointId { get; set; } |
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; |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue