Browse Source
# Conflicts: # src/Skia/Avalonia.Skia/PlatformRenderInterface.cs # src/Skia/Avalonia.Skia/SkiaOptions.cspull/3386/head
161 changed files with 19462 additions and 1437 deletions
@ -1,6 +1,6 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="SkiaSharp" Version="1.68.2" /> |
<PackageReference Include="SkiaSharp" Version="1.68.2.1" /> |
||||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" /> |
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2.1" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -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,848 @@ |
|||||
|
// 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 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 |
||||
|
{ |
||||
|
if (value != null) |
||||
|
{ |
||||
|
SelectionTreeHelper.TraverseIndexPath( |
||||
|
_rootNode, |
||||
|
value, |
||||
|
realizeChildren: true, |
||||
|
(currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_rootNode.AnchorIndex = -1; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
// Raise SelectionChanged event
|
||||
|
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 => |
||||
|
{ |
||||
|
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void BeginOperation() |
||||
|
{ |
||||
|
if (_operationCount++ == 0) |
||||
|
{ |
||||
|
_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); |
||||
|
_rootNode.Cleanup(); |
||||
|
} |
||||
|
|
||||
|
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,966 @@ |
|||||
|
// 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; |
||||
|
|
||||
|
#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 => 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() |
||||
|
{ |
||||
|
foreach (var child in _childrenNodes) |
||||
|
{ |
||||
|
if (child != null && child != _manager.SharedLeafNode) |
||||
|
{ |
||||
|
child.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
// Remove the items from the selection for leaf
|
||||
|
if (ItemsSourceView!.Count > 0) |
||||
|
{ |
||||
|
bool 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; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// No more items in the list, clear
|
||||
|
ClearSelection(); |
||||
|
RealizedChildrenNodeCount = 0; |
||||
|
selectionInvalidated = true; |
||||
|
} |
||||
|
|
||||
|
// Check if removing a node invalidated an ancestors
|
||||
|
// selection state. For example if parent was partially selected before
|
||||
|
// removing an item, it could be selected now.
|
||||
|
if (!selectionInvalidated) |
||||
|
{ |
||||
|
var parent = _parent; |
||||
|
|
||||
|
while (parent != null) |
||||
|
{ |
||||
|
var isSelected = parent.IsSelectedWithPartial(); |
||||
|
// If a parent is partially selected, then it will become selected.
|
||||
|
// If it is selected or not selected - there is no change.
|
||||
|
if (!isSelected.HasValue) |
||||
|
{ |
||||
|
selectionInvalidated = true; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
parent = parent._parent; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,227 @@ |
|||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Specialized; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Collections; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Controls.Utils |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
|
||||
|
/// </summary>
|
||||
|
internal class SelectedItemsSync |
||||
|
{ |
||||
|
private IList? _items; |
||||
|
private bool _updatingItems; |
||||
|
private bool _updatingModel; |
||||
|
|
||||
|
public SelectedItemsSync(ISelectionModel model) |
||||
|
{ |
||||
|
model = model ?? throw new ArgumentNullException(nameof(model)); |
||||
|
Model = model; |
||||
|
} |
||||
|
|
||||
|
public ISelectionModel Model { get; private set; } |
||||
|
|
||||
|
public IList GetOrCreateItems() |
||||
|
{ |
||||
|
if (_items == null) |
||||
|
{ |
||||
|
var items = new AvaloniaList<object>(Model.SelectedItems); |
||||
|
items.CollectionChanged += ItemsCollectionChanged; |
||||
|
Model.SelectionChanged += SelectionModelSelectionChanged; |
||||
|
_items = items; |
||||
|
} |
||||
|
|
||||
|
return _items; |
||||
|
} |
||||
|
|
||||
|
public void SetItems(IList? items) |
||||
|
{ |
||||
|
items ??= new AvaloniaList<object>(); |
||||
|
|
||||
|
if (items.IsFixedSize) |
||||
|
{ |
||||
|
throw new NotSupportedException( |
||||
|
"Cannot assign fixed size selection to SelectedItems."); |
||||
|
} |
||||
|
|
||||
|
if (_items is INotifyCollectionChanged incc) |
||||
|
{ |
||||
|
incc.CollectionChanged -= ItemsCollectionChanged; |
||||
|
} |
||||
|
|
||||
|
if (_items == null) |
||||
|
{ |
||||
|
Model.SelectionChanged += SelectionModelSelectionChanged; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
_updatingModel = true; |
||||
|
_items = items; |
||||
|
|
||||
|
using (Model.Update()) |
||||
|
{ |
||||
|
Model.ClearSelection(); |
||||
|
Add(items); |
||||
|
} |
||||
|
|
||||
|
if (_items is INotifyCollectionChanged incc2) |
||||
|
{ |
||||
|
incc2.CollectionChanged += ItemsCollectionChanged; |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_updatingModel = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void SetModel(ISelectionModel model) |
||||
|
{ |
||||
|
model = model ?? throw new ArgumentNullException(nameof(model)); |
||||
|
|
||||
|
if (_items != null) |
||||
|
{ |
||||
|
Model.SelectionChanged -= SelectionModelSelectionChanged; |
||||
|
Model = model; |
||||
|
Model.SelectionChanged += SelectionModelSelectionChanged; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
_updatingItems = true; |
||||
|
_items.Clear(); |
||||
|
|
||||
|
foreach (var i in model.SelectedItems) |
||||
|
{ |
||||
|
_items.Add(i); |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_updatingItems = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
||||
|
{ |
||||
|
if (_updatingItems) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (_items == null) |
||||
|
{ |
||||
|
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); |
||||
|
} |
||||
|
|
||||
|
void Remove() |
||||
|
{ |
||||
|
foreach (var i in e.OldItems) |
||||
|
{ |
||||
|
var index = IndexOf(Model.Source, i); |
||||
|
|
||||
|
if (index != -1) |
||||
|
{ |
||||
|
Model.Deselect(index); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
using var operation = Model.Update(); |
||||
|
|
||||
|
_updatingModel = true; |
||||
|
|
||||
|
switch (e.Action) |
||||
|
{ |
||||
|
case NotifyCollectionChangedAction.Add: |
||||
|
Add(e.NewItems); |
||||
|
break; |
||||
|
case NotifyCollectionChangedAction.Remove: |
||||
|
Remove(); |
||||
|
break; |
||||
|
case NotifyCollectionChangedAction.Replace: |
||||
|
Remove(); |
||||
|
Add(e.NewItems); |
||||
|
break; |
||||
|
case NotifyCollectionChangedAction.Reset: |
||||
|
Model.ClearSelection(); |
||||
|
Add(_items); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_updatingModel = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void Add(IList newItems) |
||||
|
{ |
||||
|
foreach (var i in newItems) |
||||
|
{ |
||||
|
var index = IndexOf(Model.Source, i); |
||||
|
|
||||
|
if (index != -1) |
||||
|
{ |
||||
|
Model.Select(index); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) |
||||
|
{ |
||||
|
if (_updatingModel) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (_items == null) |
||||
|
{ |
||||
|
throw new AvaloniaInternalException("SelectionModelChanged raised but we don't have items."); |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var deselected = e.DeselectedItems.ToList(); |
||||
|
var selected = e.SelectedItems.ToList(); |
||||
|
|
||||
|
_updatingItems = true; |
||||
|
|
||||
|
foreach (var i in deselected) |
||||
|
{ |
||||
|
_items.Remove(i); |
||||
|
} |
||||
|
|
||||
|
foreach (var i in selected) |
||||
|
{ |
||||
|
_items.Add(i); |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_updatingItems = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static int IndexOf(object source, object item) |
||||
|
{ |
||||
|
if (source is IList l) |
||||
|
{ |
||||
|
return l.IndexOf(item); |
||||
|
} |
||||
|
else if (source is ItemsSourceView v) |
||||
|
{ |
||||
|
return v.IndexOf(item); |
||||
|
} |
||||
|
|
||||
|
return -1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,189 @@ |
|||||
|
// 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.Utils |
||||
|
{ |
||||
|
internal static class SelectionTreeHelper |
||||
|
{ |
||||
|
public static void TraverseIndexPath( |
||||
|
SelectionNode root, |
||||
|
IndexPath path, |
||||
|
bool realizeChildren, |
||||
|
Action<SelectionNode, IndexPath, int, int> nodeAction) |
||||
|
{ |
||||
|
var node = root; |
||||
|
|
||||
|
for (int depth = 0; depth < path.GetSize(); depth++) |
||||
|
{ |
||||
|
int childIndex = path.GetAt(depth); |
||||
|
nodeAction(node, path, depth, childIndex); |
||||
|
|
||||
|
if (depth < path.GetSize() - 1) |
||||
|
{ |
||||
|
node = node.GetAt(childIndex, realizeChildren)!; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void Traverse( |
||||
|
SelectionNode root, |
||||
|
bool realizeChildren, |
||||
|
Action<TreeWalkNodeInfo> nodeAction) |
||||
|
{ |
||||
|
var pendingNodes = new List<TreeWalkNodeInfo>(); |
||||
|
var current = new IndexPath(null); |
||||
|
|
||||
|
pendingNodes.Add(new TreeWalkNodeInfo(root, current)); |
||||
|
|
||||
|
while (pendingNodes.Count > 0) |
||||
|
{ |
||||
|
var nextNode = pendingNodes.Last(); |
||||
|
pendingNodes.RemoveAt(pendingNodes.Count - 1); |
||||
|
int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount; |
||||
|
for (int i = count - 1; i >= 0; i--) |
||||
|
{ |
||||
|
var child = nextNode.Node.GetAt(i, realizeChildren); |
||||
|
var childPath = nextNode.Path.CloneWithChildIndex(i); |
||||
|
if (child != null) |
||||
|
{ |
||||
|
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, nextNode.Node)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Queue the children first and then perform the action. This way
|
||||
|
// the action can remove the children in the action if necessary
|
||||
|
nodeAction(nextNode); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void TraverseRangeRealizeChildren( |
||||
|
SelectionNode root, |
||||
|
IndexPath start, |
||||
|
IndexPath end, |
||||
|
Action<TreeWalkNodeInfo> nodeAction) |
||||
|
{ |
||||
|
var pendingNodes = new List<TreeWalkNodeInfo>(); |
||||
|
var current = start; |
||||
|
|
||||
|
// Build up the stack to account for the depth first walk up to the
|
||||
|
// start index path.
|
||||
|
TraverseIndexPath( |
||||
|
root, |
||||
|
start, |
||||
|
true, |
||||
|
(node, path, depth, childIndex) => |
||||
|
{ |
||||
|
var currentPath = StartPath(path, depth); |
||||
|
bool isStartPath = IsSubSet(start, currentPath); |
||||
|
bool isEndPath = IsSubSet(end, currentPath); |
||||
|
|
||||
|
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0; |
||||
|
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : node.DataCount - 1; |
||||
|
|
||||
|
for (int i = endIndex; i >= startIndex; i--) |
||||
|
{ |
||||
|
var child = node.GetAt(i, realizeChild: true); |
||||
|
if (child != null) |
||||
|
{ |
||||
|
var childPath = currentPath.CloneWithChildIndex(i); |
||||
|
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, node)); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// From the start index path, do a depth first walk as long as the
|
||||
|
// current path is less than the end path.
|
||||
|
while (pendingNodes.Count > 0) |
||||
|
{ |
||||
|
var info = pendingNodes.Last(); |
||||
|
pendingNodes.RemoveAt(pendingNodes.Count - 1); |
||||
|
int depth = info.Path.GetSize(); |
||||
|
bool isStartPath = IsSubSet(start, info.Path); |
||||
|
bool isEndPath = IsSubSet(end, info.Path); |
||||
|
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0; |
||||
|
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1; |
||||
|
for (int i = endIndex; i >= startIndex; i--) |
||||
|
{ |
||||
|
var child = info.Node.GetAt(i, realizeChild: true); |
||||
|
if (child != null) |
||||
|
{ |
||||
|
var childPath = info.Path.CloneWithChildIndex(i); |
||||
|
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, info.Node)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
nodeAction(info); |
||||
|
|
||||
|
if (info.Path.CompareTo(end) == 0) |
||||
|
{ |
||||
|
// We reached the end index path. stop iterating.
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static bool IsSubSet(IndexPath path, IndexPath subset) |
||||
|
{ |
||||
|
var subsetSize = subset.GetSize(); |
||||
|
if (path.GetSize() < subsetSize) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < subsetSize; i++) |
||||
|
{ |
||||
|
if (path.GetAt(i) != subset.GetAt(i)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private static IndexPath StartPath(IndexPath path, int length) |
||||
|
{ |
||||
|
var subPath = new List<int>(); |
||||
|
for (int i = 0; i < length; i++) |
||||
|
{ |
||||
|
subPath.Add(path.GetAt(i)); |
||||
|
} |
||||
|
|
||||
|
return new IndexPath(subPath); |
||||
|
} |
||||
|
|
||||
|
public struct TreeWalkNodeInfo |
||||
|
{ |
||||
|
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode? parent) |
||||
|
{ |
||||
|
node = node ?? throw new ArgumentNullException(nameof(node)); |
||||
|
|
||||
|
Node = node; |
||||
|
Path = indexPath; |
||||
|
ParentNode = parent; |
||||
|
} |
||||
|
|
||||
|
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath) |
||||
|
{ |
||||
|
node = node ?? throw new ArgumentNullException(nameof(node)); |
||||
|
|
||||
|
Node = node; |
||||
|
Path = indexPath; |
||||
|
ParentNode = null; |
||||
|
} |
||||
|
|
||||
|
public SelectionNode Node { get; } |
||||
|
public IndexPath Path { get; } |
||||
|
public SelectionNode? ParentNode { get; } |
||||
|
}; |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Remote.Protocol; |
||||
|
using Avalonia.Remote.Protocol.Designer; |
||||
|
using Avalonia.Threading; |
||||
|
|
||||
|
namespace Avalonia.DesignerSupport.Remote |
||||
|
{ |
||||
|
class FileWatcherTransport : IAvaloniaRemoteTransportConnection, ITransportWithEnforcedMethod |
||||
|
{ |
||||
|
private string _path; |
||||
|
private string _lastContents; |
||||
|
private bool _disposed; |
||||
|
|
||||
|
public FileWatcherTransport(Uri file) |
||||
|
{ |
||||
|
_path = file.LocalPath; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_disposed = true; |
||||
|
} |
||||
|
|
||||
|
void Dump(object o, string pad) |
||||
|
{ |
||||
|
foreach (var p in o.GetType().GetProperties()) |
||||
|
{ |
||||
|
Console.Write($"{pad}{p.Name}: "); |
||||
|
var v = p.GetValue(o); |
||||
|
if (v == null || v.GetType().IsPrimitive || v is string || v is Guid) |
||||
|
Console.WriteLine(v); |
||||
|
else |
||||
|
{ |
||||
|
Console.WriteLine(); |
||||
|
Dump(v, pad + " "); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public Task Send(object data) |
||||
|
{ |
||||
|
Console.WriteLine(data.GetType().Name); |
||||
|
Dump(data, " "); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage; |
||||
|
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage |
||||
|
{ |
||||
|
add |
||||
|
{ |
||||
|
_onMessage+=value; |
||||
|
} |
||||
|
remove { _onMessage -= value; } |
||||
|
} |
||||
|
|
||||
|
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException; |
||||
|
public void Start() |
||||
|
{ |
||||
|
UpdaterThread(); |
||||
|
} |
||||
|
|
||||
|
// I couldn't get FileSystemWatcher working on Linux, so I came up with this abomination
|
||||
|
async void UpdaterThread() |
||||
|
{ |
||||
|
while (!_disposed) |
||||
|
{ |
||||
|
var data = File.ReadAllText(_path); |
||||
|
if (data != _lastContents) |
||||
|
{ |
||||
|
Console.WriteLine("Triggering XAML update"); |
||||
|
_lastContents = data; |
||||
|
_onMessage?.Invoke(this, new UpdateXamlMessage { Xaml = data }); |
||||
|
} |
||||
|
|
||||
|
await Task.Delay(100); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public string PreviewerMethod => RemoteDesignerEntryPoint.Methods.Html; |
||||
|
} |
||||
|
|
||||
|
interface ITransportWithEnforcedMethod |
||||
|
{ |
||||
|
string PreviewerMethod { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,266 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.IO.Compression; |
||||
|
using System.Linq; |
||||
|
using System.Net; |
||||
|
using System.Text; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Remote.Protocol; |
||||
|
using Avalonia.Remote.Protocol.Viewport; |
||||
|
|
||||
|
namespace Avalonia.DesignerSupport.Remote.HtmlTransport |
||||
|
{ |
||||
|
public class HtmlWebSocketTransport : IAvaloniaRemoteTransportConnection |
||||
|
{ |
||||
|
private readonly IAvaloniaRemoteTransportConnection _signalTransport; |
||||
|
private readonly SimpleWebSocketHttpServer _simpleServer; |
||||
|
private readonly Dictionary<string, byte[]> _resources; |
||||
|
private SimpleWebSocket _pendingSocket; |
||||
|
private bool _disposed; |
||||
|
private object _lock = new object(); |
||||
|
private AutoResetEvent _wakeup = new AutoResetEvent(false); |
||||
|
private FrameMessage _lastFrameMessage = null; |
||||
|
private FrameMessage _lastSentFrameMessage = null; |
||||
|
private RequestViewportResizeMessage _lastViewportRequest; |
||||
|
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage; |
||||
|
private Action<IAvaloniaRemoteTransportConnection, Exception> _onException; |
||||
|
|
||||
|
private static readonly Dictionary<string, string> Mime = new Dictionary<string, string> |
||||
|
{ |
||||
|
["html"] = "text/html", ["htm"] = "text/html", ["js"] = "text/javascript", ["css"] = "text/css" |
||||
|
}; |
||||
|
|
||||
|
private static readonly byte[] NotFound = Encoding.UTF8.GetBytes("404 - Not Found"); |
||||
|
|
||||
|
|
||||
|
public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri) |
||||
|
{ |
||||
|
if (listenUri.Scheme != "http") |
||||
|
throw new ArgumentException("listenUri"); |
||||
|
|
||||
|
var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build."; |
||||
|
_resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames() |
||||
|
.Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary( |
||||
|
r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3), |
||||
|
r => |
||||
|
{ |
||||
|
|
||||
|
using (var s = |
||||
|
new GZipStream(typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceStream(r), |
||||
|
CompressionMode.Decompress)) |
||||
|
{ |
||||
|
var ms = new MemoryStream(); |
||||
|
s.CopyTo(ms); |
||||
|
return ms.ToArray(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
_signalTransport = signalTransport; |
||||
|
var address = IPAddress.Parse(listenUri.Host); |
||||
|
|
||||
|
_simpleServer = new SimpleWebSocketHttpServer(address, listenUri.Port); |
||||
|
_simpleServer.Listen(); |
||||
|
Task.Run(AcceptWorker); |
||||
|
Task.Run(SocketWorker); |
||||
|
_signalTransport.Send(new HtmlTransportStartedMessage { Uri = "http://" + address + ":" + listenUri.Port + "/" }); |
||||
|
} |
||||
|
|
||||
|
async void AcceptWorker() |
||||
|
{ |
||||
|
while (true) |
||||
|
{ |
||||
|
|
||||
|
using (var req = await _simpleServer.AcceptAsync()) |
||||
|
{ |
||||
|
|
||||
|
if (!req.IsWebsocketRequest) |
||||
|
{ |
||||
|
|
||||
|
var key = req.Path == "/" ? "index.html" : req.Path.TrimStart('/').Replace('/', '.'); |
||||
|
if (_resources.TryGetValue(key, out var data)) |
||||
|
{ |
||||
|
var ext = Path.GetExtension(key).Substring(1); |
||||
|
string mime = null; |
||||
|
if (ext == null || !Mime.TryGetValue(ext, out mime)) |
||||
|
mime = "application/octet-stream"; |
||||
|
await req.RespondAsync(200, data, mime); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
await req.RespondAsync(404, NotFound, "text/plain"); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var socket = await req.AcceptWebSocket(); |
||||
|
SocketReceiveWorker(socket); |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
_pendingSocket?.Dispose(); |
||||
|
_pendingSocket = socket; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async void SocketReceiveWorker(SimpleWebSocket socket) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
while (true) |
||||
|
{ |
||||
|
var msg = await socket.ReceiveMessage().ConfigureAwait(false); |
||||
|
if(msg == null) |
||||
|
return; |
||||
|
if (msg.IsText) |
||||
|
{ |
||||
|
var s = Encoding.UTF8.GetString(msg.Data); |
||||
|
var parts = s.Split(':'); |
||||
|
if (parts[0] == "frame-received") |
||||
|
_onMessage?.Invoke(this, new FrameReceivedMessage { SequenceId = long.Parse(parts[1]) }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
catch(Exception e) |
||||
|
{ |
||||
|
Console.Error.WriteLine(e.ToString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async void SocketWorker() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
SimpleWebSocket socket = null; |
||||
|
while (true) |
||||
|
{ |
||||
|
if (_disposed) |
||||
|
{ |
||||
|
socket?.Dispose(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
FrameMessage sendNow = null; |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
if (_pendingSocket != null) |
||||
|
{ |
||||
|
socket?.Dispose(); |
||||
|
socket = _pendingSocket; |
||||
|
_pendingSocket = null; |
||||
|
_lastSentFrameMessage = null; |
||||
|
} |
||||
|
|
||||
|
if (_lastFrameMessage != _lastSentFrameMessage) |
||||
|
_lastSentFrameMessage = sendNow = _lastFrameMessage; |
||||
|
} |
||||
|
|
||||
|
if (sendNow != null && socket != null) |
||||
|
{ |
||||
|
await socket.SendMessage( |
||||
|
$"frame:{sendNow.SequenceId}:{sendNow.Width}:{sendNow.Height}:{sendNow.Stride}:{sendNow.DpiX}:{sendNow.DpiY}"); |
||||
|
await socket.SendMessage(false, sendNow.Data); |
||||
|
} |
||||
|
|
||||
|
_wakeup.WaitOne(TimeSpan.FromSeconds(1)); |
||||
|
} |
||||
|
} |
||||
|
catch(Exception e) |
||||
|
{ |
||||
|
Console.Error.WriteLine(e.ToString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_pendingSocket?.Dispose(); |
||||
|
_simpleServer.Dispose(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public Task Send(object data) |
||||
|
{ |
||||
|
if (data is FrameMessage frame) |
||||
|
{ |
||||
|
_lastFrameMessage = frame; |
||||
|
_wakeup.Set(); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
if (data is RequestViewportResizeMessage req) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
return _signalTransport.Send(data); |
||||
|
} |
||||
|
|
||||
|
public void Start() |
||||
|
{ |
||||
|
_onMessage?.Invoke(this, new Avalonia.Remote.Protocol.Viewport.ClientSupportedPixelFormatsMessage |
||||
|
{ |
||||
|
Formats = new []{PixelFormat.Rgba8888} |
||||
|
}); |
||||
|
_signalTransport.Start(); |
||||
|
} |
||||
|
|
||||
|
#region Forward
|
||||
|
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage |
||||
|
{ |
||||
|
add |
||||
|
{ |
||||
|
bool subscribeToInner; |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
subscribeToInner = _onMessage == null; |
||||
|
_onMessage += value; |
||||
|
} |
||||
|
|
||||
|
if (subscribeToInner) |
||||
|
_signalTransport.OnMessage += OnSignalTransportMessage; |
||||
|
} |
||||
|
remove |
||||
|
{ |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
_onMessage -= value; |
||||
|
if (_onMessage == null) |
||||
|
_signalTransport.OnMessage -= OnSignalTransportMessage; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnSignalTransportMessage(IAvaloniaRemoteTransportConnection signal, object message) => _onMessage?.Invoke(this, message); |
||||
|
|
||||
|
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException |
||||
|
{ |
||||
|
add |
||||
|
{ |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
var subscribeToInner = _onException == null; |
||||
|
_onException += value; |
||||
|
if (subscribeToInner) |
||||
|
_signalTransport.OnException += OnSignalTransportException; |
||||
|
} |
||||
|
} |
||||
|
remove |
||||
|
{ |
||||
|
lock (_lock) |
||||
|
{ |
||||
|
_onException -= value; |
||||
|
if(_onException==null) |
||||
|
_signalTransport.OnException -= OnSignalTransportException; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnSignalTransportException(IAvaloniaRemoteTransportConnection arg1, Exception ex) |
||||
|
{ |
||||
|
_onException?.Invoke(this, ex); |
||||
|
} |
||||
|
#endregion
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,472 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Net; |
||||
|
using System.Net.Sockets; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Security.Cryptography; |
||||
|
using System.Text; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.DesignerSupport.Remote.HtmlTransport |
||||
|
{ |
||||
|
public class SimpleWebSocketHttpServer : IDisposable |
||||
|
{ |
||||
|
private readonly IPAddress _address; |
||||
|
private readonly int _port; |
||||
|
private TcpListener _listener; |
||||
|
|
||||
|
public async Task<SimpleWebSocketHttpRequest> AcceptAsync() |
||||
|
{ |
||||
|
while (true) |
||||
|
{ |
||||
|
var res = await AcceptAsyncImpl(); |
||||
|
if (res != null) |
||||
|
return res; |
||||
|
} |
||||
|
} |
||||
|
async Task<SimpleWebSocketHttpRequest> AcceptAsyncImpl() |
||||
|
{ |
||||
|
if (_listener == null) |
||||
|
throw new InvalidOperationException("Currently not listening"); |
||||
|
var socket = await _listener.AcceptSocketAsync(); |
||||
|
var stream = new NetworkStream(socket); |
||||
|
bool error = true; |
||||
|
async Task<string> ReadLineAsync() |
||||
|
{ |
||||
|
var readBuffer = new byte[1]; |
||||
|
var lineBuffer = new byte[1024]; |
||||
|
for (var c = 0; c < 1024; c++) |
||||
|
{ |
||||
|
if (await stream.ReadAsync(readBuffer, 0, 1) == 0) |
||||
|
throw new EndOfStreamException(); |
||||
|
if (readBuffer[0] == 10) |
||||
|
{ |
||||
|
if (c == 0) |
||||
|
return ""; |
||||
|
if (lineBuffer[c - 1] == 13) |
||||
|
c--; |
||||
|
if (c == 0) |
||||
|
return ""; |
||||
|
|
||||
|
return Encoding.UTF8.GetString(lineBuffer, 0, c); |
||||
|
} |
||||
|
lineBuffer[c] = readBuffer[0]; |
||||
|
} |
||||
|
|
||||
|
throw new InvalidDataException("Header is too large"); |
||||
|
} |
||||
|
|
||||
|
var headers = new Dictionary<string, string>(); |
||||
|
string line = null; |
||||
|
try |
||||
|
{ |
||||
|
|
||||
|
line = await ReadLineAsync(); |
||||
|
var sp = line.Split(' '); |
||||
|
if (sp.Length != 3 || !sp[2].StartsWith("HTTP") || sp[0] != "GET") |
||||
|
return null; |
||||
|
var path = sp[1]; |
||||
|
|
||||
|
while (true) |
||||
|
{ |
||||
|
line = await ReadLineAsync(); |
||||
|
if (line == "") |
||||
|
break; |
||||
|
sp = line.Split(new[] {':'}, 2); |
||||
|
headers[sp[0]] = sp[1].TrimStart(); |
||||
|
} |
||||
|
|
||||
|
error = false; |
||||
|
|
||||
|
return new SimpleWebSocketHttpRequest(stream, path, headers); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
error = true; |
||||
|
return null; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
if (error) |
||||
|
stream.Dispose(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public void Listen() |
||||
|
{ |
||||
|
var listener = new TcpListener(_address, _port); |
||||
|
listener.Start(); |
||||
|
_listener = listener; |
||||
|
} |
||||
|
|
||||
|
public SimpleWebSocketHttpServer(IPAddress address, int port) |
||||
|
{ |
||||
|
_address = address; |
||||
|
_port = port; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_listener?.Stop(); |
||||
|
_listener = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public class SimpleWebSocketHttpRequest : IDisposable |
||||
|
{ |
||||
|
public Dictionary<string, string> Headers { get; } |
||||
|
public string Path { get; } |
||||
|
private NetworkStream _stream; |
||||
|
public bool IsWebsocketRequest { get; } |
||||
|
public IReadOnlyList<string> WebSocketProtocols { get; } |
||||
|
private string _websocketKey; |
||||
|
|
||||
|
public SimpleWebSocketHttpRequest(NetworkStream stream, string path, Dictionary<string, string> headers) |
||||
|
{ |
||||
|
Path = path; |
||||
|
Headers = headers; |
||||
|
|
||||
|
_stream = stream; |
||||
|
if (headers.TryGetValue("Connection", out var h) |
||||
|
&& h.Contains("Upgrade") |
||||
|
&& headers.TryGetValue("Upgrade", out h) && |
||||
|
h == "websocket" |
||||
|
&& headers.TryGetValue("Sec-WebSocket-Key", out _websocketKey)) |
||||
|
{ |
||||
|
IsWebsocketRequest = true; |
||||
|
if (headers.TryGetValue("Sec-WebSocket-Protocol", out h)) |
||||
|
WebSocketProtocols = h.Split(',').Select(x => x.Trim()).ToArray(); |
||||
|
else WebSocketProtocols = new string[0]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task RespondAsync(int code, byte[] data, string contentType) |
||||
|
{ |
||||
|
var headers = Encoding.UTF8.GetBytes($"HTTP/1.1 {code} {(HttpStatusCode)code}\r\nConnection: close\r\nContent-Type: {contentType}\r\nContent-Length: {data.Length}\r\n\r\n"); |
||||
|
await _stream.WriteAsync(headers, 0, headers.Length); |
||||
|
await _stream.WriteAsync(data, 0, data.Length); |
||||
|
_stream.Dispose(); |
||||
|
_stream = null; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
public async Task<SimpleWebSocket> AcceptWebSocket(string protocol = null) |
||||
|
{ |
||||
|
|
||||
|
var handshakeSource = _websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; |
||||
|
string handshake; |
||||
|
using (var sha1 = SHA1.Create()) |
||||
|
handshake = Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(handshakeSource))); |
||||
|
var headers = |
||||
|
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " |
||||
|
+ handshake + "\r\n"; |
||||
|
if (protocol != null) |
||||
|
headers += protocol + "\r\n"; |
||||
|
headers += "\r\n"; |
||||
|
var bheaders = Encoding.UTF8.GetBytes(headers); |
||||
|
await _stream.WriteAsync(bheaders, 0, bheaders.Length); |
||||
|
|
||||
|
var s = _stream; |
||||
|
_stream = null; |
||||
|
return new SimpleWebSocket(s); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() => _stream?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
public class SimpleWebSocket : IDisposable |
||||
|
{ |
||||
|
class AsyncLock |
||||
|
{ |
||||
|
private object _syncRoot = new object(); |
||||
|
private Queue<TaskCompletionSource<IDisposable>> _queue = new Queue<TaskCompletionSource<IDisposable>>(); |
||||
|
private bool _locked; |
||||
|
public Task<IDisposable> LockAsync() |
||||
|
{ |
||||
|
lock (_syncRoot) |
||||
|
{ |
||||
|
if (!_locked) |
||||
|
{ |
||||
|
_locked = true; |
||||
|
return Task.FromResult<IDisposable>(new Lock(this)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var tcs = new TaskCompletionSource<IDisposable>(); |
||||
|
_queue.Enqueue(tcs); |
||||
|
return tcs.Task; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void Unlock() |
||||
|
{ |
||||
|
lock (_syncRoot) |
||||
|
{ |
||||
|
if (_queue.Count != 0) |
||||
|
_queue.Dequeue().SetResult(new Lock(this)); |
||||
|
else |
||||
|
_locked = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class Lock : IDisposable |
||||
|
{ |
||||
|
private AsyncLock _parent; |
||||
|
private object _syncRoot = new object(); |
||||
|
|
||||
|
public Lock(AsyncLock parent) |
||||
|
{ |
||||
|
_parent = parent; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
lock (_syncRoot) |
||||
|
{ |
||||
|
if (_parent == null) |
||||
|
return; |
||||
|
var p = _parent; |
||||
|
_parent = null; |
||||
|
p.Unlock(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Stream _stream; |
||||
|
private AsyncLock _sendLock = new AsyncLock(); |
||||
|
private AsyncLock _recvLock = new AsyncLock(); |
||||
|
private const int WebsocketInitialHeaderLength = 2; |
||||
|
private const int WebsocketLen16Length = 4; |
||||
|
private const int WebsocketLen64Length = 10; |
||||
|
|
||||
|
private const int WebsocketLen16Code = 126; |
||||
|
private const int WebsocketLen64Code = 127; |
||||
|
|
||||
|
[StructLayout(LayoutKind.Explicit)] |
||||
|
struct WebSocketHeader |
||||
|
{ |
||||
|
[FieldOffset(0)] public byte Mask; |
||||
|
[FieldOffset(1)] public byte Length8; |
||||
|
[FieldOffset(2)] public ushort Length16; |
||||
|
[FieldOffset(2)] public ulong Length64; |
||||
|
} |
||||
|
|
||||
|
readonly byte[] _sendHeaderBuffer = new byte[10]; |
||||
|
readonly MemoryStream _receiveFrameStream = new MemoryStream(); |
||||
|
readonly MemoryStream _receiveMessageStream = new MemoryStream(); |
||||
|
private FrameType _currentMessageFrameType; |
||||
|
|
||||
|
enum FrameType |
||||
|
{ |
||||
|
Continue = 0x0, |
||||
|
Text = 0x1, |
||||
|
Binary = 0x2, |
||||
|
Close = 0x8, |
||||
|
Ping = 0x9, |
||||
|
Pong = 0xA |
||||
|
} |
||||
|
|
||||
|
internal SimpleWebSocket(Stream stream) |
||||
|
{ |
||||
|
_stream = stream; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_stream?.Dispose(); |
||||
|
_stream = null; |
||||
|
} |
||||
|
|
||||
|
public Task SendMessage(string text) |
||||
|
{ |
||||
|
var data = Encoding.UTF8.GetBytes(text); |
||||
|
return SendMessage(true, data); |
||||
|
} |
||||
|
public Task SendMessage(bool isText, byte[] data) => SendMessage(isText, data, 0, data.Length); |
||||
|
|
||||
|
|
||||
|
public Task SendMessage(bool isText, byte[] data, int offset, int length) |
||||
|
=> SendFrame(isText ? FrameType.Text : FrameType.Binary, data, offset, length); |
||||
|
|
||||
|
async Task SendFrame(FrameType type, byte[] data, int offset, int length) |
||||
|
{ |
||||
|
using (var l = await _sendLock.LockAsync()) |
||||
|
{ |
||||
|
var header = new WebSocketHeader(); |
||||
|
|
||||
|
int headerLength; |
||||
|
if (data.Length <= 125) |
||||
|
{ |
||||
|
headerLength = WebsocketInitialHeaderLength; |
||||
|
header.Length8 = (byte) length; |
||||
|
} |
||||
|
else if (length <= 0xffff) |
||||
|
{ |
||||
|
headerLength = WebsocketLen16Length; |
||||
|
header.Length8 = WebsocketLen16Code; |
||||
|
header.Length16 = (ushort) IPAddress.HostToNetworkOrder((short) (ushort) length); |
||||
|
|
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
headerLength = WebsocketLen64Length; |
||||
|
header.Length8 = WebsocketLen64Code; |
||||
|
header.Length64 = (ulong) IPAddress.HostToNetworkOrder((long) length); |
||||
|
} |
||||
|
|
||||
|
var endOfMessage = true; |
||||
|
header.Mask = (byte) (((endOfMessage ? 1u : 0u) << 7) | ((byte) (type) & 0xf)); |
||||
|
unsafe |
||||
|
{ |
||||
|
Marshal.Copy(new IntPtr(&header), _sendHeaderBuffer, 0, headerLength); |
||||
|
} |
||||
|
|
||||
|
await _stream.WriteAsync(_sendHeaderBuffer, 0, headerLength); |
||||
|
await _stream.WriteAsync(data, offset, length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
struct Frame |
||||
|
{ |
||||
|
public byte[] Data; |
||||
|
public bool EndOfMessage; |
||||
|
public FrameType FrameType; |
||||
|
} |
||||
|
|
||||
|
byte[] _recvHeaderBuffer = new byte[8]; |
||||
|
byte[] _maskBuffer = new byte[4]; |
||||
|
async Task<Frame> ReadFrame() |
||||
|
{ |
||||
|
_receiveFrameStream.Position = 0; |
||||
|
_receiveFrameStream.SetLength(0); |
||||
|
await ReadExact(_stream, _recvHeaderBuffer, 0, 2); |
||||
|
var masked = (_recvHeaderBuffer[1] & 0x80) != 0; |
||||
|
var len0 = (_recvHeaderBuffer[1] & 0x7F); |
||||
|
var endOfMessage = (_recvHeaderBuffer[0] & 0x80) != 0; |
||||
|
var frameType = (FrameType) (_recvHeaderBuffer[0] & 0xf); |
||||
|
int length; |
||||
|
if (len0 <= 125) |
||||
|
length = len0; |
||||
|
else if (len0 == WebsocketLen16Code) |
||||
|
{ |
||||
|
await ReadExact(_stream, _recvHeaderBuffer, 0, 2); |
||||
|
length = (ushort) IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_recvHeaderBuffer, 0)); |
||||
|
} |
||||
|
|
||||
|
else |
||||
|
{ |
||||
|
await ReadExact(_stream, _recvHeaderBuffer, 0, 8); |
||||
|
length = (int) (ulong) IPAddress.NetworkToHostOrder((long) BitConverter.ToUInt64(_recvHeaderBuffer, 0)); |
||||
|
} |
||||
|
|
||||
|
if (masked) |
||||
|
await ReadExact(_stream, _maskBuffer, 0, 4); |
||||
|
await ReadExact(_stream, _receiveFrameStream, length); |
||||
|
var data = _receiveFrameStream.ToArray(); |
||||
|
if(masked) |
||||
|
for (var c = 0; c < data.Length; c++) |
||||
|
data[c] = (byte) (data[c] ^ _maskBuffer[c % 4]); |
||||
|
|
||||
|
return new Frame |
||||
|
{ |
||||
|
Data = data, |
||||
|
EndOfMessage = endOfMessage, |
||||
|
FrameType = frameType |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public async Task<SimpleWebSocketMessage> ReceiveMessage() |
||||
|
{ |
||||
|
using (await _recvLock.LockAsync()) |
||||
|
{ |
||||
|
while (true) |
||||
|
{ |
||||
|
var frame = await ReadFrame(); |
||||
|
|
||||
|
if (frame.FrameType == FrameType.Close) |
||||
|
return null; |
||||
|
if (frame.FrameType == FrameType.Ping) |
||||
|
await SendFrame(FrameType.Pong, frame.Data, 0, frame.Data.Length); |
||||
|
if (frame.FrameType == FrameType.Text || frame.FrameType == FrameType.Binary) |
||||
|
{ |
||||
|
var isText = frame.FrameType == FrameType.Text; |
||||
|
if (_receiveMessageStream.Length == 0 && frame.EndOfMessage) |
||||
|
return new SimpleWebSocketMessage |
||||
|
{ |
||||
|
IsText = isText, |
||||
|
Data = frame.Data |
||||
|
}; |
||||
|
|
||||
|
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length); |
||||
|
_currentMessageFrameType = frame.FrameType; |
||||
|
} |
||||
|
if (frame.FrameType == FrameType.Continue) |
||||
|
{ |
||||
|
frame.FrameType = _currentMessageFrameType; |
||||
|
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length); |
||||
|
if (frame.EndOfMessage) |
||||
|
{ |
||||
|
var isText = frame.FrameType == FrameType.Text; |
||||
|
var data = _receiveMessageStream.ToArray(); |
||||
|
_receiveMessageStream.Position = 0; |
||||
|
_receiveMessageStream.SetLength(0); |
||||
|
return new SimpleWebSocketMessage |
||||
|
{ |
||||
|
IsText = isText, |
||||
|
Data = data |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
byte[] _readExactBuffer = new byte[4096]; |
||||
|
async Task ReadExact(Stream from, MemoryStream to, int length) |
||||
|
{ |
||||
|
while (length>0) |
||||
|
{ |
||||
|
var toRead = Math.Min(length, _readExactBuffer.Length); |
||||
|
var read = await from.ReadAsync(_readExactBuffer, 0, toRead); |
||||
|
to.Write(_readExactBuffer, 0, read); |
||||
|
if (read <= 0) |
||||
|
throw new EndOfStreamException(); |
||||
|
length -= read; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async Task ReadExact(Stream from, byte[] to, int offset, int length) |
||||
|
{ |
||||
|
while (length > 0) |
||||
|
{ |
||||
|
var read = await from.ReadAsync(to, offset, length); |
||||
|
if (read <= 0) |
||||
|
throw new EndOfStreamException(); |
||||
|
length -= read; |
||||
|
offset += read; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class SimpleWebSocketMessage |
||||
|
{ |
||||
|
public bool IsText { get; set; } |
||||
|
public byte[] Data { get; set; } |
||||
|
|
||||
|
public string AsString() |
||||
|
{ |
||||
|
return Encoding.UTF8.GetString(Data); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
build |
||||
|
node_modules |
||||
File diff suppressed because it is too large
@ -0,0 +1,41 @@ |
|||||
|
{ |
||||
|
"name": "simple", |
||||
|
"version": "1.0.0", |
||||
|
"description": "", |
||||
|
"main": "index.js", |
||||
|
"scripts": { |
||||
|
"webpack-ver": "cross-env NODE_ENV=production webpack --version", |
||||
|
"dist": "cross-env NODE_ENV=production webpack --display-error-details", |
||||
|
"watch": "cross-env NODE_ENV=development webpack --watch --display-error-details" |
||||
|
}, |
||||
|
"author": "", |
||||
|
"license": "ISC", |
||||
|
"devDependencies": { |
||||
|
"awesome-typescript-loader": "^5.0.0", |
||||
|
"clean-webpack-plugin": "^0.1.19", |
||||
|
"compression-webpack-plugin": "^2.0.0", |
||||
|
"copy-webpack-plugin": "^4.6.0", |
||||
|
"cross-env": "^5.1.6", |
||||
|
"css-loader": "^1.0.0", |
||||
|
"file-loader": "^1.1.11", |
||||
|
"html-webpack-plugin": "^3.2.0", |
||||
|
"mini-css-extract-plugin": "^0.4.1", |
||||
|
"source-map-loader": "^0.2.3", |
||||
|
"style-loader": "^0.21.0", |
||||
|
"to-string-loader": "^1.1.5", |
||||
|
"tsconfig-paths-webpack-plugin": "^3.2.0", |
||||
|
"typescript": "^2.9.2", |
||||
|
"url-loader": "^1.0.1", |
||||
|
"webpack": "~4.16.3", |
||||
|
"webpack-cli": "~2.1.3", |
||||
|
"webpack-livereload-plugin": "~2.1.1" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@types/react": "^16.3.14", |
||||
|
"@types/react-dom": "^16.0.5", |
||||
|
"mobx": "4.3.0", |
||||
|
"mobx-react": "^5.1.2", |
||||
|
"react": "^16.3.2", |
||||
|
"react-dom": "^16.3.2" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
import {PreviewerFrame, PreviewerServerConnection} from "src/PreviewerServerConnection"; |
||||
|
import * as React from "react"; |
||||
|
|
||||
|
interface PreviewerPresenterProps { |
||||
|
conn: PreviewerServerConnection; |
||||
|
} |
||||
|
|
||||
|
export class PreviewerPresenter extends React.Component<PreviewerPresenterProps> { |
||||
|
private canvasRef: React.RefObject<HTMLCanvasElement>; |
||||
|
|
||||
|
constructor(props: PreviewerPresenterProps) { |
||||
|
super(props); |
||||
|
this.state = {width: 1, height: 1}; |
||||
|
this.canvasRef = React.createRef() |
||||
|
this.componentDidUpdate({ |
||||
|
conn: null! |
||||
|
}, this.state); |
||||
|
} |
||||
|
|
||||
|
componentDidMount(): void { |
||||
|
this.updateCanvas(this.canvasRef.current, this.props.conn.currentFrame); |
||||
|
} |
||||
|
|
||||
|
componentDidUpdate(prevProps: Readonly<PreviewerPresenterProps>, prevState: Readonly<{}>, snapshot?: any): void { |
||||
|
if(prevProps.conn != this.props.conn) |
||||
|
{ |
||||
|
if(prevProps.conn) |
||||
|
prevProps.conn.removeFrameListener(this.frameHandler); |
||||
|
if(this.props.conn) |
||||
|
this.props.conn.addFrameListener(this.frameHandler); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private frameHandler = (frame: PreviewerFrame)=>{ |
||||
|
this.updateCanvas(this.canvasRef.current, frame); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
updateCanvas(canvas: HTMLCanvasElement | null, frame: PreviewerFrame | null) { |
||||
|
if (!canvas) |
||||
|
return; |
||||
|
if (frame == null){ |
||||
|
canvas.width = canvas.height = 1; |
||||
|
canvas.getContext('2d')!.clearRect(0,0,1,1); |
||||
|
} |
||||
|
else { |
||||
|
canvas.width = frame.data.width; |
||||
|
canvas.height = frame.data.height; |
||||
|
const ctx = canvas.getContext('2d')!; |
||||
|
ctx.putImageData(frame.data, 0,0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return <canvas ref={this.canvasRef}/> |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
export interface PreviewerFrame { |
||||
|
data: ImageData; |
||||
|
dpiX: number; |
||||
|
dpiY: number; |
||||
|
} |
||||
|
|
||||
|
export class PreviewerServerConnection { |
||||
|
private nextFrame = { |
||||
|
width: 0, |
||||
|
height: 0, |
||||
|
stride: 0, |
||||
|
dpiX: 0, |
||||
|
dpiY: 0, |
||||
|
sequenceId: "0" |
||||
|
}; |
||||
|
|
||||
|
public currentFrame: PreviewerFrame | null; |
||||
|
private handlers = new Set<(frame: PreviewerFrame | null) => void>(); |
||||
|
private conn: WebSocket; |
||||
|
|
||||
|
public addFrameListener(listener: (frame: PreviewerFrame | null) => void) { |
||||
|
this.handlers.add(listener); |
||||
|
if (this.currentFrame) |
||||
|
listener(this.currentFrame); |
||||
|
} |
||||
|
|
||||
|
public removeFrameListener(listener: (frame: PreviewerFrame | null) => void) { |
||||
|
this.handlers.delete(listener); |
||||
|
} |
||||
|
|
||||
|
constructor(uri: string) { |
||||
|
this.currentFrame = null; |
||||
|
var conn = this.conn = new WebSocket(uri); |
||||
|
conn.binaryType = 'arraybuffer'; |
||||
|
|
||||
|
const onMessage = this.onMessage; |
||||
|
conn.onmessage = msg => onMessage(msg); |
||||
|
|
||||
|
const onClose = () => this.setFrame(null); |
||||
|
conn.onclose = () => onClose(); |
||||
|
conn.onerror = (err: Event) => { |
||||
|
onClose(); |
||||
|
console.log("Connection error: " + err); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onMessage = (msg: MessageEvent) => { |
||||
|
if (typeof msg.data == 'string' || msg.data instanceof String) { |
||||
|
const parts = msg.data.split(':'); |
||||
|
if (parts[0] == 'frame') { |
||||
|
this.nextFrame = { |
||||
|
sequenceId: parts[1], |
||||
|
width: parseInt(parts[2]), |
||||
|
height: parseInt(parts[3]), |
||||
|
stride: parseInt(parts[4]), |
||||
|
dpiX: parseInt(parts[5]), |
||||
|
dpiY: parseInt(parts[6]) |
||||
|
} |
||||
|
} |
||||
|
} else if (msg.data instanceof ArrayBuffer) { |
||||
|
const arr = new Uint8ClampedArray(msg.data, 0); |
||||
|
const imageData = new ImageData(arr, this.nextFrame.width, this.nextFrame.height); |
||||
|
this.conn.send('frame-received:' + this.nextFrame.sequenceId); |
||||
|
this.setFrame({ |
||||
|
data: imageData, |
||||
|
dpiX: this.nextFrame.dpiX, |
||||
|
dpiY: this.nextFrame.dpiY |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
private setFrame(frame: PreviewerFrame | null) { |
||||
|
this.currentFrame = frame; |
||||
|
this.handlers.forEach(h => h(this.currentFrame)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<title>Avalonia XAML previewer web edition</title> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<div id="app"> |
||||
|
<center>Loading...</center> |
||||
|
</div> |
||||
|
<noscript>Javascript is required</noscript> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,15 @@ |
|||||
|
import * as React from "react"; |
||||
|
import {PreviewerPresenter} from './FramePresenter' |
||||
|
import {PreviewerServerConnection} from "src/PreviewerServerConnection"; |
||||
|
import * as ReactDOM from "react-dom"; |
||||
|
|
||||
|
const loc = window.location; |
||||
|
const conn = new PreviewerServerConnection((loc.protocol === "https:" ? "wss" : "ws") + "://" + loc.host + "/ws"); |
||||
|
|
||||
|
const App = function(){ |
||||
|
return <div style={{width: '100%'}}> |
||||
|
<PreviewerPresenter conn={conn}/> |
||||
|
</div> |
||||
|
}; |
||||
|
|
||||
|
ReactDOM.render(<App/>, document.getElementById("app")); |
||||
@ -0,0 +1,35 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"outDir": "build/dist", |
||||
|
"module": "esnext", |
||||
|
"target": "es5", |
||||
|
"lib": ["es6", "dom"], |
||||
|
"sourceMap": true, |
||||
|
"allowJs": true, |
||||
|
"jsx": "react", |
||||
|
"moduleResolution": "node", |
||||
|
"forceConsistentCasingInFileNames": true, |
||||
|
"noImplicitReturns": true, |
||||
|
"noImplicitThis": true, |
||||
|
"noImplicitAny": true, |
||||
|
"strictNullChecks": true, |
||||
|
"suppressImplicitAnyIndexErrors": true, |
||||
|
"noUnusedLocals": false, |
||||
|
"baseUrl": ".", |
||||
|
"experimentalDecorators": true, |
||||
|
"paths": { |
||||
|
"*": ["./node_modules/@types/*", "./node_modules/*"], |
||||
|
"src/*": ["./src/*"] |
||||
|
} |
||||
|
}, |
||||
|
"include": ["./src/**/*"], |
||||
|
"exclude": [ |
||||
|
"node_modules", |
||||
|
"build", |
||||
|
"scripts", |
||||
|
"acceptance-tests", |
||||
|
"webpack", |
||||
|
"jest", |
||||
|
"src/setupTests.ts" |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
const webpack = require('webpack'); |
||||
|
const path = require('path'); |
||||
|
const LiveReloadPlugin = require('webpack-livereload-plugin'); |
||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin'); |
||||
|
const CompressionPlugin = require('compression-webpack-plugin'); |
||||
|
const CleanWebpackPlugin = require('clean-webpack-plugin'); |
||||
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); |
||||
|
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); |
||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin'); |
||||
|
const prod = process.env.NODE_ENV == 'production'; |
||||
|
|
||||
|
class Printer { |
||||
|
apply(compiler) { |
||||
|
compiler.hooks.afterEmit.tap("Printer", ()=> console.log("Build completed at " + new Date().toString())); |
||||
|
compiler.hooks.watchRun.tap("Printer", ()=> console.log("Watch triggered at " + new Date().toString())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const config = { |
||||
|
entry: { |
||||
|
bundle: './src/index.tsx' |
||||
|
}, |
||||
|
output: { |
||||
|
path: path.resolve(__dirname, 'build'), |
||||
|
publicPath: '/', |
||||
|
filename: '[name].[chunkhash].js' |
||||
|
}, |
||||
|
performance: { hints: false }, |
||||
|
mode: prod ? "production" : "development", |
||||
|
module: { |
||||
|
rules: [ |
||||
|
{ |
||||
|
enforce: "pre", |
||||
|
test: /\.js$/, |
||||
|
loader: "source-map-loader", |
||||
|
exclude: [ |
||||
|
path.resolve(__dirname, 'node_modules/mobx-state-router') |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
"oneOf": [ |
||||
|
{ |
||||
|
test: /\.(ts|tsx)$/, |
||||
|
exclude: /node_modules/, |
||||
|
use: 'awesome-typescript-loader' |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.css$/, |
||||
|
use: [ |
||||
|
MiniCssExtractPlugin.loader, |
||||
|
'css-loader' |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
test: /\.(jpg|png)$/, |
||||
|
use: { |
||||
|
loader: "url-loader", |
||||
|
options: { |
||||
|
limit: 25000, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, |
||||
|
use: [{ |
||||
|
loader: 'file-loader', |
||||
|
options: { |
||||
|
name: '[name].[ext]', |
||||
|
outputPath: 'fonts/', // where the fonts will go
|
||||
|
} |
||||
|
}] |
||||
|
}, |
||||
|
{ |
||||
|
loader: require.resolve('file-loader'), |
||||
|
exclude: [/\.(js|jsx|mjs|tsx|ts)$/, /\.html$/, /\.json$/], |
||||
|
options: { |
||||
|
name: 'assets/[name].[hash:8].[ext]', |
||||
|
}, |
||||
|
}] |
||||
|
}, |
||||
|
|
||||
|
] |
||||
|
}, |
||||
|
devtool: "source-map", |
||||
|
resolve: { |
||||
|
modules: [path.resolve(__dirname, 'node_modules')], |
||||
|
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json", logLevel: 'info' })], |
||||
|
extensions: ['.ts', '.tsx', '.js', '.json'], |
||||
|
alias: { |
||||
|
'src': path.resolve(__dirname, 'src') |
||||
|
} |
||||
|
}, |
||||
|
plugins: |
||||
|
[ |
||||
|
new Printer(), |
||||
|
new CleanWebpackPlugin([path.resolve(__dirname, 'build')]), |
||||
|
new MiniCssExtractPlugin({ |
||||
|
filename: "[name].[chunkhash]h" + |
||||
|
".css", |
||||
|
chunkFilename: "[id].[chunkhash].css" |
||||
|
}), |
||||
|
new LiveReloadPlugin({appendScriptTag: !prod}), |
||||
|
new HtmlWebpackPlugin({ |
||||
|
template: path.resolve(__dirname, './src/index.html'), |
||||
|
filename: 'index.html' //relative to root of the application
|
||||
|
}), |
||||
|
new CopyWebpackPlugin([ |
||||
|
// relative path from src
|
||||
|
//{ from: './src/favicon.ico' },
|
||||
|
//{ from: './src/assets' }
|
||||
|
]), |
||||
|
new CompressionPlugin({ |
||||
|
test: /(\?.*)?$/i |
||||
|
}) |
||||
|
] |
||||
|
}; |
||||
|
module.exports = config; |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Avalonia.Dialogs |
||||
|
{ |
||||
|
public class ManagedFileDialogOptions |
||||
|
{ |
||||
|
public bool AllowDirectorySelection { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Avalonia.Remote.Protocol |
||||
|
{ |
||||
|
[AvaloniaRemoteMessageGuid("53778004-78fa-4381-8ec3-176a6f2328b6")] |
||||
|
public class HtmlTransportStartedMessage |
||||
|
{ |
||||
|
public string Uri { get; set; } |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Animation.Animators |
||||
|
{ |
||||
|
public class BoxShadowAnimator : Animator<BoxShadow> |
||||
|
{ |
||||
|
static ColorAnimator s_colorAnimator = new ColorAnimator(); |
||||
|
static DoubleAnimator s_doubleAnimator = new DoubleAnimator(); |
||||
|
static BoolAnimator s_boolAnimator = new BoolAnimator(); |
||||
|
public override BoxShadow Interpolate(double progress, BoxShadow oldValue, BoxShadow newValue) |
||||
|
{ |
||||
|
return new BoxShadow |
||||
|
{ |
||||
|
OffsetX = s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX), |
||||
|
OffsetY = s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY), |
||||
|
Blur = s_doubleAnimator.Interpolate(progress, oldValue.Blur, newValue.Blur), |
||||
|
Spread = s_doubleAnimator.Interpolate(progress, oldValue.Spread, newValue.Spread), |
||||
|
Color = s_colorAnimator.Interpolate(progress, oldValue.Color, newValue.Color), |
||||
|
IsInset = s_boolAnimator.Interpolate(progress, oldValue.IsInset, newValue.IsInset) |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Animation.Animators |
||||
|
{ |
||||
|
public class BoxShadowsAnimator : Animator<BoxShadows> |
||||
|
{ |
||||
|
private static readonly BoxShadowAnimator s_boxShadowAnimator = new BoxShadowAnimator(); |
||||
|
public override BoxShadows Interpolate(double progress, BoxShadows oldValue, BoxShadows newValue) |
||||
|
{ |
||||
|
int cnt = progress >= 1d ? newValue.Count : oldValue.Count; |
||||
|
if (cnt == 0) |
||||
|
return new BoxShadows(); |
||||
|
|
||||
|
BoxShadow first; |
||||
|
if (oldValue.Count > 0 && newValue.Count > 0) |
||||
|
first = s_boxShadowAnimator.Interpolate(progress, oldValue[0], newValue[0]); |
||||
|
else if (oldValue.Count > 0) |
||||
|
first = oldValue[0]; |
||||
|
else |
||||
|
first = newValue[0]; |
||||
|
|
||||
|
if (cnt == 1) |
||||
|
return new BoxShadows(first); |
||||
|
|
||||
|
var rest = new BoxShadow[cnt - 1]; |
||||
|
for (var c = 0; c < rest.Length; c++) |
||||
|
{ |
||||
|
var idx = c + 1; |
||||
|
if (oldValue.Count > idx && newValue.Count > idx) |
||||
|
rest[c] = s_boxShadowAnimator.Interpolate(progress, oldValue[idx], newValue[idx]); |
||||
|
else if (oldValue.Count > idx) |
||||
|
rest[c] = oldValue[idx]; |
||||
|
else |
||||
|
rest[c] = newValue[idx]; |
||||
|
} |
||||
|
|
||||
|
return new BoxShadows(first, rest); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,133 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using Avalonia.Animation.Animators; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public struct BoxShadow |
||||
|
{ |
||||
|
public double OffsetX { get; set; } |
||||
|
public double OffsetY { get; set; } |
||||
|
public double Blur { get; set; } |
||||
|
public double Spread { get; set; } |
||||
|
public Color Color { get; set; } |
||||
|
public bool IsInset { get; set; } |
||||
|
|
||||
|
static BoxShadow() |
||||
|
{ |
||||
|
Animation.Animation.RegisterAnimator<BoxShadowAnimator>(prop => |
||||
|
typeof(BoxShadow).IsAssignableFrom(prop.PropertyType)); |
||||
|
} |
||||
|
|
||||
|
public bool Equals(in BoxShadow other) |
||||
|
{ |
||||
|
return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color); |
||||
|
} |
||||
|
|
||||
|
public override bool Equals(object obj) |
||||
|
{ |
||||
|
return obj is BoxShadow other && Equals(other); |
||||
|
} |
||||
|
|
||||
|
public override int GetHashCode() |
||||
|
{ |
||||
|
unchecked |
||||
|
{ |
||||
|
var hashCode = OffsetX.GetHashCode(); |
||||
|
hashCode = (hashCode * 397) ^ OffsetY.GetHashCode(); |
||||
|
hashCode = (hashCode * 397) ^ Blur.GetHashCode(); |
||||
|
hashCode = (hashCode * 397) ^ Spread.GetHashCode(); |
||||
|
hashCode = (hashCode * 397) ^ Color.GetHashCode(); |
||||
|
return hashCode; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; |
||||
|
|
||||
|
private readonly static char[] s_Separator = new char[] { ' ', '\t' }; |
||||
|
|
||||
|
struct ArrayReader |
||||
|
{ |
||||
|
private int _index; |
||||
|
private string[] _arr; |
||||
|
|
||||
|
public ArrayReader(string[] arr) |
||||
|
{ |
||||
|
_arr = arr; |
||||
|
_index = 0; |
||||
|
} |
||||
|
|
||||
|
public bool TryReadString(out string s) |
||||
|
{ |
||||
|
s = null; |
||||
|
if (_index >= _arr.Length) |
||||
|
return false; |
||||
|
s = _arr[_index]; |
||||
|
_index++; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public string ReadString() |
||||
|
{ |
||||
|
if(!TryReadString(out var rv)) |
||||
|
throw new FormatException(); |
||||
|
return rv; |
||||
|
} |
||||
|
} |
||||
|
public static unsafe BoxShadow Parse(string s) |
||||
|
{ |
||||
|
if(s == null) |
||||
|
throw new ArgumentNullException(); |
||||
|
if (s.Length == 0) |
||||
|
throw new FormatException(); |
||||
|
|
||||
|
var p = s.Split(s_Separator, StringSplitOptions.RemoveEmptyEntries); |
||||
|
if (p.Length == 1 && p[0] == "none") |
||||
|
return default; |
||||
|
|
||||
|
if (p.Length < 3 || p.Length > 6) |
||||
|
throw new FormatException(); |
||||
|
|
||||
|
bool inset = false; |
||||
|
|
||||
|
var tokenizer = new ArrayReader(p); |
||||
|
|
||||
|
string firstToken = tokenizer.ReadString(); |
||||
|
if (firstToken == "inset") |
||||
|
{ |
||||
|
inset = true; |
||||
|
firstToken = tokenizer.ReadString(); |
||||
|
} |
||||
|
|
||||
|
var offsetX = double.Parse(firstToken, CultureInfo.InvariantCulture); |
||||
|
var offsetY = double.Parse(tokenizer.ReadString(), CultureInfo.InvariantCulture); |
||||
|
double blur = 0; |
||||
|
double spread = 0; |
||||
|
|
||||
|
|
||||
|
tokenizer.TryReadString(out var token3); |
||||
|
tokenizer.TryReadString(out var token4); |
||||
|
tokenizer.TryReadString(out var token5); |
||||
|
|
||||
|
if (token4 != null) |
||||
|
blur = double.Parse(token3, CultureInfo.InvariantCulture); |
||||
|
if (token5 != null) |
||||
|
spread = double.Parse(token4, CultureInfo.InvariantCulture); |
||||
|
|
||||
|
var color = Color.Parse(token5 ?? token4 ?? token3); |
||||
|
return new BoxShadow |
||||
|
{ |
||||
|
IsInset = inset, |
||||
|
OffsetX = offsetX, |
||||
|
OffsetY = offsetY, |
||||
|
Blur = blur, |
||||
|
Spread = spread, |
||||
|
Color = color |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public Rect TransformBounds(in Rect rect) |
||||
|
=> IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,137 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel; |
||||
|
using Avalonia.Animation.Animators; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public struct BoxShadows |
||||
|
{ |
||||
|
private readonly BoxShadow _first; |
||||
|
private readonly BoxShadow[] _list; |
||||
|
public int Count { get; } |
||||
|
|
||||
|
static BoxShadows() |
||||
|
{ |
||||
|
Animation.Animation.RegisterAnimator<BoxShadowsAnimator>(prop => |
||||
|
typeof(BoxShadows).IsAssignableFrom(prop.PropertyType)); |
||||
|
} |
||||
|
|
||||
|
public BoxShadows(BoxShadow shadow) |
||||
|
{ |
||||
|
_first = shadow; |
||||
|
_list = null; |
||||
|
Count = 1; |
||||
|
} |
||||
|
|
||||
|
public BoxShadows(BoxShadow first, BoxShadow[] rest) |
||||
|
{ |
||||
|
_first = first; |
||||
|
_list = rest; |
||||
|
Count = 1 + (rest?.Length ?? 0); |
||||
|
} |
||||
|
|
||||
|
public BoxShadow this[int c] |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (c< 0 || c >= Count) |
||||
|
throw new IndexOutOfRangeException(); |
||||
|
if (c == 0) |
||||
|
return _first; |
||||
|
return _list[c - 1]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public struct BoxShadowsEnumerator |
||||
|
{ |
||||
|
private int _index; |
||||
|
private BoxShadows _shadows; |
||||
|
|
||||
|
public BoxShadowsEnumerator(BoxShadows shadows) |
||||
|
{ |
||||
|
_shadows = shadows; |
||||
|
_index = -1; |
||||
|
} |
||||
|
|
||||
|
public BoxShadow Current => _shadows[_index]; |
||||
|
|
||||
|
public bool MoveNext() |
||||
|
{ |
||||
|
_index++; |
||||
|
return _index < _shadows.Count; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public BoxShadowsEnumerator GetEnumerator() => new BoxShadowsEnumerator(this); |
||||
|
|
||||
|
private static readonly char[] s_Separators = new[] { ',' }; |
||||
|
public static BoxShadows Parse(string s) |
||||
|
{ |
||||
|
var sp = s.Split(s_Separators, StringSplitOptions.RemoveEmptyEntries); |
||||
|
if (sp.Length == 0 |
||||
|
|| (sp.Length == 1 && |
||||
|
(string.IsNullOrWhiteSpace(sp[0]) |
||||
|
|| sp[0] == "none"))) |
||||
|
return new BoxShadows(); |
||||
|
|
||||
|
var first = BoxShadow.Parse(sp[0]); |
||||
|
if (sp.Length == 1) |
||||
|
return new BoxShadows(first); |
||||
|
|
||||
|
var rest = new BoxShadow[sp.Length - 1]; |
||||
|
for (var c = 0; c < rest.Length; c++) |
||||
|
rest[c] = BoxShadow.Parse(sp[c + 1]); |
||||
|
return new BoxShadows(first, rest); |
||||
|
} |
||||
|
|
||||
|
public Rect TransformBounds(in Rect rect) |
||||
|
{ |
||||
|
var final = rect; |
||||
|
foreach (var shadow in this) |
||||
|
final = final.Union(shadow.TransformBounds(rect)); |
||||
|
return final; |
||||
|
} |
||||
|
|
||||
|
public bool HasInsetShadows |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
foreach(var boxShadow in this) |
||||
|
if (!boxShadow.IsEmpty && boxShadow.IsInset) |
||||
|
return true; |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow); |
||||
|
|
||||
|
public bool Equals(BoxShadows other) |
||||
|
{ |
||||
|
if (other.Count != Count) |
||||
|
return false; |
||||
|
for(var c=0; c<Count ; c++) |
||||
|
if (!this[c].Equals(other[c])) |
||||
|
return false; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public override bool Equals(object obj) |
||||
|
{ |
||||
|
return obj is BoxShadows other && Equals(other); |
||||
|
} |
||||
|
|
||||
|
public override int GetHashCode() |
||||
|
{ |
||||
|
unchecked |
||||
|
{ |
||||
|
int hashCode = 0; |
||||
|
foreach (var s in this) |
||||
|
hashCode = (hashCode * 397) ^ s.GetHashCode(); |
||||
|
return hashCode; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue