257 changed files with 26458 additions and 2029 deletions
@ -1,6 +1,6 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="SkiaSharp" Version="1.68.2" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" /> |
|||
<PackageReference Include="SkiaSharp" Version="1.68.2.1" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2.1" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -0,0 +1,89 @@ |
|||
#include "common.h" |
|||
|
|||
extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop) |
|||
{ |
|||
int effects = 0; |
|||
if((nsop & NSDragOperationCopy) != 0) |
|||
effects |= (int)AvnDragDropEffects::Copy; |
|||
if((nsop & NSDragOperationMove) != 0) |
|||
effects |= (int)AvnDragDropEffects::Move; |
|||
if((nsop & NSDragOperationLink) != 0) |
|||
effects |= (int)AvnDragDropEffects::Link; |
|||
return (AvnDragDropEffects)effects; |
|||
}; |
|||
|
|||
extern NSString* GetAvnCustomDataType() |
|||
{ |
|||
char buffer[256]; |
|||
sprintf(buffer, "net.avaloniaui.inproc.uti.n%in", getpid()); |
|||
return [NSString stringWithUTF8String:buffer]; |
|||
} |
|||
|
|||
@interface AvnDndSource : NSObject<NSDraggingSource> |
|||
|
|||
@end |
|||
|
|||
@implementation AvnDndSource |
|||
{ |
|||
NSDragOperation _operation; |
|||
ComPtr<IAvnDndResultCallback> _cb; |
|||
void* _sourceHandle; |
|||
}; |
|||
|
|||
- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context |
|||
{ |
|||
return NSDragOperationCopy; |
|||
} |
|||
|
|||
- (AvnDndSource*) initWithOperation: (NSDragOperation)operation |
|||
andCallback: (IAvnDndResultCallback*) cb |
|||
andSourceHandle: (void*) handle |
|||
{ |
|||
self = [super init]; |
|||
_operation = operation; |
|||
_cb = cb; |
|||
_sourceHandle = handle; |
|||
return self; |
|||
} |
|||
|
|||
- (void)draggingSession:(NSDraggingSession *)session |
|||
endedAtPoint:(NSPoint)screenPoint |
|||
operation:(NSDragOperation)operation |
|||
{ |
|||
if(_cb != nil) |
|||
{ |
|||
auto cb = _cb; |
|||
_cb = nil; |
|||
cb->OnDragAndDropComplete(ConvertDragDropEffects(operation)); |
|||
} |
|||
if(_sourceHandle != nil) |
|||
{ |
|||
FreeAvnGCHandle(_sourceHandle); |
|||
_sourceHandle = nil; |
|||
} |
|||
} |
|||
|
|||
- (void*) gcHandle |
|||
{ |
|||
return _sourceHandle; |
|||
} |
|||
|
|||
@end |
|||
|
|||
extern NSObject<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle) |
|||
{ |
|||
return [[AvnDndSource alloc] initWithOperation:op andCallback:cb andSourceHandle:handle]; |
|||
}; |
|||
|
|||
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject<NSDraggingInfo>* info) |
|||
{ |
|||
id obj = [info draggingSource]; |
|||
if(obj == nil) |
|||
return nil; |
|||
if([obj isKindOfClass: [AvnDndSource class]]) |
|||
{ |
|||
auto src = (AvnDndSource*)obj; |
|||
return [src gcHandle]; |
|||
} |
|||
return nil; |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.OpenGlPage" |
|||
xmlns:pages="clr-namespace:ControlCatalog.Pages"> |
|||
<Grid> |
|||
<pages:OpenGlPageControl x:Name="GL"/> |
|||
<StackPanel> |
|||
<TextBlock Text="{Binding #GL.Info}"/> |
|||
</StackPanel> |
|||
<Grid ColumnDefinitions="*,Auto" Margin="20"> |
|||
<StackPanel Grid.Column="1" MinWidth="300"> |
|||
<TextBlock>Yaw</TextBlock> |
|||
<Slider Value="{Binding Yaw, Mode=TwoWay, ElementName=GL}" Maximum="10"/> |
|||
<TextBlock>Pitch</TextBlock> |
|||
<Slider Value="{Binding Pitch, Mode=TwoWay, ElementName=GL}" Maximum="10"/> |
|||
<TextBlock>Roll</TextBlock> |
|||
<Slider Value="{Binding Roll, Mode=TwoWay, ElementName=GL}" Maximum="10"/> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<TextBlock FontWeight="Bold" Foreground="#C000C0">D</TextBlock> |
|||
<TextBlock FontWeight="Bold" Foreground="#00C090">I</TextBlock> |
|||
<TextBlock FontWeight="Bold" Foreground="#90C000">S</TextBlock> |
|||
<TextBlock FontWeight="Bold" Foreground="#C09000">C</TextBlock> |
|||
<TextBlock FontWeight="Bold" Foreground="#00C090">O</TextBlock> |
|||
</StackPanel> |
|||
<Slider Value="{Binding Disco, Mode=TwoWay, ElementName=GL}" Maximum="1"/> |
|||
</StackPanel> |
|||
</Grid> |
|||
</Grid> |
|||
</UserControl> |
|||
@ -0,0 +1,401 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.OpenGL; |
|||
using Avalonia.Platform.Interop; |
|||
using Avalonia.Threading; |
|||
using static Avalonia.OpenGL.GlConsts; |
|||
// ReSharper disable StringLiteralTypo
|
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class OpenGlPage : UserControl |
|||
{ |
|||
|
|||
} |
|||
|
|||
public class OpenGlPageControl : OpenGlControlBase |
|||
{ |
|||
private float _yaw; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, float> YawProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Yaw", o => o.Yaw, (o, v) => o.Yaw = v); |
|||
|
|||
public float Yaw |
|||
{ |
|||
get => _yaw; |
|||
set => SetAndRaise(YawProperty, ref _yaw, value); |
|||
} |
|||
|
|||
private float _pitch; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, float> PitchProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Pitch", o => o.Pitch, (o, v) => o.Pitch = v); |
|||
|
|||
public float Pitch |
|||
{ |
|||
get => _pitch; |
|||
set => SetAndRaise(PitchProperty, ref _pitch, value); |
|||
} |
|||
|
|||
|
|||
private float _roll; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, float> RollProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Roll", o => o.Roll, (o, v) => o.Roll = v); |
|||
|
|||
public float Roll |
|||
{ |
|||
get => _roll; |
|||
set => SetAndRaise(RollProperty, ref _roll, value); |
|||
} |
|||
|
|||
|
|||
private float _disco; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, float> DiscoProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Disco", o => o.Disco, (o, v) => o.Disco = v); |
|||
|
|||
public float Disco |
|||
{ |
|||
get => _disco; |
|||
set => SetAndRaise(DiscoProperty, ref _disco, value); |
|||
} |
|||
|
|||
private string _info; |
|||
|
|||
public static readonly DirectProperty<OpenGlPageControl, string> InfoProperty = |
|||
AvaloniaProperty.RegisterDirect<OpenGlPageControl, string>("Info", o => o.Info, (o, v) => o.Info = v); |
|||
|
|||
public string Info |
|||
{ |
|||
get => _info; |
|||
private set => SetAndRaise(InfoProperty, ref _info, value); |
|||
} |
|||
|
|||
static OpenGlPageControl() |
|||
{ |
|||
AffectsRender<OpenGlPageControl>(YawProperty, PitchProperty, RollProperty, DiscoProperty); |
|||
} |
|||
|
|||
private int _vertexShader; |
|||
private int _fragmentShader; |
|||
private int _shaderProgram; |
|||
private int _vertexBufferObject; |
|||
private int _indexBufferObject; |
|||
private int _vertexArrayObject; |
|||
private GlExtrasInterface _glExt; |
|||
|
|||
private string GetShader(bool fragment, string shader) |
|||
{ |
|||
var version = (GlVersion.Type == GlProfileType.OpenGL ? |
|||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 150 : 120 : |
|||
100); |
|||
var data = "#version " + version + "\n"; |
|||
if (GlVersion.Type == GlProfileType.OpenGLES) |
|||
data += "precision mediump float;\n"; |
|||
if (version >= 150) |
|||
{ |
|||
shader = shader.Replace("attribute", "in"); |
|||
if (fragment) |
|||
shader = shader |
|||
.Replace("varying", "in") |
|||
.Replace("//DECLAREGLFRAG", "out vec4 outFragColor;") |
|||
.Replace("gl_FragColor", "outFragColor"); |
|||
else |
|||
shader = shader.Replace("varying", "out"); |
|||
} |
|||
|
|||
data += shader; |
|||
|
|||
return data; |
|||
} |
|||
|
|||
|
|||
private string VertexShaderSource => GetShader(false, @"
|
|||
attribute vec3 aPos; |
|||
attribute vec3 aNormal; |
|||
uniform mat4 uModel; |
|||
uniform mat4 uProjection; |
|||
uniform mat4 uView; |
|||
|
|||
varying vec3 FragPos; |
|||
varying vec3 VecPos; |
|||
varying vec3 Normal; |
|||
uniform float uTime; |
|||
uniform float uDisco; |
|||
void main() |
|||
{ |
|||
float discoScale = sin(uTime * 10.0) / 10.0; |
|||
float distortionX = 1.0 + uDisco * cos(uTime * 20.0) / 10.0; |
|||
|
|||
float scale = 1.0 + uDisco * discoScale; |
|||
|
|||
vec3 scaledPos = aPos; |
|||
scaledPos.x = scaledPos.x * distortionX; |
|||
|
|||
scaledPos *= scale; |
|||
gl_Position = uProjection * uView * uModel * vec4(scaledPos, 1.0); |
|||
FragPos = vec3(uModel * vec4(aPos, 1.0)); |
|||
VecPos = aPos; |
|||
Normal = normalize(vec3(uModel * vec4(aNormal, 1.0))); |
|||
} |
|||
");
|
|||
|
|||
private string FragmentShaderSource => GetShader(true, @"
|
|||
varying vec3 FragPos; |
|||
varying vec3 VecPos; |
|||
varying vec3 Normal; |
|||
uniform float uMaxY; |
|||
uniform float uMinY; |
|||
uniform float uTime; |
|||
uniform float uDisco; |
|||
//DECLAREGLFRAG
|
|||
|
|||
void main() |
|||
{ |
|||
float y = (VecPos.y - uMinY) / (uMaxY - uMinY); |
|||
float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + uTime * 40.0 + y * 50.0); |
|||
float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - uTime * 20.0 - y * 30.0); |
|||
|
|||
vec3 discoColor = vec3( |
|||
0.5 + abs(0.5 - y) * cos(uTime * 10.0), |
|||
0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)), |
|||
0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0)))); |
|||
|
|||
vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25); |
|||
objectColor = objectColor * (1.0 - uDisco) + discoColor * uDisco; |
|||
|
|||
float ambientStrength = 0.3; |
|||
vec3 lightColor = vec3(1.0, 1.0, 1.0); |
|||
vec3 lightPos = vec3(uMaxY * 2.0, uMaxY * 2.0, uMaxY * 2.0); |
|||
vec3 ambient = ambientStrength * lightColor; |
|||
|
|||
|
|||
vec3 norm = normalize(Normal); |
|||
vec3 lightDir = normalize(lightPos - FragPos); |
|||
|
|||
float diff = max(dot(norm, lightDir), 0.0); |
|||
vec3 diffuse = diff * lightColor; |
|||
|
|||
vec3 result = (ambient + diffuse) * objectColor; |
|||
gl_FragColor = vec4(result, 1.0); |
|||
|
|||
} |
|||
");
|
|||
|
|||
[StructLayout(LayoutKind.Sequential, Pack = 4)] |
|||
private struct Vertex |
|||
{ |
|||
public Vector3 Position; |
|||
public Vector3 Normal; |
|||
} |
|||
|
|||
private readonly Vertex[] _points; |
|||
private readonly ushort[] _indices; |
|||
private readonly float _minY; |
|||
private readonly float _maxY; |
|||
|
|||
|
|||
public OpenGlPageControl() |
|||
{ |
|||
var name = typeof(OpenGlPage).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin")); |
|||
using (var sr = new BinaryReader(typeof(OpenGlPage).Assembly.GetManifestResourceStream(name))) |
|||
{ |
|||
var buf = new byte[sr.ReadInt32()]; |
|||
sr.Read(buf, 0, buf.Length); |
|||
var points = new float[buf.Length / 4]; |
|||
Buffer.BlockCopy(buf, 0, points, 0, buf.Length); |
|||
buf = new byte[sr.ReadInt32()]; |
|||
sr.Read(buf, 0, buf.Length); |
|||
_indices = new ushort[buf.Length / 2]; |
|||
Buffer.BlockCopy(buf, 0, _indices, 0, buf.Length); |
|||
_points = new Vertex[points.Length / 3]; |
|||
for (var primitive = 0; primitive < points.Length / 3; primitive++) |
|||
{ |
|||
var srci = primitive * 3; |
|||
_points[primitive] = new Vertex |
|||
{ |
|||
Position = new Vector3(points[srci], points[srci + 1], points[srci + 2]) |
|||
}; |
|||
} |
|||
|
|||
for (int i = 0; i < _indices.Length; i += 3) |
|||
{ |
|||
Vector3 a = _points[_indices[i]].Position; |
|||
Vector3 b = _points[_indices[i + 1]].Position; |
|||
Vector3 c = _points[_indices[i + 2]].Position; |
|||
var normal = Vector3.Normalize(Vector3.Cross(c - b, a - b)); |
|||
|
|||
_points[_indices[i]].Normal += normal; |
|||
_points[_indices[i + 1]].Normal += normal; |
|||
_points[_indices[i + 2]].Normal += normal; |
|||
} |
|||
|
|||
for (int i = 0; i < _points.Length; i++) |
|||
{ |
|||
_points[i].Normal = Vector3.Normalize(_points[i].Normal); |
|||
_maxY = Math.Max(_maxY, _points[i].Position.Y); |
|||
_minY = Math.Min(_minY, _points[i].Position.Y); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
private void CheckError(GlInterface gl) |
|||
{ |
|||
int err; |
|||
while ((err = gl.GetError()) != GL_NO_ERROR) |
|||
Console.WriteLine(err); |
|||
} |
|||
|
|||
protected unsafe override void OnOpenGlInit(GlInterface GL, int fb) |
|||
{ |
|||
CheckError(GL); |
|||
_glExt = new GlExtrasInterface(GL); |
|||
|
|||
Info = $"Renderer: {GL.GetString(GL_RENDERER)} Version: {GL.GetString(GL_VERSION)}"; |
|||
|
|||
// Load the source of the vertex shader and compile it.
|
|||
_vertexShader = GL.CreateShader(GL_VERTEX_SHADER); |
|||
Console.WriteLine(GL.CompileShaderAndGetError(_vertexShader, VertexShaderSource)); |
|||
|
|||
// Load the source of the fragment shader and compile it.
|
|||
_fragmentShader = GL.CreateShader(GL_FRAGMENT_SHADER); |
|||
Console.WriteLine(GL.CompileShaderAndGetError(_fragmentShader, FragmentShaderSource)); |
|||
|
|||
// Create the shader program, attach the vertex and fragment shaders and link the program.
|
|||
_shaderProgram = GL.CreateProgram(); |
|||
GL.AttachShader(_shaderProgram, _vertexShader); |
|||
GL.AttachShader(_shaderProgram, _fragmentShader); |
|||
const int positionLocation = 0; |
|||
const int normalLocation = 1; |
|||
GL.BindAttribLocationString(_shaderProgram, positionLocation, "aPos"); |
|||
GL.BindAttribLocationString(_shaderProgram, normalLocation, "aNormal"); |
|||
Console.WriteLine(GL.LinkProgramAndGetError(_shaderProgram)); |
|||
CheckError(GL); |
|||
|
|||
// Create the vertex buffer object (VBO) for the vertex data.
|
|||
_vertexBufferObject = GL.GenBuffer(); |
|||
// Bind the VBO and copy the vertex data into it.
|
|||
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); |
|||
CheckError(GL); |
|||
var vertexSize = Marshal.SizeOf<Vertex>(); |
|||
fixed (void* pdata = _points) |
|||
GL.BufferData(GL_ARRAY_BUFFER, new IntPtr(_points.Length * vertexSize), |
|||
new IntPtr(pdata), GL_STATIC_DRAW); |
|||
|
|||
_indexBufferObject = GL.GenBuffer(); |
|||
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject); |
|||
CheckError(GL); |
|||
fixed (void* pdata = _indices) |
|||
GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, new IntPtr(_indices.Length * sizeof(ushort)), new IntPtr(pdata), |
|||
GL_STATIC_DRAW); |
|||
CheckError(GL); |
|||
_vertexArrayObject = _glExt.GenVertexArray(); |
|||
_glExt.BindVertexArray(_vertexArrayObject); |
|||
CheckError(GL); |
|||
GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT, |
|||
0, vertexSize, IntPtr.Zero); |
|||
GL.VertexAttribPointer(normalLocation, 3, GL_FLOAT, |
|||
0, vertexSize, new IntPtr(12)); |
|||
GL.EnableVertexAttribArray(positionLocation); |
|||
GL.EnableVertexAttribArray(normalLocation); |
|||
CheckError(GL); |
|||
|
|||
} |
|||
|
|||
protected override void OnOpenGlDeinit(GlInterface GL, int fb) |
|||
{ |
|||
// Unbind everything
|
|||
GL.BindBuffer(GL_ARRAY_BUFFER, 0); |
|||
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); |
|||
_glExt.BindVertexArray(0); |
|||
GL.UseProgram(0); |
|||
|
|||
// Delete all resources.
|
|||
GL.DeleteBuffers(2, new[] { _vertexBufferObject, _indexBufferObject }); |
|||
_glExt.DeleteVertexArrays(1, new[] { _vertexArrayObject }); |
|||
GL.DeleteProgram(_shaderProgram); |
|||
GL.DeleteShader(_fragmentShader); |
|||
GL.DeleteShader(_vertexShader); |
|||
} |
|||
|
|||
static Stopwatch St = Stopwatch.StartNew(); |
|||
protected override unsafe void OnOpenGlRender(GlInterface gl, int fb) |
|||
{ |
|||
gl.ClearColor(0, 0, 0, 0); |
|||
gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); |
|||
gl.Enable(GL_DEPTH_TEST); |
|||
gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); |
|||
var GL = gl; |
|||
|
|||
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); |
|||
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject); |
|||
_glExt.BindVertexArray(_vertexArrayObject); |
|||
GL.UseProgram(_shaderProgram); |
|||
CheckError(GL); |
|||
var projection = |
|||
Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)(Bounds.Width / Bounds.Height), |
|||
0.01f, 1000); |
|||
|
|||
|
|||
var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0)); |
|||
var model = Matrix4x4.CreateFromYawPitchRoll(_yaw, _pitch, _roll); |
|||
var modelLoc = GL.GetUniformLocationString(_shaderProgram, "uModel"); |
|||
var viewLoc = GL.GetUniformLocationString(_shaderProgram, "uView"); |
|||
var projectionLoc = GL.GetUniformLocationString(_shaderProgram, "uProjection"); |
|||
var maxYLoc = GL.GetUniformLocationString(_shaderProgram, "uMaxY"); |
|||
var minYLoc = GL.GetUniformLocationString(_shaderProgram, "uMinY"); |
|||
var timeLoc = GL.GetUniformLocationString(_shaderProgram, "uTime"); |
|||
var discoLoc = GL.GetUniformLocationString(_shaderProgram, "uDisco"); |
|||
GL.UniformMatrix4fv(modelLoc, 1, false, &model); |
|||
GL.UniformMatrix4fv(viewLoc, 1, false, &view); |
|||
GL.UniformMatrix4fv(projectionLoc, 1, false, &projection); |
|||
GL.Uniform1f(maxYLoc, _maxY); |
|||
GL.Uniform1f(minYLoc, _minY); |
|||
GL.Uniform1f(timeLoc, (float)St.Elapsed.TotalSeconds); |
|||
GL.Uniform1f(discoLoc, _disco); |
|||
CheckError(GL); |
|||
GL.DrawElements(GL_TRIANGLES, _indices.Length, GL_UNSIGNED_SHORT, IntPtr.Zero); |
|||
|
|||
CheckError(GL); |
|||
if (_disco > 0.01) |
|||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); |
|||
} |
|||
|
|||
class GlExtrasInterface : GlInterfaceBase<GlInterface.GlContextInfo> |
|||
{ |
|||
public GlExtrasInterface(GlInterface gl) : base(gl.GetProcAddress, gl.ContextInfo) |
|||
{ |
|||
} |
|||
|
|||
public delegate void GlDeleteVertexArrays(int count, int[] buffers); |
|||
[GlMinVersionEntryPoint("glDeleteVertexArrays", 3,0)] |
|||
[GlExtensionEntryPoint("glDeleteVertexArraysOES", "GL_OES_vertex_array_object")] |
|||
public GlDeleteVertexArrays DeleteVertexArrays { get; } |
|||
|
|||
public delegate void GlBindVertexArray(int array); |
|||
[GlMinVersionEntryPoint("glBindVertexArray", 3,0)] |
|||
[GlExtensionEntryPoint("glBindVertexArrayOES", "GL_OES_vertex_array_object")] |
|||
public GlBindVertexArray BindVertexArray { get; } |
|||
public delegate void GlGenVertexArrays(int n, int[] rv); |
|||
|
|||
[GlMinVersionEntryPoint("glGenVertexArrays",3,0)] |
|||
[GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")] |
|||
public GlGenVertexArrays GenVertexArrays { get; } |
|||
|
|||
public int GenVertexArray() |
|||
{ |
|||
var rv = new int[1]; |
|||
GenVertexArrays(1, rv); |
|||
return rv[0]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,115 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.ObjectModel; |
|||
using System.Linq; |
|||
using System.Reactive; |
|||
using Avalonia.Controls; |
|||
using ReactiveUI; |
|||
|
|||
namespace ControlCatalog.ViewModels |
|||
{ |
|||
public class TreeViewPageViewModel : ReactiveObject |
|||
{ |
|||
private readonly Node _root; |
|||
private SelectionMode _selectionMode; |
|||
|
|||
public TreeViewPageViewModel() |
|||
{ |
|||
_root = new Node(); |
|||
|
|||
Items = _root.Children; |
|||
Selection = new SelectionModel(); |
|||
Selection.SelectionChanged += SelectionChanged; |
|||
|
|||
AddItemCommand = ReactiveCommand.Create(AddItem); |
|||
RemoveItemCommand = ReactiveCommand.Create(RemoveItem); |
|||
} |
|||
|
|||
public ObservableCollection<Node> Items { get; } |
|||
public SelectionModel Selection { get; } |
|||
public ReactiveCommand<Unit, Unit> AddItemCommand { get; } |
|||
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; } |
|||
|
|||
public SelectionMode SelectionMode |
|||
{ |
|||
get => _selectionMode; |
|||
set |
|||
{ |
|||
Selection.ClearSelection(); |
|||
this.RaiseAndSetIfChanged(ref _selectionMode, value); |
|||
} |
|||
} |
|||
|
|||
private void AddItem() |
|||
{ |
|||
var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root; |
|||
parentItem.AddItem(); |
|||
} |
|||
|
|||
private void RemoveItem() |
|||
{ |
|||
while (Selection.SelectedItems.Count > 0) |
|||
{ |
|||
Node lastItem = (Node)Selection.SelectedItems[0]; |
|||
RecursiveRemove(Items, lastItem); |
|||
Selection.DeselectAt(Selection.SelectedIndices[0]); |
|||
} |
|||
|
|||
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem) |
|||
{ |
|||
if (items.Remove(selectedItem)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
foreach (Node item in items) |
|||
{ |
|||
if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
|
|||
private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) |
|||
{ |
|||
var selected = string.Join(",", e.SelectedIndices); |
|||
var deselected = string.Join(",", e.DeselectedIndices); |
|||
System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'"); |
|||
} |
|||
|
|||
public class Node |
|||
{ |
|||
private ObservableCollection<Node> _children; |
|||
private int _childIndex = 10; |
|||
|
|||
public Node() |
|||
{ |
|||
Header = "Item"; |
|||
} |
|||
|
|||
public Node(Node parent, int index) |
|||
{ |
|||
Parent = parent; |
|||
Header = parent.Header + ' ' + index; |
|||
} |
|||
|
|||
public Node Parent { get; } |
|||
public string Header { get; } |
|||
public bool AreChildrenInitialized => _children != null; |
|||
public ObservableCollection<Node> Children => _children ??= CreateChildren(); |
|||
public void AddItem() => Children.Add(new Node(this, _childIndex++)); |
|||
public void RemoveItem(Node child) => Children.Remove(child); |
|||
public override string ToString() => Header; |
|||
|
|||
private ObservableCollection<Node> CreateChildren() |
|||
{ |
|||
return new ObservableCollection<Node>( |
|||
Enumerable.Range(0, 10).Select(i => new Node(this, i))); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
using System; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Media; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace RenderDemo.Controls |
|||
{ |
|||
public class LineBoundsDemoControl : Control |
|||
{ |
|||
static LineBoundsDemoControl() |
|||
{ |
|||
AffectsRender<LineBoundsDemoControl>(AngleProperty); |
|||
} |
|||
|
|||
public LineBoundsDemoControl() |
|||
{ |
|||
var timer = new DispatcherTimer(); |
|||
timer.Interval = TimeSpan.FromSeconds(1 / 60.0); |
|||
timer.Tick += (sender, e) => Angle += Math.PI / 360; |
|||
timer.Start(); |
|||
} |
|||
|
|||
public static readonly StyledProperty<double> AngleProperty = |
|||
AvaloniaProperty.Register<LineBoundsDemoControl, double>(nameof(Angle)); |
|||
|
|||
public double Angle |
|||
{ |
|||
get => GetValue(AngleProperty); |
|||
set => SetValue(AngleProperty, value); |
|||
} |
|||
|
|||
public override void Render(DrawingContext drawingContext) |
|||
{ |
|||
var lineLength = Math.Sqrt((100 * 100) + (100 * 100)); |
|||
|
|||
var diffX = LineBoundsHelper.CalculateAdjSide(Angle, lineLength); |
|||
var diffY = LineBoundsHelper.CalculateOppSide(Angle, lineLength); |
|||
|
|||
|
|||
var p1 = new Point(200, 200); |
|||
var p2 = new Point(p1.X + diffX, p1.Y + diffY); |
|||
|
|||
var pen = new Pen(Brushes.Green, 20, lineCap: PenLineCap.Square); |
|||
var boundPen = new Pen(Brushes.Black); |
|||
|
|||
drawingContext.DrawLine(pen, p1, p2); |
|||
|
|||
drawingContext.DrawRectangle(boundPen, LineBoundsHelper.CalculateBounds(p1, p2, pen)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
xmlns:controls="clr-namespace:RenderDemo.Controls" |
|||
x:Class="RenderDemo.Pages.LineBoundsPage"> |
|||
<controls:LineBoundsDemoControl /> |
|||
</UserControl> |
|||
@ -0,0 +1,19 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace RenderDemo.Pages |
|||
{ |
|||
public class LineBoundsPage : UserControl |
|||
{ |
|||
public LineBoundsPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -1,5 +1,5 @@ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ |
|||
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using System.Threading; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
public class DisposableLock |
|||
{ |
|||
private readonly object _lock = new object(); |
|||
|
|||
/// <summary>
|
|||
/// Tries to take a lock
|
|||
/// </summary>
|
|||
/// <returns>IDisposable if succeeded to obtain the lock</returns>
|
|||
public IDisposable TryLock() |
|||
{ |
|||
if (Monitor.TryEnter(_lock)) |
|||
return new UnlockDisposable(_lock); |
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Enters a waiting lock
|
|||
/// </summary>
|
|||
public IDisposable Lock() |
|||
{ |
|||
Monitor.Enter(_lock); |
|||
return new UnlockDisposable(_lock); |
|||
} |
|||
|
|||
private sealed class UnlockDisposable : IDisposable |
|||
{ |
|||
private object _lock; |
|||
|
|||
public UnlockDisposable(object @lock) |
|||
{ |
|||
_lock = @lock; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
object @lock = Interlocked.Exchange(ref _lock, null); |
|||
|
|||
if (@lock != null) |
|||
{ |
|||
Monitor.Exit(@lock); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,249 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Holds the selected items for a control.
|
|||
/// </summary>
|
|||
public interface ISelectionModel : INotifyPropertyChanged |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the anchor index.
|
|||
/// </summary>
|
|||
IndexPath AnchorIndex { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or set the index of the first selected item.
|
|||
/// </summary>
|
|||
IndexPath SelectedIndex { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or set the indexes of the selected items.
|
|||
/// </summary>
|
|||
IReadOnlyList<IndexPath> SelectedIndices { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the first selected item.
|
|||
/// </summary>
|
|||
object SelectedItem { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the selected items.
|
|||
/// </summary>
|
|||
IReadOnlyList<object> SelectedItems { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the model represents a single or multiple selection.
|
|||
/// </summary>
|
|||
bool SingleSelect { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether to always keep an item selected where possible.
|
|||
/// </summary>
|
|||
bool AutoSelect { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the collection that contains the items that can be selected.
|
|||
/// </summary>
|
|||
object Source { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Raised when the children of a selection are required.
|
|||
/// </summary>
|
|||
event EventHandler<SelectionModelChildrenRequestedEventArgs> ChildrenRequested; |
|||
|
|||
/// <summary>
|
|||
/// Raised when the selection has changed.
|
|||
/// </summary>
|
|||
event EventHandler<SelectionModelSelectionChangedEventArgs> SelectionChanged; |
|||
|
|||
/// <summary>
|
|||
/// Clears the selection.
|
|||
/// </summary>
|
|||
void ClearSelection(); |
|||
|
|||
/// <summary>
|
|||
/// Deselects an item.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item.</param>
|
|||
void Deselect(int index); |
|||
|
|||
/// <summary>
|
|||
/// Deselects an item.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="itemIndex">The index of the item in the group.</param>
|
|||
void Deselect(int groupIndex, int itemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Deselects an item.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item.</param>
|
|||
void DeselectAt(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Deselects a range of items.
|
|||
/// </summary>
|
|||
/// <param name="start">The start index of the range.</param>
|
|||
/// <param name="end">The end index of the range.</param>
|
|||
void DeselectRange(IndexPath start, IndexPath end); |
|||
|
|||
/// <summary>
|
|||
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The end index of the range.</param>
|
|||
void DeselectRangeFromAnchor(int index); |
|||
|
|||
/// <summary>
|
|||
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="endGroupIndex">
|
|||
/// The index of the item group that represents the end of the selection.
|
|||
/// </param>
|
|||
/// <param name="endItemIndex">
|
|||
/// The index of the item in the group that represents the end of the selection.
|
|||
/// </param>
|
|||
void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The end index of the range.</param>
|
|||
void DeselectRangeFromAnchorTo(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Disposes the object and clears the selection.
|
|||
/// </summary>
|
|||
void Dispose(); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item is selected.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
bool IsSelected(int index); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item is selected.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="itemIndex">The index of the item in the group.</param>
|
|||
bool IsSelected(int groupIndex, int itemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item is selected.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
public bool IsSelectedAt(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item or its descendents are selected.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
/// <returns>
|
|||
/// True if the item and all its descendents are selected, false if the item and all its
|
|||
/// descendents are deselected, or null if a combination of selected and deselected.
|
|||
/// </returns>
|
|||
bool? IsSelectedWithPartial(int index); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item or its descendents are selected.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="itemIndex">The index of the item in the group.</param>
|
|||
/// <returns>
|
|||
/// True if the item and all its descendents are selected, false if the item and all its
|
|||
/// descendents are deselected, or null if a combination of selected and deselected.
|
|||
/// </returns>
|
|||
bool? IsSelectedWithPartial(int groupIndex, int itemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Checks whether an item or its descendents are selected.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
/// <returns>
|
|||
/// True if the item and all its descendents are selected, false if the item and all its
|
|||
/// descendents are deselected, or null if a combination of selected and deselected.
|
|||
/// </returns>
|
|||
bool? IsSelectedWithPartialAt(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Selects an item.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
void Select(int index); |
|||
|
|||
/// <summary>
|
|||
/// Selects an item.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="itemIndex">The index of the item in the group.</param>
|
|||
void Select(int groupIndex, int itemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Selects an item.
|
|||
/// </summary>
|
|||
/// <param name="index">The index of the item</param>
|
|||
void SelectAt(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Selects all items.
|
|||
/// </summary>
|
|||
void SelectAll(); |
|||
|
|||
/// <summary>
|
|||
/// Selects a range of items.
|
|||
/// </summary>
|
|||
/// <param name="start">The start index of the range.</param>
|
|||
/// <param name="end">The end index of the range.</param>
|
|||
void SelectRange(IndexPath start, IndexPath end); |
|||
|
|||
/// <summary>
|
|||
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The end index of the range.</param>
|
|||
void SelectRangeFromAnchor(int index); |
|||
|
|||
/// <summary>
|
|||
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="endGroupIndex">
|
|||
/// The index of the item group that represents the end of the selection.
|
|||
/// </param>
|
|||
/// <param name="endItemIndex">
|
|||
/// The index of the item in the group that represents the end of the selection.
|
|||
/// </param>
|
|||
void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex); |
|||
|
|||
/// <summary>
|
|||
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The end index of the range.</param>
|
|||
void SelectRangeFromAnchorTo(IndexPath index); |
|||
|
|||
/// <summary>
|
|||
/// Sets the <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="index">The anchor index.</param>
|
|||
void SetAnchorIndex(int index); |
|||
|
|||
/// <summary>
|
|||
/// Sets the <see cref="AnchorIndex"/>.
|
|||
/// </summary>
|
|||
/// <param name="groupIndex">The index of the item group.</param>
|
|||
/// <param name="index">The index of the item in the group.</param>
|
|||
void SetAnchorIndex(int groupIndex, int index); |
|||
|
|||
/// <summary>
|
|||
/// Begins a batch update of the selection.
|
|||
/// </summary>
|
|||
/// <returns>An <see cref="IDisposable"/> that finishes the batch update.</returns>
|
|||
IDisposable Update(); |
|||
} |
|||
} |
|||
@ -0,0 +1,180 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public readonly struct IndexPath : IComparable<IndexPath>, IEquatable<IndexPath> |
|||
{ |
|||
public static readonly IndexPath Unselected = default; |
|||
|
|||
private readonly int _index; |
|||
private readonly int[]? _path; |
|||
|
|||
public IndexPath(int index) |
|||
{ |
|||
_index = index + 1; |
|||
_path = null; |
|||
} |
|||
|
|||
public IndexPath(int groupIndex, int itemIndex) |
|||
{ |
|||
_index = 0; |
|||
_path = new[] { groupIndex, itemIndex }; |
|||
} |
|||
|
|||
public IndexPath(IEnumerable<int>? indices) |
|||
{ |
|||
if (indices != null) |
|||
{ |
|||
_index = 0; |
|||
_path = indices.ToArray(); |
|||
} |
|||
else |
|||
{ |
|||
_index = 0; |
|||
_path = null; |
|||
} |
|||
} |
|||
|
|||
private IndexPath(int[] basePath, int index) |
|||
{ |
|||
basePath = basePath ?? throw new ArgumentNullException(nameof(basePath)); |
|||
|
|||
_index = 0; |
|||
_path = new int[basePath.Length + 1]; |
|||
Array.Copy(basePath, _path, basePath.Length); |
|||
_path[basePath.Length] = index; |
|||
} |
|||
|
|||
public int GetSize() => _path?.Length ?? (_index == 0 ? 0 : 1); |
|||
|
|||
public int GetAt(int index) |
|||
{ |
|||
if (index >= GetSize()) |
|||
{ |
|||
throw new IndexOutOfRangeException(); |
|||
} |
|||
|
|||
return _path?[index] ?? (_index - 1); |
|||
} |
|||
|
|||
public int CompareTo(IndexPath other) |
|||
{ |
|||
var rhsPath = other; |
|||
int compareResult = 0; |
|||
int lhsCount = GetSize(); |
|||
int rhsCount = rhsPath.GetSize(); |
|||
|
|||
if (lhsCount == 0 || rhsCount == 0) |
|||
{ |
|||
// one of the paths are empty, compare based on size
|
|||
compareResult = (lhsCount - rhsCount); |
|||
} |
|||
else |
|||
{ |
|||
// both paths are non-empty, but can be of different size
|
|||
for (int i = 0; i < Math.Min(lhsCount, rhsCount); i++) |
|||
{ |
|||
if (GetAt(i) < rhsPath.GetAt(i)) |
|||
{ |
|||
compareResult = -1; |
|||
break; |
|||
} |
|||
else if (GetAt(i) > rhsPath.GetAt(i)) |
|||
{ |
|||
compareResult = 1; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
// if both match upto min(lhsCount, rhsCount), compare based on size
|
|||
compareResult = compareResult == 0 ? (lhsCount - rhsCount) : compareResult; |
|||
} |
|||
|
|||
if (compareResult != 0) |
|||
{ |
|||
compareResult = compareResult > 0 ? 1 : -1; |
|||
} |
|||
|
|||
return compareResult; |
|||
} |
|||
|
|||
public IndexPath CloneWithChildIndex(int childIndex) |
|||
{ |
|||
if (_path != null) |
|||
{ |
|||
return new IndexPath(_path, childIndex); |
|||
} |
|||
else if (_index != 0) |
|||
{ |
|||
return new IndexPath(_index - 1, childIndex); |
|||
} |
|||
else |
|||
{ |
|||
return new IndexPath(childIndex); |
|||
} |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
if (_path != null) |
|||
{ |
|||
return "R" + string.Join(".", _path); |
|||
} |
|||
else if (_index != 0) |
|||
{ |
|||
return "R" + (_index - 1); |
|||
} |
|||
else |
|||
{ |
|||
return "R"; |
|||
} |
|||
} |
|||
|
|||
public static IndexPath CreateFrom(int index) => new IndexPath(index); |
|||
|
|||
public static IndexPath CreateFrom(int groupIndex, int itemIndex) => new IndexPath(groupIndex, itemIndex); |
|||
|
|||
public static IndexPath CreateFromIndices(IList<int> indices) => new IndexPath(indices); |
|||
|
|||
public override bool Equals(object obj) => obj is IndexPath other && Equals(other); |
|||
|
|||
public bool Equals(IndexPath other) => CompareTo(other) == 0; |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
var hashCode = -504981047; |
|||
|
|||
if (_path != null) |
|||
{ |
|||
foreach (var i in _path) |
|||
{ |
|||
hashCode = hashCode * -1521134295 + i.GetHashCode(); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
hashCode = hashCode * -1521134295 + _index.GetHashCode(); |
|||
} |
|||
|
|||
return hashCode; |
|||
} |
|||
|
|||
public static bool operator <(IndexPath x, IndexPath y) { return x.CompareTo(y) < 0; } |
|||
public static bool operator >(IndexPath x, IndexPath y) { return x.CompareTo(y) > 0; } |
|||
public static bool operator <=(IndexPath x, IndexPath y) { return x.CompareTo(y) <= 0; } |
|||
public static bool operator >=(IndexPath x, IndexPath y) { return x.CompareTo(y) >= 0; } |
|||
public static bool operator ==(IndexPath x, IndexPath y) { return x.CompareTo(y) == 0; } |
|||
public static bool operator !=(IndexPath x, IndexPath y) { return x.CompareTo(y) != 0; } |
|||
public static bool operator ==(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) == 0; } |
|||
public static bool operator !=(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) != 0; } |
|||
} |
|||
} |
|||
@ -0,0 +1,232 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal readonly struct IndexRange : IEquatable<IndexRange> |
|||
{ |
|||
private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue); |
|||
|
|||
public IndexRange(int begin, int end) |
|||
{ |
|||
// Accept out of order begin/end pairs, just swap them.
|
|||
if (begin > end) |
|||
{ |
|||
int temp = begin; |
|||
begin = end; |
|||
end = temp; |
|||
} |
|||
|
|||
Begin = begin; |
|||
End = end; |
|||
} |
|||
|
|||
public int Begin { get; } |
|||
public int End { get; } |
|||
public int Count => (End - Begin) + 1; |
|||
|
|||
public bool Contains(int index) => index >= Begin && index <= End; |
|||
|
|||
public bool Split(int splitIndex, out IndexRange before, out IndexRange after) |
|||
{ |
|||
bool afterIsValid; |
|||
|
|||
before = new IndexRange(Begin, splitIndex); |
|||
|
|||
if (splitIndex < End) |
|||
{ |
|||
after = new IndexRange(splitIndex + 1, End); |
|||
afterIsValid = true; |
|||
} |
|||
else |
|||
{ |
|||
after = new IndexRange(); |
|||
afterIsValid = false; |
|||
} |
|||
|
|||
return afterIsValid; |
|||
} |
|||
|
|||
public bool Intersects(IndexRange other) |
|||
{ |
|||
return (Begin <= other.End) && (End >= other.Begin); |
|||
} |
|||
|
|||
public bool Adjacent(IndexRange other) |
|||
{ |
|||
return Begin == other.End + 1 || End == other.Begin - 1; |
|||
} |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return obj is IndexRange range && Equals(range); |
|||
} |
|||
|
|||
public bool Equals(IndexRange other) |
|||
{ |
|||
return Begin == other.Begin && End == other.End; |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
var hashCode = 1903003160; |
|||
hashCode = hashCode * -1521134295 + Begin.GetHashCode(); |
|||
hashCode = hashCode * -1521134295 + End.GetHashCode(); |
|||
return hashCode; |
|||
} |
|||
|
|||
public override string ToString() => $"[{Begin}..{End}]"; |
|||
|
|||
public static bool operator ==(IndexRange left, IndexRange right) => left.Equals(right); |
|||
public static bool operator !=(IndexRange left, IndexRange right) => !(left == right); |
|||
|
|||
public static int Add( |
|||
IList<IndexRange> ranges, |
|||
IndexRange range, |
|||
IList<IndexRange>? added = null) |
|||
{ |
|||
var result = 0; |
|||
|
|||
for (var i = 0; i < ranges.Count && range != s_invalid; ++i) |
|||
{ |
|||
var existing = ranges[i]; |
|||
|
|||
if (range.Intersects(existing) || range.Adjacent(existing)) |
|||
{ |
|||
if (range.Begin < existing.Begin) |
|||
{ |
|||
var add = new IndexRange(range.Begin, existing.Begin - 1); |
|||
ranges[i] = new IndexRange(range.Begin, existing.End); |
|||
added?.Add(add); |
|||
result += add.Count; |
|||
} |
|||
|
|||
range = range.End <= existing.End ? |
|||
s_invalid : |
|||
new IndexRange(existing.End + 1, range.End); |
|||
} |
|||
else if (range.End < existing.Begin) |
|||
{ |
|||
ranges.Insert(i, range); |
|||
added?.Add(range); |
|||
result += range.Count; |
|||
range = s_invalid; |
|||
} |
|||
} |
|||
|
|||
if (range != s_invalid) |
|||
{ |
|||
ranges.Add(range); |
|||
added?.Add(range); |
|||
result += range.Count; |
|||
} |
|||
|
|||
MergeRanges(ranges); |
|||
return result; |
|||
} |
|||
|
|||
public static int Remove( |
|||
IList<IndexRange> ranges, |
|||
IndexRange range, |
|||
IList<IndexRange>? removed = null) |
|||
{ |
|||
var result = 0; |
|||
|
|||
for (var i = 0; i < ranges.Count; ++i) |
|||
{ |
|||
var existing = ranges[i]; |
|||
|
|||
if (range.Intersects(existing)) |
|||
{ |
|||
if (range.Begin <= existing.Begin && range.End >= existing.End) |
|||
{ |
|||
ranges.RemoveAt(i--); |
|||
removed?.Add(existing); |
|||
result += existing.Count; |
|||
} |
|||
else if (range.Begin > existing.Begin && range.End >= existing.End) |
|||
{ |
|||
ranges[i] = new IndexRange(existing.Begin, range.Begin - 1); |
|||
removed?.Add(new IndexRange(range.Begin, existing.End)); |
|||
result += existing.End - (range.Begin - 1); |
|||
} |
|||
else if (range.Begin > existing.Begin && range.End < existing.End) |
|||
{ |
|||
ranges[i] = new IndexRange(existing.Begin, range.Begin - 1); |
|||
ranges.Insert(++i, new IndexRange(range.End + 1, existing.End)); |
|||
removed?.Add(range); |
|||
result += range.Count; |
|||
} |
|||
else if (range.End <= existing.End) |
|||
{ |
|||
var remove = new IndexRange(existing.Begin, range.End); |
|||
ranges[i] = new IndexRange(range.End + 1, existing.End); |
|||
removed?.Add(remove); |
|||
result += remove.Count; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static IEnumerable<IndexRange> Subtract( |
|||
IndexRange lhs, |
|||
IEnumerable<IndexRange> rhs) |
|||
{ |
|||
var result = new List<IndexRange> { lhs }; |
|||
|
|||
foreach (var range in rhs) |
|||
{ |
|||
Remove(result, range); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static IEnumerable<int> EnumerateIndices(IEnumerable<IndexRange> ranges) |
|||
{ |
|||
foreach (var range in ranges) |
|||
{ |
|||
for (var i = range.Begin; i <= range.End; ++i) |
|||
{ |
|||
yield return i; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static int GetCount(IEnumerable<IndexRange> ranges) |
|||
{ |
|||
var result = 0; |
|||
|
|||
foreach (var range in ranges) |
|||
{ |
|||
result += (range.End - range.Begin) + 1; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static void MergeRanges(IList<IndexRange> ranges) |
|||
{ |
|||
for (var i = ranges.Count - 2; i >= 0; --i) |
|||
{ |
|||
var r = ranges[i]; |
|||
var r1 = ranges[i + 1]; |
|||
|
|||
if (r.Intersects(r1) || r.End == r1.Begin - 1) |
|||
{ |
|||
ranges[i] = new IndexRange(r.Begin, r1.End); |
|||
ranges.RemoveAt(i + 1); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,49 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public interface ISelectedItemInfo |
|||
{ |
|||
public IndexPath Path { get; } |
|||
} |
|||
|
|||
internal class SelectedItems<TValue, Tinfo> : IReadOnlyList<TValue> |
|||
where Tinfo : ISelectedItemInfo |
|||
{ |
|||
private readonly List<Tinfo> _infos; |
|||
private readonly Func<List<Tinfo>, int, TValue> _getAtImpl; |
|||
|
|||
public SelectedItems( |
|||
List<Tinfo> infos, |
|||
int count, |
|||
Func<List<Tinfo>, int, TValue> getAtImpl) |
|||
{ |
|||
_infos = infos; |
|||
_getAtImpl = getAtImpl; |
|||
Count = count; |
|||
} |
|||
|
|||
public TValue this[int index] => _getAtImpl(_infos, index); |
|||
|
|||
public int Count { get; } |
|||
|
|||
public IEnumerator<TValue> GetEnumerator() |
|||
{ |
|||
for (var i = 0; i < Count; ++i) |
|||
{ |
|||
yield return this[i]; |
|||
} |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,864 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using System.Linq; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class SelectionModel : ISelectionModel, IDisposable |
|||
{ |
|||
private readonly SelectionNode _rootNode; |
|||
private bool _singleSelect; |
|||
private bool _autoSelect; |
|||
private int _operationCount; |
|||
private IndexPath _oldAnchorIndex; |
|||
private IReadOnlyList<IndexPath>? _selectedIndicesCached; |
|||
private IReadOnlyList<object?>? _selectedItemsCached; |
|||
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs; |
|||
|
|||
public event EventHandler<SelectionModelChildrenRequestedEventArgs>? ChildrenRequested; |
|||
public event PropertyChangedEventHandler? PropertyChanged; |
|||
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged; |
|||
|
|||
public SelectionModel() |
|||
{ |
|||
_rootNode = new SelectionNode(this, null); |
|||
SharedLeafNode = new SelectionNode(this, null); |
|||
} |
|||
|
|||
public object? Source |
|||
{ |
|||
get => _rootNode.Source; |
|||
set |
|||
{ |
|||
if (_rootNode.Source != value) |
|||
{ |
|||
var raiseChanged = _rootNode.Source == null && SelectedIndices.Count > 0; |
|||
|
|||
if (_rootNode.Source != null) |
|||
{ |
|||
if (_rootNode.Source != null) |
|||
{ |
|||
using (var operation = new Operation(this)) |
|||
{ |
|||
ClearSelection(resetAnchor: true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
_rootNode.Source = value; |
|||
ApplyAutoSelect(); |
|||
|
|||
RaisePropertyChanged("Source"); |
|||
|
|||
if (raiseChanged) |
|||
{ |
|||
var e = new SelectionModelSelectionChangedEventArgs( |
|||
null, |
|||
SelectedIndices, |
|||
null, |
|||
SelectedItems); |
|||
OnSelectionChanged(e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool SingleSelect |
|||
{ |
|||
get => _singleSelect; |
|||
set |
|||
{ |
|||
if (_singleSelect != value) |
|||
{ |
|||
_singleSelect = value; |
|||
var selectedIndices = SelectedIndices; |
|||
|
|||
if (value && selectedIndices != null && selectedIndices.Count > 0) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
|
|||
// We want to be single select, so make sure there is only
|
|||
// one selected item.
|
|||
var firstSelectionIndexPath = selectedIndices[0]; |
|||
ClearSelection(resetAnchor: true); |
|||
SelectWithPathImpl(firstSelectionIndexPath, select: true); |
|||
SelectedIndex = firstSelectionIndexPath; |
|||
} |
|||
|
|||
RaisePropertyChanged("SingleSelect"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool RetainSelectionOnReset |
|||
{ |
|||
get => _rootNode.RetainSelectionOnReset; |
|||
set => _rootNode.RetainSelectionOnReset = value; |
|||
} |
|||
|
|||
public bool AutoSelect |
|||
{ |
|||
get => _autoSelect; |
|||
set |
|||
{ |
|||
if (_autoSelect != value) |
|||
{ |
|||
_autoSelect = value; |
|||
ApplyAutoSelect(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IndexPath AnchorIndex |
|||
{ |
|||
get |
|||
{ |
|||
IndexPath anchor = default; |
|||
|
|||
if (_rootNode.AnchorIndex >= 0) |
|||
{ |
|||
var path = new List<int>(); |
|||
SelectionNode? current = _rootNode; |
|||
|
|||
while (current?.AnchorIndex >= 0) |
|||
{ |
|||
path.Add(current.AnchorIndex); |
|||
current = current.GetAt(current.AnchorIndex, false); |
|||
} |
|||
|
|||
anchor = new IndexPath(path); |
|||
} |
|||
|
|||
return anchor; |
|||
} |
|||
set |
|||
{ |
|||
var oldValue = AnchorIndex; |
|||
|
|||
if (value != null) |
|||
{ |
|||
SelectionTreeHelper.TraverseIndexPath( |
|||
_rootNode, |
|||
value, |
|||
realizeChildren: true, |
|||
(currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth)); |
|||
} |
|||
else |
|||
{ |
|||
_rootNode.AnchorIndex = -1; |
|||
} |
|||
|
|||
if (_operationCount == 0 && oldValue != AnchorIndex) |
|||
{ |
|||
RaisePropertyChanged("AnchorIndex"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IndexPath SelectedIndex |
|||
{ |
|||
get |
|||
{ |
|||
IndexPath selectedIndex = default; |
|||
var selectedIndices = SelectedIndices; |
|||
|
|||
if (selectedIndices?.Count > 0) |
|||
{ |
|||
selectedIndex = selectedIndices[0]; |
|||
} |
|||
|
|||
return selectedIndex; |
|||
} |
|||
set |
|||
{ |
|||
var isSelected = IsSelectedWithPartialAt(value); |
|||
|
|||
if (!IsSelectedAt(value) || SelectedItems.Count > 1) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
ClearSelection(resetAnchor: true); |
|||
SelectWithPathImpl(value, select: true); |
|||
ApplyAutoSelect(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public object? SelectedItem |
|||
{ |
|||
get |
|||
{ |
|||
object? item = null; |
|||
var selectedItems = SelectedItems; |
|||
|
|||
if (selectedItems?.Count > 0) |
|||
{ |
|||
item = selectedItems[0]; |
|||
} |
|||
|
|||
return item; |
|||
} |
|||
} |
|||
|
|||
public IReadOnlyList<object?> SelectedItems |
|||
{ |
|||
get |
|||
{ |
|||
if (_selectedItemsCached == null) |
|||
{ |
|||
var selectedInfos = new List<SelectedItemInfo>(); |
|||
var count = 0; |
|||
|
|||
if (_rootNode.Source != null) |
|||
{ |
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
realizeChildren: false, |
|||
currentInfo => |
|||
{ |
|||
if (currentInfo.Node.SelectedCount > 0) |
|||
{ |
|||
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path)); |
|||
count += currentInfo.Node.SelectedCount; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// Instead of creating a dumb vector that takes up the space for all the selected items,
|
|||
// we create a custom IReadOnlyList implementation that calls back using a delegate to find
|
|||
// the selected item at a particular index. This avoid having to create the storage and copying
|
|||
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
|
|||
// easier to consume flat vector view of objects.
|
|||
var selectedItems = new SelectedItems<object?, SelectedItemInfo> ( |
|||
selectedInfos, |
|||
count, |
|||
(infos, index) => |
|||
{ |
|||
var currentIndex = 0; |
|||
object? item = null; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var node = info.Node; |
|||
|
|||
if (node != null) |
|||
{ |
|||
var currentCount = node.SelectedCount; |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
var targetIndex = node.SelectedIndices[index - currentIndex]; |
|||
item = node.ItemsSourceView!.GetAt(targetIndex); |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException( |
|||
"Selection has changed since SelectedItems property was read."); |
|||
} |
|||
} |
|||
|
|||
return item; |
|||
}); |
|||
|
|||
_selectedItemsCached = selectedItems; |
|||
} |
|||
|
|||
return _selectedItemsCached; |
|||
} |
|||
} |
|||
|
|||
public IReadOnlyList<IndexPath> SelectedIndices |
|||
{ |
|||
get |
|||
{ |
|||
if (_selectedIndicesCached == null) |
|||
{ |
|||
var selectedInfos = new List<SelectedItemInfo>(); |
|||
var count = 0; |
|||
|
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
false, |
|||
currentInfo => |
|||
{ |
|||
if (currentInfo.Node.SelectedCount > 0) |
|||
{ |
|||
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path)); |
|||
count += currentInfo.Node.SelectedCount; |
|||
} |
|||
}); |
|||
|
|||
// Instead of creating a dumb vector that takes up the space for all the selected indices,
|
|||
// we create a custom VectorView implimentation that calls back using a delegate to find
|
|||
// the IndexPath at a particular index. This avoid having to create the storage and copying
|
|||
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
|
|||
// easier to consume flat vector view of IndexPaths.
|
|||
var indices = new SelectedItems<IndexPath, SelectedItemInfo>( |
|||
selectedInfos, |
|||
count, |
|||
(infos, index) => // callback for GetAt(index)
|
|||
{ |
|||
var currentIndex = 0; |
|||
IndexPath path = default; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var node = info.Node; |
|||
|
|||
if (node != null) |
|||
{ |
|||
var currentCount = node.SelectedCount; |
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
int targetIndex = node.SelectedIndices[index - currentIndex]; |
|||
path = info.Path.CloneWithChildIndex(targetIndex); |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException( |
|||
"Selection has changed since SelectedIndices property was read."); |
|||
} |
|||
} |
|||
|
|||
return path; |
|||
}); |
|||
|
|||
_selectedIndicesCached = indices; |
|||
} |
|||
|
|||
return _selectedIndicesCached; |
|||
} |
|||
} |
|||
|
|||
internal SelectionNode SharedLeafNode { get; private set; } |
|||
|
|||
public void Dispose() |
|||
{ |
|||
ClearSelection(resetAnchor: false); |
|||
_rootNode.Cleanup(); |
|||
_rootNode.Dispose(); |
|||
_selectedIndicesCached = null; |
|||
_selectedItemsCached = null; |
|||
} |
|||
|
|||
public void SetAnchorIndex(int index) => AnchorIndex = new IndexPath(index); |
|||
|
|||
public void SetAnchorIndex(int groupIndex, int index) => AnchorIndex = new IndexPath(groupIndex, index); |
|||
|
|||
public void Select(int index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectImpl(index, select: true); |
|||
} |
|||
|
|||
public void Select(int groupIndex, int itemIndex) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectWithGroupImpl(groupIndex, itemIndex, select: true); |
|||
} |
|||
|
|||
public void SelectAt(IndexPath index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectWithPathImpl(index, select: true); |
|||
} |
|||
|
|||
public void Deselect(int index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectImpl(index, select: false); |
|||
ApplyAutoSelect(); |
|||
} |
|||
|
|||
public void Deselect(int groupIndex, int itemIndex) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectWithGroupImpl(groupIndex, itemIndex, select: false); |
|||
ApplyAutoSelect(); |
|||
} |
|||
|
|||
public void DeselectAt(IndexPath index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectWithPathImpl(index, select: false); |
|||
ApplyAutoSelect(); |
|||
} |
|||
|
|||
public bool IsSelected(int index) => _rootNode.IsSelected(index); |
|||
|
|||
public bool IsSelected(int grouIndex, int itemIndex) |
|||
{ |
|||
return IsSelectedAt(new IndexPath(grouIndex, itemIndex)); |
|||
} |
|||
|
|||
public bool IsSelectedAt(IndexPath index) |
|||
{ |
|||
var path = index; |
|||
SelectionNode? node = _rootNode; |
|||
|
|||
for (int i = 0; i < path.GetSize() - 1; i++) |
|||
{ |
|||
var childIndex = path.GetAt(i); |
|||
node = node.GetAt(childIndex, realizeChild: false); |
|||
|
|||
if (node == null) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return node.IsSelected(index.GetAt(index.GetSize() - 1)); |
|||
} |
|||
|
|||
public bool? IsSelectedWithPartial(int index) |
|||
{ |
|||
if (index < 0) |
|||
{ |
|||
throw new ArgumentException("Index must be >= 0", nameof(index)); |
|||
} |
|||
|
|||
var isSelected = _rootNode.IsSelectedWithPartial(index); |
|||
return isSelected; |
|||
} |
|||
|
|||
public bool? IsSelectedWithPartial(int groupIndex, int itemIndex) |
|||
{ |
|||
if (groupIndex < 0) |
|||
{ |
|||
throw new ArgumentException("Group index must be >= 0", nameof(groupIndex)); |
|||
} |
|||
|
|||
if (itemIndex < 0) |
|||
{ |
|||
throw new ArgumentException("Item index must be >= 0", nameof(itemIndex)); |
|||
} |
|||
|
|||
var isSelected = (bool?)false; |
|||
var childNode = _rootNode.GetAt(groupIndex, realizeChild: false); |
|||
|
|||
if (childNode != null) |
|||
{ |
|||
isSelected = childNode.IsSelectedWithPartial(itemIndex); |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
public bool? IsSelectedWithPartialAt(IndexPath index) |
|||
{ |
|||
var path = index; |
|||
var isRealized = true; |
|||
SelectionNode? node = _rootNode; |
|||
|
|||
for (int i = 0; i < path.GetSize() - 1; i++) |
|||
{ |
|||
var childIndex = path.GetAt(i); |
|||
node = node.GetAt(childIndex, realizeChild: false); |
|||
|
|||
if (node == null) |
|||
{ |
|||
isRealized = false; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
var isSelected = (bool?)false; |
|||
|
|||
if (isRealized) |
|||
{ |
|||
var size = path.GetSize(); |
|||
if (size == 0) |
|||
{ |
|||
isSelected = SelectionNode.ConvertToNullableBool(node!.EvaluateIsSelectedBasedOnChildrenNodes()); |
|||
} |
|||
else |
|||
{ |
|||
isSelected = node!.IsSelectedWithPartial(path.GetAt(size - 1)); |
|||
} |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
public void SelectRangeFromAnchor(int index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeFromAnchorImpl(index, select: true); |
|||
} |
|||
|
|||
public void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, select: true); |
|||
} |
|||
|
|||
public void SelectRangeFromAnchorTo(IndexPath index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeImpl(AnchorIndex, index, select: true); |
|||
} |
|||
|
|||
public void DeselectRangeFromAnchor(int index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeFromAnchorImpl(index, select: false); |
|||
} |
|||
|
|||
public void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, false /* select */); |
|||
} |
|||
|
|||
public void DeselectRangeFromAnchorTo(IndexPath index) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeImpl(AnchorIndex, index, select: false); |
|||
} |
|||
|
|||
public void SelectRange(IndexPath start, IndexPath end) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeImpl(start, end, select: true); |
|||
} |
|||
|
|||
public void DeselectRange(IndexPath start, IndexPath end) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectRangeImpl(start, end, select: false); |
|||
} |
|||
|
|||
public void SelectAll() |
|||
{ |
|||
using var operation = new Operation(this); |
|||
|
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
realizeChildren: true, |
|||
info => |
|||
{ |
|||
if (info.Node.DataCount > 0) |
|||
{ |
|||
info.Node.SelectAll(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public void ClearSelection() |
|||
{ |
|||
using var operation = new Operation(this); |
|||
ClearSelection(resetAnchor: true); |
|||
ApplyAutoSelect(); |
|||
} |
|||
|
|||
public IDisposable Update() => new Operation(this); |
|||
|
|||
protected void OnPropertyChanged(string propertyName) |
|||
{ |
|||
RaisePropertyChanged(propertyName); |
|||
} |
|||
|
|||
private void RaisePropertyChanged(string propertyName) |
|||
{ |
|||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
|||
} |
|||
|
|||
public void OnSelectionInvalidatedDueToCollectionChange( |
|||
bool selectionInvalidated, |
|||
IReadOnlyList<object?>? removedItems) |
|||
{ |
|||
SelectionModelSelectionChangedEventArgs? e = null; |
|||
|
|||
if (selectionInvalidated) |
|||
{ |
|||
e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null); |
|||
} |
|||
|
|||
OnSelectionChanged(e); |
|||
ApplyAutoSelect(); |
|||
} |
|||
|
|||
internal IObservable<object?>? ResolvePath(object data, IndexPath dataIndexPath) |
|||
{ |
|||
IObservable<object?>? resolved = null; |
|||
|
|||
// Raise ChildrenRequested event if there is a handler
|
|||
if (ChildrenRequested != null) |
|||
{ |
|||
if (_childrenRequestedEventArgs == null) |
|||
{ |
|||
_childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, dataIndexPath, false); |
|||
} |
|||
else |
|||
{ |
|||
_childrenRequestedEventArgs.Initialize(data, dataIndexPath, false); |
|||
} |
|||
|
|||
ChildrenRequested(this, _childrenRequestedEventArgs); |
|||
resolved = _childrenRequestedEventArgs.Children; |
|||
|
|||
// Clear out the values in the args so that it cannot be used after the event handler call.
|
|||
_childrenRequestedEventArgs.Initialize(null, default, true); |
|||
} |
|||
|
|||
return resolved; |
|||
} |
|||
|
|||
private void ClearSelection(bool resetAnchor) |
|||
{ |
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
realizeChildren: false, |
|||
info => info.Node.Clear()); |
|||
|
|||
if (resetAnchor) |
|||
{ |
|||
AnchorIndex = default; |
|||
} |
|||
} |
|||
|
|||
private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null) |
|||
{ |
|||
_selectedIndicesCached = null; |
|||
_selectedItemsCached = null; |
|||
|
|||
if (e != null) |
|||
{ |
|||
SelectionChanged?.Invoke(this, e); |
|||
|
|||
RaisePropertyChanged(nameof(SelectedIndex)); |
|||
RaisePropertyChanged(nameof(SelectedIndices)); |
|||
|
|||
if (_rootNode.Source != null) |
|||
{ |
|||
RaisePropertyChanged(nameof(SelectedItem)); |
|||
RaisePropertyChanged(nameof(SelectedItems)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void SelectImpl(int index, bool select) |
|||
{ |
|||
if (_singleSelect) |
|||
{ |
|||
ClearSelection(resetAnchor: true); |
|||
} |
|||
|
|||
var selected = _rootNode.Select(index, select); |
|||
|
|||
if (selected) |
|||
{ |
|||
AnchorIndex = new IndexPath(index); |
|||
} |
|||
} |
|||
|
|||
private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select) |
|||
{ |
|||
if (_singleSelect) |
|||
{ |
|||
ClearSelection(resetAnchor: true); |
|||
} |
|||
|
|||
var childNode = _rootNode.GetAt(groupIndex, realizeChild: true); |
|||
var selected = childNode!.Select(itemIndex, select); |
|||
|
|||
if (selected) |
|||
{ |
|||
AnchorIndex = new IndexPath(groupIndex, itemIndex); |
|||
} |
|||
} |
|||
|
|||
private void SelectWithPathImpl(IndexPath index, bool select) |
|||
{ |
|||
bool selected = false; |
|||
|
|||
if (_singleSelect) |
|||
{ |
|||
ClearSelection(resetAnchor: true); |
|||
} |
|||
|
|||
SelectionTreeHelper.TraverseIndexPath( |
|||
_rootNode, |
|||
index, |
|||
true, |
|||
(currentNode, path, depth, childIndex) => |
|||
{ |
|||
if (depth == path.GetSize() - 1) |
|||
{ |
|||
selected = currentNode.Select(childIndex, select); |
|||
} |
|||
} |
|||
); |
|||
|
|||
if (selected) |
|||
{ |
|||
AnchorIndex = index; |
|||
} |
|||
} |
|||
|
|||
private void SelectRangeFromAnchorImpl(int index, bool select) |
|||
{ |
|||
int anchorIndex = 0; |
|||
var anchor = AnchorIndex; |
|||
|
|||
if (anchor != null) |
|||
{ |
|||
anchorIndex = anchor.GetAt(0); |
|||
} |
|||
|
|||
_rootNode.SelectRange(new IndexRange(anchorIndex, index), select); |
|||
} |
|||
|
|||
private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select) |
|||
{ |
|||
var startGroupIndex = 0; |
|||
var startItemIndex = 0; |
|||
var anchorIndex = AnchorIndex; |
|||
|
|||
if (anchorIndex != null) |
|||
{ |
|||
startGroupIndex = anchorIndex.GetAt(0); |
|||
startItemIndex = anchorIndex.GetAt(1); |
|||
} |
|||
|
|||
// Make sure start > end
|
|||
if (startGroupIndex > endGroupIndex || |
|||
(startGroupIndex == endGroupIndex && startItemIndex > endItemIndex)) |
|||
{ |
|||
int temp = startGroupIndex; |
|||
startGroupIndex = endGroupIndex; |
|||
endGroupIndex = temp; |
|||
temp = startItemIndex; |
|||
startItemIndex = endItemIndex; |
|||
endItemIndex = temp; |
|||
} |
|||
|
|||
for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++) |
|||
{ |
|||
var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!; |
|||
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0; |
|||
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1; |
|||
groupNode.SelectRange(new IndexRange(startIndex, endIndex), select); |
|||
} |
|||
} |
|||
|
|||
private void SelectRangeImpl(IndexPath start, IndexPath end, bool select) |
|||
{ |
|||
var winrtStart = start; |
|||
var winrtEnd = end; |
|||
|
|||
// Make sure start <= end
|
|||
if (winrtEnd.CompareTo(winrtStart) == -1) |
|||
{ |
|||
var temp = winrtStart; |
|||
winrtStart = winrtEnd; |
|||
winrtEnd = temp; |
|||
} |
|||
|
|||
// Note: Since we do not know the depth of the tree, we have to walk to each leaf
|
|||
SelectionTreeHelper.TraverseRangeRealizeChildren( |
|||
_rootNode, |
|||
winrtStart, |
|||
winrtEnd, |
|||
info => |
|||
{ |
|||
if (info.Path >= winrtStart && info.Path <= winrtEnd) |
|||
{ |
|||
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private void BeginOperation() |
|||
{ |
|||
if (_operationCount++ == 0) |
|||
{ |
|||
_oldAnchorIndex = AnchorIndex; |
|||
_rootNode.BeginOperation(); |
|||
} |
|||
} |
|||
|
|||
private void EndOperation() |
|||
{ |
|||
if (_operationCount == 0) |
|||
{ |
|||
throw new AvaloniaInternalException("No selection operation in progress."); |
|||
} |
|||
|
|||
SelectionModelSelectionChangedEventArgs? e = null; |
|||
|
|||
if (--_operationCount == 0) |
|||
{ |
|||
var changes = new List<SelectionNodeOperation>(); |
|||
_rootNode.EndOperation(changes); |
|||
|
|||
if (changes.Count > 0) |
|||
{ |
|||
var changeSet = new SelectionModelChangeSet(changes); |
|||
e = changeSet.CreateEventArgs(); |
|||
} |
|||
|
|||
OnSelectionChanged(e); |
|||
|
|||
if (_oldAnchorIndex != AnchorIndex) |
|||
{ |
|||
RaisePropertyChanged(nameof(AnchorIndex)); |
|||
} |
|||
|
|||
_rootNode.Cleanup(); |
|||
_oldAnchorIndex = default; |
|||
} |
|||
} |
|||
|
|||
private void ApplyAutoSelect() |
|||
{ |
|||
if (AutoSelect) |
|||
{ |
|||
_selectedIndicesCached = null; |
|||
|
|||
if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0) |
|||
{ |
|||
using var operation = new Operation(this); |
|||
SelectImpl(0, true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal class SelectedItemInfo : ISelectedItemInfo |
|||
{ |
|||
public SelectedItemInfo(SelectionNode node, IndexPath path) |
|||
{ |
|||
Node = node; |
|||
Path = path; |
|||
} |
|||
|
|||
public SelectionNode Node { get; } |
|||
public IndexPath Path { get; } |
|||
public int Count => Node.SelectedCount; |
|||
} |
|||
|
|||
private struct Operation : IDisposable |
|||
{ |
|||
private readonly SelectionModel _manager; |
|||
public Operation(SelectionModel manager) => (_manager = manager).BeginOperation(); |
|||
public void Dispose() => _manager.EndOperation(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,170 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class SelectionModelChangeSet |
|||
{ |
|||
private readonly List<SelectionNodeOperation> _changes; |
|||
|
|||
public SelectionModelChangeSet(List<SelectionNodeOperation> changes) |
|||
{ |
|||
_changes = changes; |
|||
} |
|||
|
|||
public SelectionModelSelectionChangedEventArgs CreateEventArgs() |
|||
{ |
|||
var deselectedIndexCount = 0; |
|||
var selectedIndexCount = 0; |
|||
var deselectedItemCount = 0; |
|||
var selectedItemCount = 0; |
|||
|
|||
foreach (var change in _changes) |
|||
{ |
|||
deselectedIndexCount += change.DeselectedCount; |
|||
selectedIndexCount += change.SelectedCount; |
|||
|
|||
if (change.Items != null) |
|||
{ |
|||
deselectedItemCount += change.DeselectedCount; |
|||
selectedItemCount += change.SelectedCount; |
|||
} |
|||
} |
|||
|
|||
var deselectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>( |
|||
_changes, |
|||
deselectedIndexCount, |
|||
GetDeselectedIndexAt); |
|||
var selectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>( |
|||
_changes, |
|||
selectedIndexCount, |
|||
GetSelectedIndexAt); |
|||
var deselectedItems = new SelectedItems<object?, SelectionNodeOperation>( |
|||
_changes, |
|||
deselectedItemCount, |
|||
GetDeselectedItemAt); |
|||
var selectedItems = new SelectedItems<object?, SelectionNodeOperation>( |
|||
_changes, |
|||
selectedItemCount, |
|||
GetSelectedItemAt); |
|||
|
|||
return new SelectionModelSelectionChangedEventArgs( |
|||
deselectedIndices, |
|||
selectedIndices, |
|||
deselectedItems, |
|||
selectedItems); |
|||
} |
|||
|
|||
private IndexPath GetDeselectedIndexAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index) |
|||
{ |
|||
static int GetCount(SelectionNodeOperation info) => info.DeselectedCount; |
|||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges; |
|||
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x)); |
|||
} |
|||
|
|||
private IndexPath GetSelectedIndexAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index) |
|||
{ |
|||
static int GetCount(SelectionNodeOperation info) => info.SelectedCount; |
|||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges; |
|||
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x)); |
|||
} |
|||
|
|||
private object? GetDeselectedItemAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index) |
|||
{ |
|||
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.DeselectedCount : 0; |
|||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges; |
|||
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x)); |
|||
} |
|||
|
|||
private object? GetSelectedItemAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index) |
|||
{ |
|||
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.SelectedCount : 0; |
|||
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges; |
|||
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x)); |
|||
} |
|||
|
|||
private IndexPath GetIndexAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index, |
|||
Func<SelectionNodeOperation, int> getCount, |
|||
Func<SelectionNodeOperation, List<IndexRange>?> getRanges) |
|||
{ |
|||
var currentIndex = 0; |
|||
IndexPath path = default; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var currentCount = getCount(info); |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex); |
|||
path = info.Path.CloneWithChildIndex(targetIndex); |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
|
|||
return path; |
|||
} |
|||
|
|||
private object? GetItemAt( |
|||
List<SelectionNodeOperation> infos, |
|||
int index, |
|||
Func<SelectionNodeOperation, int> getCount, |
|||
Func<SelectionNodeOperation, List<IndexRange>?> getRanges) |
|||
{ |
|||
var currentIndex = 0; |
|||
object? item = null; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var currentCount = getCount(info); |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex); |
|||
item = info.Items?.GetAt(targetIndex); |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
|
|||
return item; |
|||
} |
|||
|
|||
private int GetIndexAt(List<IndexRange>? ranges, int index) |
|||
{ |
|||
var currentIndex = 0; |
|||
|
|||
if (ranges != null) |
|||
{ |
|||
foreach (var range in ranges) |
|||
{ |
|||
var currentCount = (range.End - range.Begin) + 1; |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
return range.Begin + (index - currentIndex); |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
} |
|||
|
|||
throw new IndexOutOfRangeException(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Provides data for the <see cref="SelectionModel.ChildrenRequested"/> event.
|
|||
/// </summary>
|
|||
public class SelectionModelChildrenRequestedEventArgs : EventArgs |
|||
{ |
|||
private object? _source; |
|||
private IndexPath _sourceIndexPath; |
|||
private bool _throwOnAccess; |
|||
|
|||
internal SelectionModelChildrenRequestedEventArgs( |
|||
object source, |
|||
IndexPath sourceIndexPath, |
|||
bool throwOnAccess) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
Initialize(source, sourceIndexPath, throwOnAccess); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets an observable which produces the children of the <see cref="Source"/>
|
|||
/// object.
|
|||
/// </summary>
|
|||
public IObservable<object?>? Children { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the object whose children are being requested.
|
|||
/// </summary>
|
|||
public object Source |
|||
{ |
|||
get |
|||
{ |
|||
if (_throwOnAccess) |
|||
{ |
|||
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs)); |
|||
} |
|||
|
|||
return _source!; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the object whose children are being requested.
|
|||
/// </summary>
|
|||
public IndexPath SourceIndex |
|||
{ |
|||
get |
|||
{ |
|||
if (_throwOnAccess) |
|||
{ |
|||
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs)); |
|||
} |
|||
|
|||
return _sourceIndexPath; |
|||
} |
|||
} |
|||
|
|||
internal void Initialize( |
|||
object? source, |
|||
IndexPath sourceIndexPath, |
|||
bool throwOnAccess) |
|||
{ |
|||
if (!throwOnAccess && source == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(source)); |
|||
} |
|||
|
|||
_source = source; |
|||
_sourceIndexPath = sourceIndexPath; |
|||
_throwOnAccess = throwOnAccess; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class SelectionModelSelectionChangedEventArgs : EventArgs |
|||
{ |
|||
public SelectionModelSelectionChangedEventArgs( |
|||
IReadOnlyList<IndexPath>? deselectedIndices, |
|||
IReadOnlyList<IndexPath>? selectedIndices, |
|||
IReadOnlyList<object?>? deselectedItems, |
|||
IReadOnlyList<object?>? selectedItems) |
|||
{ |
|||
DeselectedIndices = deselectedIndices ?? Array.Empty<IndexPath>(); |
|||
SelectedIndices = selectedIndices ?? Array.Empty<IndexPath>(); |
|||
DeselectedItems = deselectedItems ?? Array.Empty<object?>(); |
|||
SelectedItems= selectedItems ?? Array.Empty<object?>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the indices of the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<IndexPath> DeselectedIndices { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the indices of the items that were added to the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<IndexPath> SelectedIndices { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<object?> DeselectedItems { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were added to the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<object?> SelectedItems { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,950 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Tracks nested selection.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// SelectionNode is the internal tree data structure that we keep track of for selection in
|
|||
/// a nested scenario. This would map to one ItemsSourceView/Collection. This node reacts to
|
|||
/// collection changes and keeps the selected indices up to date. This can either be a leaf
|
|||
/// node or a non leaf node.
|
|||
/// </remarks>
|
|||
internal class SelectionNode : IDisposable |
|||
{ |
|||
private readonly SelectionModel _manager; |
|||
private readonly List<SelectionNode?> _childrenNodes = new List<SelectionNode?>(); |
|||
private readonly SelectionNode? _parent; |
|||
private readonly List<IndexRange> _selected = new List<IndexRange>(); |
|||
private readonly List<int> _selectedIndicesCached = new List<int>(); |
|||
private IDisposable? _childrenSubscription; |
|||
private SelectionNodeOperation? _operation; |
|||
private object? _source; |
|||
private bool _selectedIndicesCacheIsValid; |
|||
private bool _retainSelectionOnReset; |
|||
private List<object?>? _selectedItems; |
|||
|
|||
public SelectionNode(SelectionModel manager, SelectionNode? parent) |
|||
{ |
|||
_manager = manager; |
|||
_parent = parent; |
|||
} |
|||
|
|||
public int AnchorIndex { get; set; } = -1; |
|||
|
|||
public bool RetainSelectionOnReset |
|||
{ |
|||
get => _retainSelectionOnReset; |
|||
set |
|||
{ |
|||
if (_retainSelectionOnReset != value) |
|||
{ |
|||
_retainSelectionOnReset = value; |
|||
|
|||
if (_retainSelectionOnReset) |
|||
{ |
|||
_selectedItems = new List<object?>(); |
|||
PopulateSelectedItemsFromSelectedIndices(); |
|||
} |
|||
else |
|||
{ |
|||
_selectedItems = null; |
|||
} |
|||
|
|||
foreach (var child in _childrenNodes) |
|||
{ |
|||
if (child != null) |
|||
{ |
|||
child.RetainSelectionOnReset = value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public object? Source |
|||
{ |
|||
get => _source; |
|||
set |
|||
{ |
|||
if (_source != value) |
|||
{ |
|||
if (_source != null) |
|||
{ |
|||
ClearSelection(); |
|||
ClearChildNodes(); |
|||
UnhookCollectionChangedHandler(); |
|||
} |
|||
|
|||
_source = value; |
|||
|
|||
// Setup ItemsSourceView
|
|||
var newDataSource = value as ItemsSourceView; |
|||
|
|||
if (value != null && newDataSource == null) |
|||
{ |
|||
newDataSource = new ItemsSourceView((IEnumerable)value); |
|||
} |
|||
|
|||
ItemsSourceView = newDataSource; |
|||
|
|||
PopulateSelectedItemsFromSelectedIndices(); |
|||
HookupCollectionChangedHandler(); |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public ItemsSourceView? ItemsSourceView { get; private set; } |
|||
public int DataCount => ItemsSourceView?.Count ?? 0; |
|||
public int ChildrenNodeCount => _childrenNodes.Count; |
|||
public int RealizedChildrenNodeCount { get; private set; } |
|||
|
|||
public IndexPath IndexPath |
|||
{ |
|||
get |
|||
{ |
|||
var path = new List<int>(); ; |
|||
var parent = _parent; |
|||
var child = this; |
|||
|
|||
while (parent != null) |
|||
{ |
|||
var childNodes = parent._childrenNodes; |
|||
var index = childNodes.IndexOf(child); |
|||
|
|||
// We are walking up to the parent, so the path will be backwards
|
|||
path.Insert(0, index); |
|||
child = parent; |
|||
parent = parent._parent; |
|||
} |
|||
|
|||
return new IndexPath(path); |
|||
} |
|||
} |
|||
|
|||
// For a genuine tree view, we dont know which node is leaf until we
|
|||
// actually walk to it, so currently the tree builds up to the leaf. I don't
|
|||
// create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid
|
|||
// an explosion of node objects. However, I'm still creating the m_childrenNodes
|
|||
// collection unfortunately.
|
|||
public SelectionNode? GetAt(int index, bool realizeChild) |
|||
{ |
|||
SelectionNode? child = null; |
|||
|
|||
if (realizeChild) |
|||
{ |
|||
if (ItemsSourceView == null || index < 0 || index >= ItemsSourceView.Count) |
|||
{ |
|||
throw new IndexOutOfRangeException(); |
|||
} |
|||
|
|||
if (_childrenNodes.Count == 0) |
|||
{ |
|||
if (ItemsSourceView != null) |
|||
{ |
|||
for (int i = 0; i < ItemsSourceView.Count; i++) |
|||
{ |
|||
_childrenNodes.Add(null); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (_childrenNodes[index] == null) |
|||
{ |
|||
var childData = ItemsSourceView!.GetAt(index); |
|||
IObservable<object?>? resolver = null; |
|||
|
|||
if (childData != null) |
|||
{ |
|||
var childDataIndexPath = IndexPath.CloneWithChildIndex(index); |
|||
resolver = _manager.ResolvePath(childData, childDataIndexPath); |
|||
} |
|||
|
|||
if (resolver != null) |
|||
{ |
|||
child = new SelectionNode(_manager, parent: this); |
|||
child.SetChildrenObservable(resolver); |
|||
} |
|||
else if (childData is IEnumerable<object> || childData is IList) |
|||
{ |
|||
child = new SelectionNode(_manager, parent: this); |
|||
child.Source = childData; |
|||
} |
|||
else |
|||
{ |
|||
child = _manager.SharedLeafNode; |
|||
} |
|||
|
|||
if (_operation != null && child != _manager.SharedLeafNode) |
|||
{ |
|||
child.BeginOperation(); |
|||
} |
|||
|
|||
_childrenNodes[index] = child; |
|||
RealizedChildrenNodeCount++; |
|||
} |
|||
else |
|||
{ |
|||
child = _childrenNodes[index]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (_childrenNodes.Count > 0) |
|||
{ |
|||
child = _childrenNodes[index]; |
|||
} |
|||
} |
|||
|
|||
return child; |
|||
} |
|||
|
|||
public void SetChildrenObservable(IObservable<object?> resolver) |
|||
{ |
|||
_childrenSubscription = resolver.Subscribe(x => |
|||
{ |
|||
if (Source != null) |
|||
{ |
|||
using (_manager.Update()) |
|||
{ |
|||
SelectionTreeHelper.Traverse( |
|||
this, |
|||
realizeChildren: false, |
|||
info => info.Node.Clear()); |
|||
} |
|||
} |
|||
|
|||
Source = x; |
|||
}); |
|||
} |
|||
|
|||
public int SelectedCount { get; private set; } |
|||
|
|||
public bool IsSelected(int index) |
|||
{ |
|||
var isSelected = false; |
|||
|
|||
foreach (var range in _selected) |
|||
{ |
|||
if (range.Contains(index)) |
|||
{ |
|||
isSelected = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
// True -> Selected
|
|||
// False -> Not Selected
|
|||
// Null -> Some descendents are selected and some are not
|
|||
public bool? IsSelectedWithPartial() |
|||
{ |
|||
var isSelected = (bool?)false; |
|||
|
|||
if (_parent != null) |
|||
{ |
|||
var parentsChildren = _parent._childrenNodes; |
|||
|
|||
var myIndexInParent = parentsChildren.IndexOf(this); |
|||
|
|||
if (myIndexInParent != -1) |
|||
{ |
|||
isSelected = _parent.IsSelectedWithPartial(myIndexInParent); |
|||
} |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
// True -> Selected
|
|||
// False -> Not Selected
|
|||
// Null -> Some descendents are selected and some are not
|
|||
public bool? IsSelectedWithPartial(int index) |
|||
{ |
|||
SelectionState selectionState; |
|||
|
|||
if (_childrenNodes.Count == 0 || // no nodes realized
|
|||
_childrenNodes.Count <= index || // target node is not realized
|
|||
_childrenNodes[index] == null || // target node is not realized
|
|||
_childrenNodes[index] == _manager.SharedLeafNode) // target node is a leaf node.
|
|||
{ |
|||
// Ask parent if the target node is selected.
|
|||
selectionState = IsSelected(index) ? SelectionState.Selected : SelectionState.NotSelected; |
|||
} |
|||
else |
|||
{ |
|||
// targetNode is the node representing the index. This node is the parent.
|
|||
// targetNode is a non-leaf node, containing one or many children nodes. Evaluate
|
|||
// based on children of targetNode.
|
|||
var targetNode = _childrenNodes[index]; |
|||
selectionState = targetNode!.EvaluateIsSelectedBasedOnChildrenNodes(); |
|||
} |
|||
|
|||
return ConvertToNullableBool(selectionState); |
|||
} |
|||
|
|||
public int SelectedIndex |
|||
{ |
|||
get => SelectedCount > 0 ? SelectedIndices[0] : -1; |
|||
set |
|||
{ |
|||
if (IsValidIndex(value) && (SelectedCount != 1 || !IsSelected(value))) |
|||
{ |
|||
ClearSelection(); |
|||
|
|||
if (value != -1) |
|||
{ |
|||
Select(value, true); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public List<int> SelectedIndices |
|||
{ |
|||
get |
|||
{ |
|||
if (!_selectedIndicesCacheIsValid) |
|||
{ |
|||
_selectedIndicesCacheIsValid = true; |
|||
|
|||
foreach (var range in _selected) |
|||
{ |
|||
for (int index = range.Begin; index <= range.End; index++) |
|||
{ |
|||
// Avoid duplicates
|
|||
if (!_selectedIndicesCached.Contains(index)) |
|||
{ |
|||
_selectedIndicesCached.Add(index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Sort the list for easy consumption
|
|||
_selectedIndicesCached.Sort(); |
|||
} |
|||
|
|||
return _selectedIndicesCached; |
|||
} |
|||
} |
|||
|
|||
public IEnumerable<object> SelectedItems |
|||
{ |
|||
get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x)); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_childrenSubscription?.Dispose(); |
|||
ItemsSourceView?.Dispose(); |
|||
ClearChildNodes(); |
|||
UnhookCollectionChangedHandler(); |
|||
} |
|||
|
|||
public void BeginOperation() |
|||
{ |
|||
if (_operation != null) |
|||
{ |
|||
throw new AvaloniaInternalException("Selection operation already in progress."); |
|||
} |
|||
|
|||
_operation = new SelectionNodeOperation(this); |
|||
|
|||
for (var i = 0; i < _childrenNodes.Count; ++i) |
|||
{ |
|||
var child = _childrenNodes[i]; |
|||
|
|||
if (child != null && child != _manager.SharedLeafNode) |
|||
{ |
|||
child.BeginOperation(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void EndOperation(List<SelectionNodeOperation> changes) |
|||
{ |
|||
if (_operation == null) |
|||
{ |
|||
throw new AvaloniaInternalException("No selection operation in progress."); |
|||
} |
|||
|
|||
if (_operation.HasChanges) |
|||
{ |
|||
changes.Add(_operation); |
|||
} |
|||
|
|||
_operation = null; |
|||
|
|||
for (var i = 0; i < _childrenNodes.Count; ++i) |
|||
{ |
|||
var child = _childrenNodes[i]; |
|||
|
|||
if (child != null && child != _manager.SharedLeafNode) |
|||
{ |
|||
child.EndOperation(changes); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool Cleanup() |
|||
{ |
|||
var result = SelectedCount == 0; |
|||
|
|||
for (var i = 0; i < _childrenNodes.Count; ++i) |
|||
{ |
|||
var child = _childrenNodes[i]; |
|||
|
|||
if (child != null) |
|||
{ |
|||
if (child.Cleanup()) |
|||
{ |
|||
child.Dispose(); |
|||
_childrenNodes[i] = null; |
|||
} |
|||
else |
|||
{ |
|||
result = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public bool Select(int index, bool select) |
|||
{ |
|||
return Select(index, select, raiseOnSelectionChanged: true); |
|||
} |
|||
|
|||
public bool ToggleSelect(int index) |
|||
{ |
|||
return Select(index, !IsSelected(index)); |
|||
} |
|||
|
|||
public void SelectAll() |
|||
{ |
|||
if (ItemsSourceView != null) |
|||
{ |
|||
var size = ItemsSourceView.Count; |
|||
|
|||
if (size > 0) |
|||
{ |
|||
SelectRange(new IndexRange(0, size - 1), select: true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Clear() => ClearSelection(); |
|||
|
|||
public bool SelectRange(IndexRange range, bool select) |
|||
{ |
|||
if (IsValidIndex(range.Begin) && IsValidIndex(range.End)) |
|||
{ |
|||
if (select) |
|||
{ |
|||
AddRange(range, raiseOnSelectionChanged: true); |
|||
} |
|||
else |
|||
{ |
|||
RemoveRange(range, raiseOnSelectionChanged: true); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private void HookupCollectionChangedHandler() |
|||
{ |
|||
if (ItemsSourceView != null) |
|||
{ |
|||
ItemsSourceView.CollectionChanged += OnSourceListChanged; |
|||
} |
|||
} |
|||
|
|||
private void UnhookCollectionChangedHandler() |
|||
{ |
|||
if (ItemsSourceView != null) |
|||
{ |
|||
ItemsSourceView.CollectionChanged -= OnSourceListChanged; |
|||
} |
|||
} |
|||
|
|||
private bool IsValidIndex(int index) |
|||
{ |
|||
return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count); |
|||
} |
|||
|
|||
private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged) |
|||
{ |
|||
var selected = new List<IndexRange>(); |
|||
|
|||
SelectedCount += IndexRange.Add(_selected, addRange, selected); |
|||
|
|||
if (selected.Count > 0) |
|||
{ |
|||
_operation?.Selected(selected); |
|||
|
|||
if (_selectedItems != null && ItemsSourceView != null) |
|||
{ |
|||
for (var i = addRange.Begin; i <= addRange.End; ++i) |
|||
{ |
|||
_selectedItems.Add(ItemsSourceView!.GetAt(i)); |
|||
} |
|||
} |
|||
|
|||
if (raiseOnSelectionChanged) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged) |
|||
{ |
|||
var removed = new List<IndexRange>(); |
|||
|
|||
SelectedCount -= IndexRange.Remove(_selected, removeRange, removed); |
|||
|
|||
if (removed.Count > 0) |
|||
{ |
|||
_operation?.Deselected(removed); |
|||
|
|||
if (_selectedItems != null) |
|||
{ |
|||
for (var i = removeRange.Begin; i <= removeRange.End; ++i) |
|||
{ |
|||
_selectedItems.Remove(ItemsSourceView!.GetAt(i)); |
|||
} |
|||
} |
|||
|
|||
if (raiseOnSelectionChanged) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void ClearSelection() |
|||
{ |
|||
// Deselect all items
|
|||
if (_selected.Count > 0) |
|||
{ |
|||
_operation?.Deselected(_selected); |
|||
_selected.Clear(); |
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
_selectedItems?.Clear(); |
|||
SelectedCount = 0; |
|||
AnchorIndex = -1; |
|||
} |
|||
|
|||
private void ClearChildNodes() |
|||
{ |
|||
for (int i = 0; i < _childrenNodes.Count; i++) |
|||
{ |
|||
var child = _childrenNodes[i]; |
|||
|
|||
if (child != null && child != _manager.SharedLeafNode) |
|||
{ |
|||
child.Dispose(); |
|||
_childrenNodes[i] = null; |
|||
} |
|||
} |
|||
|
|||
RealizedChildrenNodeCount = 0; |
|||
} |
|||
|
|||
private bool Select(int index, bool select, bool raiseOnSelectionChanged) |
|||
{ |
|||
if (IsValidIndex(index)) |
|||
{ |
|||
// Ignore duplicate selection calls
|
|||
if (IsSelected(index) == select) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
var range = new IndexRange(index, index); |
|||
|
|||
if (select) |
|||
{ |
|||
AddRange(range, raiseOnSelectionChanged); |
|||
} |
|||
else |
|||
{ |
|||
RemoveRange(range, raiseOnSelectionChanged); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args) |
|||
{ |
|||
bool selectionInvalidated = false; |
|||
List<object?>? removed = null; |
|||
|
|||
switch (args.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
{ |
|||
selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
{ |
|||
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems); |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Reset: |
|||
{ |
|||
if (_selectedItems == null) |
|||
{ |
|||
ClearSelection(); |
|||
} |
|||
else |
|||
{ |
|||
removed = RecreateSelectionFromSelectedItems(); |
|||
} |
|||
|
|||
selectionInvalidated = true; |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Replace: |
|||
{ |
|||
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems); |
|||
selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (selectionInvalidated) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
_manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed); |
|||
} |
|||
|
|||
private bool OnItemsAdded(int index, int count) |
|||
{ |
|||
var selectionInvalidated = false; |
|||
|
|||
// Update ranges for leaf items
|
|||
var toAdd = new List<IndexRange>(); |
|||
|
|||
for (int i = 0; i < _selected.Count; i++) |
|||
{ |
|||
var range = _selected[i]; |
|||
|
|||
// The range is after the inserted items, need to shift the range right
|
|||
if (range.End >= index) |
|||
{ |
|||
int begin = range.Begin; |
|||
|
|||
// If the index left of newIndex is inside the range,
|
|||
// Split the range and remember the left piece to add later
|
|||
if (range.Contains(index - 1)) |
|||
{ |
|||
range.Split(index - 1, out var before, out _); |
|||
toAdd.Add(before); |
|||
begin = index; |
|||
} |
|||
|
|||
// Shift the range to the right
|
|||
_selected[i] = new IndexRange(begin + count, range.End + count); |
|||
selectionInvalidated = true; |
|||
} |
|||
} |
|||
|
|||
// Add the left sides of the split ranges
|
|||
_selected.AddRange(toAdd); |
|||
|
|||
// Update for non-leaf if we are tracking non-leaf nodes
|
|||
if (_childrenNodes.Count > 0) |
|||
{ |
|||
selectionInvalidated = true; |
|||
for (int i = 0; i < count; i++) |
|||
{ |
|||
_childrenNodes.Insert(index, null); |
|||
} |
|||
} |
|||
|
|||
// Adjust the anchor
|
|||
if (AnchorIndex >= index) |
|||
{ |
|||
AnchorIndex += count; |
|||
} |
|||
|
|||
// Check if adding a node invalidated an ancestors
|
|||
// selection state. For example if parent was selected before
|
|||
// adding a new item makes the parent partially selected now.
|
|||
if (!selectionInvalidated) |
|||
{ |
|||
var parent = _parent; |
|||
|
|||
while (parent != null) |
|||
{ |
|||
var isSelected = parent.IsSelectedWithPartial(); |
|||
|
|||
// If a parent is selected, then it will become partially selected.
|
|||
// If it is not selected or partially selected - there is no change.
|
|||
if (isSelected == true) |
|||
{ |
|||
selectionInvalidated = true; |
|||
break; |
|||
} |
|||
|
|||
parent = parent._parent; |
|||
} |
|||
} |
|||
|
|||
return selectionInvalidated; |
|||
} |
|||
|
|||
private (bool, List<object?>) OnItemsRemoved(int index, IList items) |
|||
{ |
|||
var selectionInvalidated = false; |
|||
var removed = new List<object?>(); |
|||
var count = items.Count; |
|||
var isSelected = false; |
|||
|
|||
for (int i = 0; i <= count - 1; i++) |
|||
{ |
|||
if (IsSelected(index + i)) |
|||
{ |
|||
isSelected = true; |
|||
removed.Add(items[i]); |
|||
} |
|||
} |
|||
|
|||
if (isSelected) |
|||
{ |
|||
var removeRange = new IndexRange(index, index + count - 1); |
|||
SelectedCount -= IndexRange.Remove(_selected, removeRange); |
|||
selectionInvalidated = true; |
|||
|
|||
if (_selectedItems != null) |
|||
{ |
|||
foreach (var i in items) |
|||
{ |
|||
_selectedItems.Remove(i); |
|||
} |
|||
} |
|||
} |
|||
|
|||
for (int i = 0; i < _selected.Count; i++) |
|||
{ |
|||
var range = _selected[i]; |
|||
|
|||
// The range is after the removed items, need to shift the range left
|
|||
if (range.End > index) |
|||
{ |
|||
// Shift the range to the left
|
|||
_selected[i] = new IndexRange(range.Begin - count, range.End - count); |
|||
selectionInvalidated = true; |
|||
} |
|||
} |
|||
|
|||
// Update for non-leaf if we are tracking non-leaf nodes
|
|||
if (_childrenNodes.Count > 0) |
|||
{ |
|||
selectionInvalidated = true; |
|||
for (int i = 0; i < count; i++) |
|||
{ |
|||
if (_childrenNodes[index] != null) |
|||
{ |
|||
removed.AddRange(_childrenNodes[index]!.SelectedItems); |
|||
RealizedChildrenNodeCount--; |
|||
_childrenNodes[index]!.Dispose(); |
|||
} |
|||
_childrenNodes.RemoveAt(index); |
|||
} |
|||
} |
|||
|
|||
//Adjust the anchor
|
|||
if (AnchorIndex >= index) |
|||
{ |
|||
AnchorIndex -= count; |
|||
} |
|||
|
|||
return (selectionInvalidated, removed); |
|||
} |
|||
|
|||
private void OnSelectionChanged() |
|||
{ |
|||
_selectedIndicesCacheIsValid = false; |
|||
_selectedIndicesCached.Clear(); |
|||
} |
|||
|
|||
public static bool? ConvertToNullableBool(SelectionState isSelected) |
|||
{ |
|||
bool? result = null; // PartialySelected
|
|||
|
|||
if (isSelected == SelectionState.Selected) |
|||
{ |
|||
result = true; |
|||
} |
|||
else if (isSelected == SelectionState.NotSelected) |
|||
{ |
|||
result = false; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public SelectionState EvaluateIsSelectedBasedOnChildrenNodes() |
|||
{ |
|||
var selectionState = SelectionState.NotSelected; |
|||
int realizedChildrenNodeCount = RealizedChildrenNodeCount; |
|||
int selectedCount = SelectedCount; |
|||
|
|||
if (realizedChildrenNodeCount != 0 || selectedCount != 0) |
|||
{ |
|||
// There are realized children or some selected leaves.
|
|||
int dataCount = DataCount; |
|||
if (realizedChildrenNodeCount == 0 && selectedCount > 0) |
|||
{ |
|||
// All nodes are leaves under it - we didn't create children nodes as an optimization.
|
|||
// See if all/some or none of the leaves are selected.
|
|||
selectionState = dataCount != selectedCount ? |
|||
SelectionState.PartiallySelected : |
|||
dataCount == selectedCount ? SelectionState.Selected : SelectionState.NotSelected; |
|||
} |
|||
else |
|||
{ |
|||
// There are child nodes, walk them individually and evaluate based on each child
|
|||
// being selected/not selected or partially selected.
|
|||
selectedCount = 0; |
|||
int notSelectedCount = 0; |
|||
for (int i = 0; i < ChildrenNodeCount; i++) |
|||
{ |
|||
var child = GetAt(i, realizeChild: false); |
|||
|
|||
if (child != null) |
|||
{ |
|||
// child is realized, ask it.
|
|||
var isChildSelected = IsSelectedWithPartial(i); |
|||
if (isChildSelected == null) |
|||
{ |
|||
selectionState = SelectionState.PartiallySelected; |
|||
break; |
|||
} |
|||
else if (isChildSelected == true) |
|||
{ |
|||
selectedCount++; |
|||
} |
|||
else |
|||
{ |
|||
notSelectedCount++; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// not realized.
|
|||
if (IsSelected(i)) |
|||
{ |
|||
selectedCount++; |
|||
} |
|||
else |
|||
{ |
|||
notSelectedCount++; |
|||
} |
|||
} |
|||
|
|||
if (selectedCount > 0 && notSelectedCount > 0) |
|||
{ |
|||
selectionState = SelectionState.PartiallySelected; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (selectionState != SelectionState.PartiallySelected) |
|||
{ |
|||
if (selectedCount != 0 && selectedCount != dataCount) |
|||
{ |
|||
selectionState = SelectionState.PartiallySelected; |
|||
} |
|||
else |
|||
{ |
|||
selectionState = selectedCount == dataCount ? SelectionState.Selected : SelectionState.NotSelected; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return selectionState; |
|||
} |
|||
|
|||
private void PopulateSelectedItemsFromSelectedIndices() |
|||
{ |
|||
if (_selectedItems != null) |
|||
{ |
|||
_selectedItems.Clear(); |
|||
|
|||
foreach (var i in SelectedIndices) |
|||
{ |
|||
_selectedItems.Add(ItemsSourceView!.GetAt(i)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private List<object?> RecreateSelectionFromSelectedItems() |
|||
{ |
|||
var removed = new List<object?>(); |
|||
|
|||
_selected.Clear(); |
|||
SelectedCount = 0; |
|||
|
|||
for (var i = 0; i < _selectedItems!.Count; ++i) |
|||
{ |
|||
var item = _selectedItems[i]; |
|||
var index = ItemsSourceView!.IndexOf(item); |
|||
|
|||
if (index != -1) |
|||
{ |
|||
IndexRange.Add(_selected, new IndexRange(index, index)); |
|||
++SelectedCount; |
|||
} |
|||
else |
|||
{ |
|||
removed.Add(item); |
|||
_selectedItems.RemoveAt(i--); |
|||
} |
|||
} |
|||
|
|||
return removed; |
|||
} |
|||
|
|||
public enum SelectionState |
|||
{ |
|||
Selected, |
|||
NotSelected, |
|||
PartiallySelected |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,110 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class SelectionNodeOperation : ISelectedItemInfo |
|||
{ |
|||
private readonly SelectionNode _owner; |
|||
private List<IndexRange>? _selected; |
|||
private List<IndexRange>? _deselected; |
|||
private int _selectedCount = -1; |
|||
private int _deselectedCount = -1; |
|||
|
|||
public SelectionNodeOperation(SelectionNode owner) |
|||
{ |
|||
_owner = owner; |
|||
} |
|||
|
|||
public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0; |
|||
public List<IndexRange>? SelectedRanges => _selected; |
|||
public List<IndexRange>? DeselectedRanges => _deselected; |
|||
public IndexPath Path => _owner.IndexPath; |
|||
public ItemsSourceView? Items => _owner.ItemsSourceView; |
|||
|
|||
public int SelectedCount |
|||
{ |
|||
get |
|||
{ |
|||
if (_selectedCount == -1) |
|||
{ |
|||
_selectedCount = (_selected != null) ? IndexRange.GetCount(_selected) : 0; |
|||
} |
|||
|
|||
return _selectedCount; |
|||
} |
|||
} |
|||
|
|||
public int DeselectedCount |
|||
{ |
|||
get |
|||
{ |
|||
if (_deselectedCount == -1) |
|||
{ |
|||
_deselectedCount = (_deselected != null) ? IndexRange.GetCount(_deselected) : 0; |
|||
} |
|||
|
|||
return _deselectedCount; |
|||
} |
|||
} |
|||
|
|||
public void Selected(IndexRange range) |
|||
{ |
|||
Add(range, ref _selected, _deselected); |
|||
_selectedCount = -1; |
|||
} |
|||
|
|||
public void Selected(IEnumerable<IndexRange> ranges) |
|||
{ |
|||
foreach (var range in ranges) |
|||
{ |
|||
Selected(range); |
|||
} |
|||
} |
|||
|
|||
public void Deselected(IndexRange range) |
|||
{ |
|||
Add(range, ref _deselected, _selected); |
|||
_deselectedCount = -1; |
|||
} |
|||
|
|||
public void Deselected(IEnumerable<IndexRange> ranges) |
|||
{ |
|||
foreach (var range in ranges) |
|||
{ |
|||
Deselected(range); |
|||
} |
|||
} |
|||
|
|||
private static void Add( |
|||
IndexRange range, |
|||
ref List<IndexRange>? add, |
|||
List<IndexRange>? remove) |
|||
{ |
|||
if (remove != null) |
|||
{ |
|||
var removed = new List<IndexRange>(); |
|||
IndexRange.Remove(remove, range, removed); |
|||
var selected = IndexRange.Subtract(range, removed); |
|||
|
|||
if (selected.Any()) |
|||
{ |
|||
add ??= new List<IndexRange>(); |
|||
|
|||
foreach (var r in selected) |
|||
{ |
|||
IndexRange.Add(add, r); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
add ??= new List<IndexRange>(); |
|||
IndexRange.Add(add, range); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue