13 changed files with 4324 additions and 13 deletions
@ -0,0 +1,48 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public interface ISelectionModel : INotifyPropertyChanged |
|||
{ |
|||
IEnumerable? Source { get; set; } |
|||
bool SingleSelect { get; set; } |
|||
int SelectedIndex { get; set; } |
|||
IReadOnlyList<int> SelectedIndexes { get; } |
|||
object? SelectedItem { get; } |
|||
IReadOnlyList<object?> SelectedItems { get; } |
|||
int AnchorIndex { get; set; } |
|||
int Count { get; } |
|||
|
|||
public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged; |
|||
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged; |
|||
public event EventHandler? LostSelection; |
|||
public event EventHandler? SourceReset; |
|||
|
|||
public void BeginBatchUpdate(); |
|||
public void EndBatchUpdate(); |
|||
bool IsSelected(int index); |
|||
void Select(int index); |
|||
void Deselect(int index); |
|||
void SelectRange(int start, int end); |
|||
void DeselectRange(int start, int end); |
|||
void Clear(); |
|||
} |
|||
|
|||
public static class SelectionModelExtensions |
|||
{ |
|||
public static void SelectAll(this ISelectionModel model) |
|||
{ |
|||
model.SelectRange(0, int.MaxValue); |
|||
} |
|||
|
|||
public static void SelectRangeFromAnchor(this ISelectionModel model, int to) |
|||
{ |
|||
model.SelectRange(model.AnchorIndex, to); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,343 @@ |
|||
// 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.Selection |
|||
{ |
|||
internal readonly struct IndexRange : IEquatable<IndexRange> |
|||
{ |
|||
private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue); |
|||
|
|||
public IndexRange(int index) |
|||
{ |
|||
Begin = index; |
|||
End = index; |
|||
} |
|||
|
|||
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 bool Contains(IReadOnlyList<IndexRange>? ranges, int index) |
|||
{ |
|||
if (ranges is null || index < 0) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
foreach (var range in ranges) |
|||
{ |
|||
if (range.Contains(index)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public static int GetAt(IReadOnlyList<IndexRange> ranges, int index) |
|||
{ |
|||
var currentIndex = 0; |
|||
|
|||
foreach (var range in ranges) |
|||
{ |
|||
var currentCount = range.Count; |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
return range.Begin + (index - currentIndex); |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
|
|||
throw new IndexOutOfRangeException("The index was out of range."); |
|||
} |
|||
|
|||
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 Add( |
|||
IList<IndexRange> destination, |
|||
IReadOnlyList<IndexRange> source, |
|||
IList<IndexRange>? added = null) |
|||
{ |
|||
var result = 0; |
|||
|
|||
foreach (var range in source) |
|||
{ |
|||
result += Add(destination, range, added); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static int Intersect( |
|||
IList<IndexRange> ranges, |
|||
IndexRange range, |
|||
IList<IndexRange>? removed = null) |
|||
{ |
|||
var result = 0; |
|||
|
|||
for (var i = 0; i < ranges.Count && range != s_invalid; ++i) |
|||
{ |
|||
var existing = ranges[i]; |
|||
|
|||
if (existing.End < range.Begin || existing.Begin > range.End) |
|||
{ |
|||
removed?.Add(existing); |
|||
ranges.RemoveAt(i--); |
|||
result += existing.Count; |
|||
} |
|||
else |
|||
{ |
|||
if (existing.Begin < range.Begin) |
|||
{ |
|||
var except = new IndexRange(existing.Begin, range.Begin - 1); |
|||
removed?.Add(except); |
|||
ranges[i] = existing = new IndexRange(range.Begin, existing.End); |
|||
result += except.Count; |
|||
} |
|||
|
|||
if (existing.End > range.End) |
|||
{ |
|||
var except = new IndexRange(range.End + 1, existing.End); |
|||
removed?.Add(except); |
|||
ranges[i] = new IndexRange(existing.Begin, range.End); |
|||
result += except.Count; |
|||
} |
|||
} |
|||
} |
|||
|
|||
MergeRanges(ranges); |
|||
|
|||
if (removed is object) |
|||
{ |
|||
MergeRanges(removed); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static int Remove( |
|||
IList<IndexRange>? ranges, |
|||
IndexRange range, |
|||
IList<IndexRange>? removed = null) |
|||
{ |
|||
if (ranges is null) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
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 int Remove( |
|||
IList<IndexRange> destination, |
|||
IReadOnlyList<IndexRange> source, |
|||
IList<IndexRange>? added = null) |
|||
{ |
|||
var result = 0; |
|||
|
|||
foreach (var range in source) |
|||
{ |
|||
result += Remove(destination, range, added); |
|||
} |
|||
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
internal class SelectedIndexes<T> : IReadOnlyList<int> |
|||
{ |
|||
private readonly SelectionModel<T>? _owner; |
|||
private readonly IReadOnlyList<IndexRange>? _ranges; |
|||
|
|||
public SelectedIndexes(SelectionModel<T> owner) => _owner = owner; |
|||
public SelectedIndexes(IReadOnlyList<IndexRange> ranges) => _ranges = ranges; |
|||
|
|||
public int this[int index] |
|||
{ |
|||
get |
|||
{ |
|||
if (index >= Count) |
|||
{ |
|||
throw new IndexOutOfRangeException("The index was out of range."); |
|||
} |
|||
|
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return _owner.SelectedIndex; |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.GetAt(Ranges!, index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return _owner.SelectedIndex == -1 ? 0 : 1; |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.GetCount(Ranges!); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private IReadOnlyList<IndexRange> Ranges => _ranges ?? _owner!.Ranges!; |
|||
|
|||
public IEnumerator<int> GetEnumerator() |
|||
{ |
|||
IEnumerator<int> SingleSelect() |
|||
{ |
|||
if (_owner.SelectedIndex >= 0) |
|||
{ |
|||
yield return _owner.SelectedIndex; |
|||
} |
|||
} |
|||
|
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return SingleSelect(); |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.EnumerateIndices(Ranges).GetEnumerator(); |
|||
} |
|||
} |
|||
|
|||
public static SelectedIndexes<T>? Create(IReadOnlyList<IndexRange>? ranges) |
|||
{ |
|||
return ranges is object ? new SelectedIndexes<T>(ranges) : null; |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
internal class SelectedItems<T> : IReadOnlyList<T> |
|||
{ |
|||
private readonly SelectionModel<T>? _owner; |
|||
private readonly ItemsSourceView<T>? _items; |
|||
private readonly IReadOnlyList<IndexRange>? _ranges; |
|||
|
|||
public SelectedItems(SelectionModel<T> owner) => _owner = owner; |
|||
|
|||
public SelectedItems(IReadOnlyList<IndexRange> ranges, ItemsSourceView<T>? items) |
|||
{ |
|||
_ranges = ranges ?? throw new ArgumentNullException(nameof(ranges)); |
|||
_items = items; |
|||
} |
|||
|
|||
[MaybeNull] |
|||
public T this[int index] |
|||
{ |
|||
#pragma warning disable CS8766
|
|||
get |
|||
#pragma warning restore CS8766
|
|||
{ |
|||
if (index >= Count) |
|||
{ |
|||
throw new IndexOutOfRangeException("The index was out of range."); |
|||
} |
|||
|
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return _owner.SelectedItem; |
|||
} |
|||
else if (Items is object) |
|||
{ |
|||
return Items[index]; |
|||
} |
|||
else |
|||
{ |
|||
return default; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
return _owner.SelectedIndex == -1 ? 0 : 1; |
|||
} |
|||
else |
|||
{ |
|||
return Ranges is object ? IndexRange.GetCount(Ranges) : 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView; |
|||
private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges; |
|||
|
|||
public IEnumerator<T> GetEnumerator() |
|||
{ |
|||
if (_owner?.SingleSelect == true) |
|||
{ |
|||
if (_owner.SelectedIndex >= 0) |
|||
{ |
|||
#pragma warning disable CS8603
|
|||
yield return _owner.SelectedItem; |
|||
#pragma warning restore CS8603
|
|||
} |
|||
} |
|||
else |
|||
{ |
|||
var items = Items; |
|||
|
|||
foreach (var range in Ranges!) |
|||
{ |
|||
for (var i = range.Begin; i <= range.End; ++i) |
|||
{ |
|||
#pragma warning disable CS8603
|
|||
yield return items is object ? items[i] : default; |
|||
#pragma warning restore CS8603
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
public static SelectedItems<T>? Create( |
|||
IReadOnlyList<IndexRange>? ranges, |
|||
ItemsSourceView<T>? items) |
|||
{ |
|||
return ranges is object ? new SelectedItems<T>(ranges, items) : null; |
|||
} |
|||
|
|||
public class Untyped : IReadOnlyList<object?> |
|||
{ |
|||
private readonly IReadOnlyList<T> _source; |
|||
public Untyped(IReadOnlyList<T> source) => _source = source; |
|||
public object? this[int index] => _source[index]; |
|||
public int Count => _source.Count; |
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
public IEnumerator<object?> GetEnumerator() |
|||
{ |
|||
foreach (var i in _source) |
|||
{ |
|||
yield return i; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,632 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.ComponentModel; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Linq; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public class SelectionModel<T> : SelectionNodeBase<T>, ISelectionModel |
|||
{ |
|||
private bool _singleSelect = true; |
|||
private int _anchorIndex = -1; |
|||
private int _selectedIndex = -1; |
|||
private Operation? _operation; |
|||
private SelectedIndexes<T>? _selectedIndexes; |
|||
private SelectedItems<T>? _selectedItems; |
|||
private SelectedItems<T>.Untyped? _selectedItemsUntyped; |
|||
private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged; |
|||
|
|||
public SelectionModel() |
|||
{ |
|||
} |
|||
|
|||
public SelectionModel(IEnumerable<T>? source) |
|||
{ |
|||
Source = source; |
|||
} |
|||
|
|||
public override IEnumerable<T>? Source |
|||
{ |
|||
get => base.Source; |
|||
set |
|||
{ |
|||
if (base.Source != value) |
|||
{ |
|||
if (_operation is object) |
|||
{ |
|||
throw new InvalidOperationException("Cannot change source while update is in progress."); |
|||
} |
|||
|
|||
if (base.Source is object) |
|||
{ |
|||
Clear(); |
|||
} |
|||
|
|||
base.Source = value; |
|||
|
|||
using var update = BatchUpdate(); |
|||
update.Operation.IsSourceUpdate = true; |
|||
TrimInvalidSelections(update.Operation); |
|||
RaisePropertyChanged(nameof(Source)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool SingleSelect |
|||
{ |
|||
get => _singleSelect; |
|||
set |
|||
{ |
|||
if (_singleSelect != value) |
|||
{ |
|||
_singleSelect = value; |
|||
RangesEnabled = !value; |
|||
|
|||
if (RangesEnabled && _selectedIndex >= 0) |
|||
{ |
|||
CommitSelect(new IndexRange(_selectedIndex)); |
|||
} |
|||
|
|||
RaisePropertyChanged(nameof(SingleSelect)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public int SelectedIndex |
|||
{ |
|||
get => _selectedIndex; |
|||
set |
|||
{ |
|||
using var update = BatchUpdate(); |
|||
Clear(); |
|||
Select(value); |
|||
} |
|||
} |
|||
|
|||
public IReadOnlyList<int> SelectedIndexes => _selectedIndexes ??= new SelectedIndexes<T>(this); |
|||
|
|||
[MaybeNull] |
|||
public T SelectedItem => GetItemAt(_selectedIndex); |
|||
|
|||
public IReadOnlyList<T> SelectedItems => _selectedItems ??= new SelectedItems<T>(this); |
|||
|
|||
public int AnchorIndex |
|||
{ |
|||
get => _anchorIndex; |
|||
set |
|||
{ |
|||
using var update = BatchUpdate(); |
|||
var index = CoerceIndex(value); |
|||
update.Operation.AnchorIndex = index; |
|||
} |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
if (SingleSelect) |
|||
{ |
|||
return _selectedIndex >= 0 ? 1 : 0; |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.GetCount(Ranges); |
|||
} |
|||
} |
|||
} |
|||
|
|||
IEnumerable? ISelectionModel.Source |
|||
{ |
|||
get => Source; |
|||
set => Source = (IEnumerable<T>?)value; |
|||
} |
|||
|
|||
object? ISelectionModel.SelectedItem => SelectedItem; |
|||
|
|||
IReadOnlyList<object?> ISelectionModel.SelectedItems |
|||
{ |
|||
get => _selectedItemsUntyped ??= new SelectedItems<T>.Untyped(SelectedItems); |
|||
} |
|||
|
|||
public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged; |
|||
public event EventHandler<SelectionModelSelectionChangedEventArgs<T>>? SelectionChanged; |
|||
public event EventHandler? LostSelection; |
|||
public event EventHandler? SourceReset; |
|||
public event PropertyChangedEventHandler? PropertyChanged; |
|||
|
|||
event EventHandler<SelectionModelSelectionChangedEventArgs>? ISelectionModel.SelectionChanged |
|||
{ |
|||
add => _untypedSelectionChanged += value; |
|||
remove => _untypedSelectionChanged -= value; |
|||
} |
|||
|
|||
public BatchUpdateOperation BatchUpdate() => new BatchUpdateOperation(this); |
|||
|
|||
public void BeginBatchUpdate() |
|||
{ |
|||
_operation ??= new Operation(this); |
|||
++_operation.UpdateCount; |
|||
} |
|||
|
|||
public void EndBatchUpdate() |
|||
{ |
|||
if (_operation is null || _operation.UpdateCount == 0) |
|||
{ |
|||
throw new InvalidOperationException("No batch update in progress."); |
|||
} |
|||
|
|||
if (--_operation.UpdateCount == 0) |
|||
{ |
|||
// If the collection is currently changing, commit the update when the
|
|||
// collection change finishes.
|
|||
if (!IsSourceCollectionChanging) |
|||
{ |
|||
CommitOperation(_operation); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool IsSelected(int index) |
|||
{ |
|||
if (index < 0) |
|||
{ |
|||
return false; |
|||
} |
|||
else if (SingleSelect) |
|||
{ |
|||
return _selectedIndex == index; |
|||
} |
|||
else |
|||
{ |
|||
return IndexRange.Contains(Ranges, index); |
|||
} |
|||
} |
|||
|
|||
public void Select(int index) => SelectRange(index, index, false, true); |
|||
|
|||
public void Deselect(int index) => DeselectRange(index, index); |
|||
|
|||
public void SelectRange(int start, int end) => SelectRange(start, end, false, false); |
|||
|
|||
public void DeselectRange(int start, int end) |
|||
{ |
|||
using var update = BatchUpdate(); |
|||
var o = update.Operation; |
|||
var range = CoerceRange(start, end); |
|||
|
|||
if (range.Begin == -1) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (RangesEnabled) |
|||
{ |
|||
var selected = Ranges.ToList(); |
|||
var deselected = new List<IndexRange>(); |
|||
var operationDeselected = new List<IndexRange>(); |
|||
|
|||
o.DeselectedRanges ??= new List<IndexRange>(); |
|||
IndexRange.Remove(o.SelectedRanges, range, operationDeselected); |
|||
IndexRange.Remove(selected, range, deselected); |
|||
IndexRange.Add(o.DeselectedRanges, deselected); |
|||
|
|||
if (IndexRange.Contains(deselected, o.SelectedIndex) || |
|||
IndexRange.Contains(operationDeselected, o.SelectedIndex)) |
|||
{ |
|||
o.SelectedIndex = GetFirstSelectedIndexFromRanges(except: deselected); |
|||
} |
|||
} |
|||
else if(range.Contains(_selectedIndex)) |
|||
{ |
|||
o.SelectedIndex = -1; |
|||
} |
|||
} |
|||
|
|||
public void Clear() => DeselectRange(0, int.MaxValue); |
|||
|
|||
protected void RaisePropertyChanged(string propertyName) |
|||
{ |
|||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
|||
} |
|||
|
|||
private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta) |
|||
{ |
|||
IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta)); |
|||
} |
|||
|
|||
private protected override void OnSourceReset() |
|||
{ |
|||
_selectedIndex = _anchorIndex = -1; |
|||
CommitDeselect(new IndexRange(0, int.MaxValue)); |
|||
|
|||
if (SourceReset is object) |
|||
{ |
|||
SourceReset.Invoke(this, EventArgs.Empty); |
|||
} |
|||
else |
|||
{ |
|||
//Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
|
|||
// this,
|
|||
// "SelectionModel received Reset but no SourceReset handler was registered to handle it. " +
|
|||
// "Selection may be out of sync.",
|
|||
// typeof(SelectionModel));
|
|||
} |
|||
} |
|||
|
|||
private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems) |
|||
{ |
|||
if (SelectionChanged is object || _untypedSelectionChanged is object) |
|||
{ |
|||
var e = new SelectionModelSelectionChangedEventArgs<T>(deselectedItems: deselectedItems); |
|||
SelectionChanged?.Invoke(this, e); |
|||
_untypedSelectionChanged?.Invoke(this, e); |
|||
} |
|||
} |
|||
|
|||
private protected override CollectionChangeState OnItemsAdded(int index, IList items) |
|||
{ |
|||
var count = items.Count; |
|||
var shifted = SelectedIndex >= index; |
|||
var shiftCount = shifted ? count : 0; |
|||
|
|||
_selectedIndex += shiftCount; |
|||
_anchorIndex += shiftCount; |
|||
|
|||
var baseResult = base.OnItemsAdded(index, items); |
|||
shifted |= baseResult.ShiftDelta != 0; |
|||
|
|||
return new CollectionChangeState |
|||
{ |
|||
ShiftIndex = index, |
|||
ShiftDelta = shifted ? count : 0, |
|||
}; |
|||
} |
|||
|
|||
private protected override CollectionChangeState OnItemsRemoved(int index, IList items) |
|||
{ |
|||
var count = items.Count; |
|||
var removedRange = new IndexRange(index, index + count - 1); |
|||
var shifted = false; |
|||
List<T>? removed; |
|||
|
|||
var baseResult = base.OnItemsRemoved(index, items); |
|||
shifted |= baseResult.ShiftDelta != 0; |
|||
removed = baseResult.RemovedItems; |
|||
|
|||
if (removedRange.Contains(SelectedIndex)) |
|||
{ |
|||
if (SingleSelect) |
|||
{ |
|||
#pragma warning disable CS8604
|
|||
removed = new List<T> { (T)items[SelectedIndex - index] }; |
|||
#pragma warning restore CS8604
|
|||
} |
|||
|
|||
_selectedIndex = GetFirstSelectedIndexFromRanges(); |
|||
} |
|||
else if (SelectedIndex >= index) |
|||
{ |
|||
_selectedIndex -= count; |
|||
shifted = true; |
|||
} |
|||
|
|||
if (removedRange.Contains(AnchorIndex)) |
|||
{ |
|||
_anchorIndex = GetFirstSelectedIndexFromRanges(); |
|||
} |
|||
else if (AnchorIndex >= index) |
|||
{ |
|||
_anchorIndex -= count; |
|||
shifted = true; |
|||
} |
|||
|
|||
return new CollectionChangeState |
|||
{ |
|||
ShiftIndex = index, |
|||
ShiftDelta = shifted ? -count : 0, |
|||
RemovedItems = removed, |
|||
}; |
|||
} |
|||
|
|||
private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (_operation?.UpdateCount > 0) |
|||
{ |
|||
throw new InvalidOperationException("Source collection was modified during selection update."); |
|||
} |
|||
|
|||
var oldAnchorIndex = _anchorIndex; |
|||
var oldSelectedIndex = _selectedIndex; |
|||
|
|||
base.OnSourceCollectionChanged(e); |
|||
|
|||
if (oldSelectedIndex != _selectedIndex) |
|||
{ |
|||
RaisePropertyChanged(nameof(SelectedIndex)); |
|||
} |
|||
|
|||
if (oldAnchorIndex != _anchorIndex) |
|||
{ |
|||
RaisePropertyChanged(nameof(AnchorIndex)); |
|||
} |
|||
} |
|||
|
|||
protected override void OnSourceCollectionChangeFinished() |
|||
{ |
|||
if (_operation is object) |
|||
{ |
|||
CommitOperation(_operation); |
|||
} |
|||
} |
|||
|
|||
private int GetFirstSelectedIndexFromRanges(List<IndexRange>? except = null) |
|||
{ |
|||
if (RangesEnabled) |
|||
{ |
|||
var count = IndexRange.GetCount(Ranges); |
|||
var index = 0; |
|||
|
|||
while (index < count) |
|||
{ |
|||
var result = IndexRange.GetAt(Ranges, index++); |
|||
|
|||
if (!IndexRange.Contains(except, result)) |
|||
{ |
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return -1; |
|||
} |
|||
|
|||
private void SelectRange( |
|||
int start, |
|||
int end, |
|||
bool forceSelectedIndex, |
|||
bool forceAnchorIndex) |
|||
{ |
|||
if (SingleSelect && start != end) |
|||
{ |
|||
throw new InvalidOperationException("Cannot select range with single selection."); |
|||
} |
|||
|
|||
var range = CoerceRange(start, end); |
|||
|
|||
if (range.Begin == -1) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
using var update = BatchUpdate(); |
|||
var o = update.Operation; |
|||
var selected = new List<IndexRange>(); |
|||
|
|||
if (RangesEnabled) |
|||
{ |
|||
o.SelectedRanges ??= new List<IndexRange>(); |
|||
IndexRange.Remove(o.DeselectedRanges, range); |
|||
IndexRange.Add(o.SelectedRanges, range); |
|||
IndexRange.Remove(o.SelectedRanges, Ranges); |
|||
|
|||
if (o.SelectedIndex == -1 || forceSelectedIndex) |
|||
{ |
|||
o.SelectedIndex = range.Begin; |
|||
} |
|||
|
|||
if (o.AnchorIndex == -1 || forceAnchorIndex) |
|||
{ |
|||
o.AnchorIndex = range.Begin; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
o.SelectedIndex = o.AnchorIndex = start; |
|||
} |
|||
} |
|||
|
|||
[return: MaybeNull] |
|||
private T GetItemAt(int index) |
|||
{ |
|||
if (ItemsView is null || index < 0 || index >= ItemsView.Count) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
return ItemsView.GetAt(index); |
|||
} |
|||
|
|||
private int CoerceIndex(int index) |
|||
{ |
|||
index = Math.Max(index, -1); |
|||
|
|||
if (ItemsView is object && index >= ItemsView.Count) |
|||
{ |
|||
index = -1; |
|||
} |
|||
|
|||
return index; |
|||
} |
|||
|
|||
private IndexRange CoerceRange(int start, int end) |
|||
{ |
|||
var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue; |
|||
|
|||
if (start > max || (start < 0 && end < 0)) |
|||
{ |
|||
return new IndexRange(-1); |
|||
} |
|||
|
|||
start = Math.Max(start, 0); |
|||
end = Math.Min(end, max); |
|||
|
|||
return new IndexRange(start, end); |
|||
} |
|||
|
|||
private void TrimInvalidSelections(Operation operation) |
|||
{ |
|||
if (ItemsView is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var max = ItemsView.Count - 1; |
|||
|
|||
if (operation.SelectedIndex > max) |
|||
{ |
|||
operation.SelectedIndex = GetFirstSelectedIndexFromRanges(); |
|||
} |
|||
|
|||
if (operation.AnchorIndex > max) |
|||
{ |
|||
operation.AnchorIndex = GetFirstSelectedIndexFromRanges(); |
|||
} |
|||
|
|||
if (RangesEnabled && Ranges.Count > 0) |
|||
{ |
|||
var selected = Ranges.ToList(); |
|||
|
|||
if (max < 0) |
|||
{ |
|||
operation.DeselectedRanges = selected; |
|||
} |
|||
else |
|||
{ |
|||
var valid = new IndexRange(0, max); |
|||
var removed = new List<IndexRange>(); |
|||
IndexRange.Intersect(selected, valid, removed); |
|||
operation.DeselectedRanges = removed; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void CommitOperation(Operation operation) |
|||
{ |
|||
try |
|||
{ |
|||
var oldAnchorIndex = _anchorIndex; |
|||
var oldSelectedIndex = _selectedIndex; |
|||
var indexesChanged = false; |
|||
|
|||
if (operation.SelectedIndex == -1 && LostSelection is object) |
|||
{ |
|||
operation.UpdateCount++; |
|||
LostSelection?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
|
|||
_selectedIndex = operation.SelectedIndex; |
|||
_anchorIndex = operation.AnchorIndex; |
|||
|
|||
if (operation.SelectedRanges is object) |
|||
{ |
|||
indexesChanged |= CommitSelect(operation.SelectedRanges) > 0; |
|||
} |
|||
|
|||
if (operation.DeselectedRanges is object) |
|||
{ |
|||
indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0; |
|||
} |
|||
|
|||
if (SelectionChanged is object || _untypedSelectionChanged is object) |
|||
{ |
|||
IReadOnlyList<IndexRange>? deselected = operation.DeselectedRanges; |
|||
IReadOnlyList<IndexRange>? selected = operation.SelectedRanges; |
|||
|
|||
if (SingleSelect && oldSelectedIndex != _selectedIndex) |
|||
{ |
|||
if (oldSelectedIndex != -1) |
|||
{ |
|||
deselected = new[] { new IndexRange(oldSelectedIndex) }; |
|||
} |
|||
|
|||
if (_selectedIndex != -1) |
|||
{ |
|||
selected = new[] { new IndexRange(_selectedIndex) }; |
|||
} |
|||
} |
|||
|
|||
if (deselected?.Count > 0 || selected?.Count > 0) |
|||
{ |
|||
var deselectedSource = operation.IsSourceUpdate ? null : ItemsView; |
|||
var e = new SelectionModelSelectionChangedEventArgs<T>( |
|||
SelectedIndexes<T>.Create(deselected), |
|||
SelectedIndexes<T>.Create(selected), |
|||
SelectedItems<T>.Create(deselected, deselectedSource), |
|||
SelectedItems<T>.Create(selected, ItemsView)); |
|||
SelectionChanged?.Invoke(this, e); |
|||
_untypedSelectionChanged?.Invoke(this, e); |
|||
} |
|||
} |
|||
|
|||
if (oldSelectedIndex != _selectedIndex) |
|||
{ |
|||
indexesChanged = true; |
|||
RaisePropertyChanged(nameof(SelectedIndex)); |
|||
RaisePropertyChanged(nameof(SelectedItem)); |
|||
} |
|||
|
|||
if (oldAnchorIndex != _anchorIndex) |
|||
{ |
|||
indexesChanged = true; |
|||
RaisePropertyChanged(nameof(AnchorIndex)); |
|||
} |
|||
|
|||
if (indexesChanged) |
|||
{ |
|||
RaisePropertyChanged(nameof(SelectedIndexes)); |
|||
RaisePropertyChanged(nameof(SelectedItems)); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_operation = null; |
|||
} |
|||
} |
|||
|
|||
public struct BatchUpdateOperation : IDisposable |
|||
{ |
|||
private readonly SelectionModel<T> _owner; |
|||
private bool _isDisposed; |
|||
|
|||
public BatchUpdateOperation(SelectionModel<T> owner) |
|||
{ |
|||
_owner = owner; |
|||
_isDisposed = false; |
|||
owner.BeginBatchUpdate(); |
|||
} |
|||
|
|||
internal Operation Operation => _owner._operation!; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (!_isDisposed) |
|||
{ |
|||
_owner?.EndBatchUpdate(); |
|||
_isDisposed = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal class Operation |
|||
{ |
|||
public Operation(SelectionModel<T> owner) |
|||
{ |
|||
AnchorIndex = owner.AnchorIndex; |
|||
SelectedIndex = owner.SelectedIndex; |
|||
} |
|||
|
|||
public int UpdateCount { get; set; } |
|||
public bool IsSourceUpdate { get; set; } |
|||
public int AnchorIndex { get; set; } |
|||
public int SelectedIndex { get; set; } |
|||
public List<IndexRange>? SelectedRanges { get; set; } |
|||
public List<IndexRange>? DeselectedRanges { get; set; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public class SelectionModelIndexesChangedEventArgs : EventArgs |
|||
{ |
|||
public SelectionModelIndexesChangedEventArgs(int startIndex, int delta) |
|||
{ |
|||
StartIndex = startIndex; |
|||
Delta = delta; |
|||
} |
|||
|
|||
public int StartIndex { get; } |
|||
public int Delta { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls.Selection; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public abstract class SelectionModelSelectionChangedEventArgs : EventArgs |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the indexes of the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public abstract IReadOnlyList<int> DeselectedIndexes { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the indexes of the items that were added to the selection.
|
|||
/// </summary>
|
|||
public abstract IReadOnlyList<int> SelectedIndexes { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<object?> DeselectedItems => GetUntypedDeselectedItems(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were added to the selection.
|
|||
/// </summary>
|
|||
public IReadOnlyList<object?> SelectedItems => GetUntypedSelectedItems(); |
|||
|
|||
protected abstract IReadOnlyList<object?> GetUntypedDeselectedItems(); |
|||
protected abstract IReadOnlyList<object?> GetUntypedSelectedItems(); |
|||
} |
|||
|
|||
public class SelectionModelSelectionChangedEventArgs<T> : SelectionModelSelectionChangedEventArgs |
|||
{ |
|||
private IReadOnlyList<object?>? _deselectedItems; |
|||
private IReadOnlyList<object?>? _selectedItems; |
|||
|
|||
public SelectionModelSelectionChangedEventArgs( |
|||
IReadOnlyList<int>? deselectedIndices = null, |
|||
IReadOnlyList<int>? selectedIndices = null, |
|||
IReadOnlyList<T>? deselectedItems = null, |
|||
IReadOnlyList<T>? selectedItems = null) |
|||
{ |
|||
DeselectedIndexes = deselectedIndices ?? Array.Empty<int>(); |
|||
SelectedIndexes = selectedIndices ?? Array.Empty<int>(); |
|||
DeselectedItems = deselectedItems ?? Array.Empty<T>(); |
|||
SelectedItems = selectedItems ?? Array.Empty<T>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the indexes of the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public override IReadOnlyList<int> DeselectedIndexes { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the indexes of the items that were added to the selection.
|
|||
/// </summary>
|
|||
public override IReadOnlyList<int> SelectedIndexes { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were removed from the selection.
|
|||
/// </summary>
|
|||
public new IReadOnlyList<T> DeselectedItems { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the items that were added to the selection.
|
|||
/// </summary>
|
|||
public new IReadOnlyList<T> SelectedItems { get; } |
|||
|
|||
protected override IReadOnlyList<object?> GetUntypedDeselectedItems() |
|||
{ |
|||
return _deselectedItems ??= (DeselectedItems as IReadOnlyList<object?>) ?? |
|||
new SelectedItems<T>.Untyped(DeselectedItems); |
|||
} |
|||
|
|||
protected override IReadOnlyList<object?> GetUntypedSelectedItems() |
|||
{ |
|||
return _selectedItems ??= (SelectedItems as IReadOnlyList<object?>) ?? |
|||
new SelectedItems<T>.Untyped(SelectedItems); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,286 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Selection |
|||
{ |
|||
public abstract class SelectionNodeBase<T> : ICollectionChangedListener |
|||
{ |
|||
private IEnumerable<T>? _source; |
|||
private bool _rangesEnabled; |
|||
private List<IndexRange>? _ranges; |
|||
private int _collectionChanging; |
|||
|
|||
public virtual IEnumerable<T>? Source |
|||
{ |
|||
get => _source; |
|||
set |
|||
{ |
|||
if (_source != value) |
|||
{ |
|||
ItemsView?.RemoveListener(this); |
|||
_source = value; |
|||
ItemsView = value is object ? ItemsSourceView<T>.GetOrCreate(value) : null; |
|||
ItemsView?.AddListener(this); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected bool IsSourceCollectionChanging => _collectionChanging > 0; |
|||
|
|||
protected bool RangesEnabled |
|||
{ |
|||
get => _rangesEnabled; |
|||
set |
|||
{ |
|||
if (_rangesEnabled != value) |
|||
{ |
|||
_rangesEnabled = value; |
|||
|
|||
if (!_rangesEnabled) |
|||
{ |
|||
_ranges = null; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal ItemsSourceView<T>? ItemsView { get; set; } |
|||
|
|||
internal IReadOnlyList<IndexRange> Ranges |
|||
{ |
|||
get |
|||
{ |
|||
if (!RangesEnabled) |
|||
{ |
|||
throw new InvalidOperationException("Ranges not enabled."); |
|||
} |
|||
|
|||
return _ranges ??= new List<IndexRange>(); |
|||
} |
|||
} |
|||
|
|||
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
++_collectionChanging; |
|||
} |
|||
|
|||
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
OnSourceCollectionChanged(e); |
|||
} |
|||
|
|||
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (--_collectionChanging == 0) |
|||
{ |
|||
OnSourceCollectionChangeFinished(); |
|||
} |
|||
} |
|||
|
|||
protected abstract void OnSourceCollectionChangeFinished(); |
|||
|
|||
private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta); |
|||
|
|||
private protected abstract void OnSourceReset(); |
|||
|
|||
private protected abstract void OnSelectionChanged(IReadOnlyList<T> deselectedItems); |
|||
|
|||
private protected int CommitSelect(IndexRange range) |
|||
{ |
|||
if (RangesEnabled) |
|||
{ |
|||
_ranges ??= new List<IndexRange>(); |
|||
return IndexRange.Add(_ranges, range); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
private protected int CommitSelect(IReadOnlyList<IndexRange> ranges) |
|||
{ |
|||
if (RangesEnabled) |
|||
{ |
|||
_ranges ??= new List<IndexRange>(); |
|||
return IndexRange.Add(_ranges, ranges); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
private protected int CommitDeselect(IndexRange range) |
|||
{ |
|||
if (RangesEnabled) |
|||
{ |
|||
_ranges ??= new List<IndexRange>(); |
|||
return IndexRange.Remove(_ranges, range); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
private protected int CommitDeselect(IReadOnlyList<IndexRange> ranges) |
|||
{ |
|||
if (RangesEnabled && _ranges is object) |
|||
{ |
|||
return IndexRange.Remove(_ranges, ranges); |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
private protected virtual CollectionChangeState OnItemsAdded(int index, IList items) |
|||
{ |
|||
var count = items.Count; |
|||
var shifted = false; |
|||
|
|||
if (_ranges is object) |
|||
{ |
|||
List<IndexRange>? toAdd = null; |
|||
|
|||
for (var i = 0; i < Ranges!.Count; ++i) |
|||
{ |
|||
var range = Ranges[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 ??= new List<IndexRange>()).Add(before); |
|||
begin = index; |
|||
} |
|||
|
|||
// Shift the range to the right
|
|||
_ranges[i] = new IndexRange(begin + count, range.End + count); |
|||
shifted = true; |
|||
} |
|||
} |
|||
|
|||
if (toAdd is object) |
|||
{ |
|||
foreach (var range in toAdd) |
|||
{ |
|||
IndexRange.Add(_ranges, range); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new CollectionChangeState |
|||
{ |
|||
ShiftIndex = index, |
|||
ShiftDelta = shifted ? count : 0, |
|||
}; |
|||
} |
|||
|
|||
private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items) |
|||
{ |
|||
var count = items.Count; |
|||
var removedRange = new IndexRange(index, index + count - 1); |
|||
bool shifted = false; |
|||
List<T>? removed = null; |
|||
|
|||
if (_ranges is object) |
|||
{ |
|||
var deselected = new List<IndexRange>(); |
|||
|
|||
if (IndexRange.Remove(_ranges, removedRange, deselected) > 0) |
|||
{ |
|||
removed = new List<T>(); |
|||
|
|||
foreach (var range in deselected) |
|||
{ |
|||
for (var i = range.Begin; i <= range.End; ++i) |
|||
{ |
|||
#pragma warning disable CS8604
|
|||
removed.Add((T)items[i - index]); |
|||
#pragma warning restore CS8604
|
|||
} |
|||
} |
|||
} |
|||
|
|||
for (var i = 0; i < Ranges!.Count; ++i) |
|||
{ |
|||
var existing = Ranges[i]; |
|||
|
|||
if (existing.End > removedRange.Begin) |
|||
{ |
|||
_ranges[i] = new IndexRange(existing.Begin - count, existing.End - count); |
|||
shifted = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new CollectionChangeState |
|||
{ |
|||
ShiftIndex = index, |
|||
ShiftDelta = shifted ? -count : 0, |
|||
RemovedItems = removed, |
|||
}; |
|||
} |
|||
|
|||
private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
var shiftDelta = 0; |
|||
var shiftIndex = -1; |
|||
List<T>? removed = null; |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
{ |
|||
var change = OnItemsAdded(e.NewStartingIndex, e.NewItems); |
|||
shiftIndex = change.ShiftIndex; |
|||
shiftDelta = change.ShiftDelta; |
|||
break; |
|||
} |
|||
case NotifyCollectionChangedAction.Remove: |
|||
{ |
|||
var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems); |
|||
shiftIndex = change.ShiftIndex; |
|||
shiftDelta = change.ShiftDelta; |
|||
removed = change.RemovedItems; |
|||
break; |
|||
} |
|||
case NotifyCollectionChangedAction.Replace: |
|||
{ |
|||
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems); |
|||
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems); |
|||
shiftIndex = removeChange.ShiftIndex; |
|||
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta; |
|||
removed = removeChange.RemovedItems; |
|||
} |
|||
break; |
|||
case NotifyCollectionChangedAction.Reset: |
|||
OnSourceReset(); |
|||
break; |
|||
} |
|||
|
|||
if (shiftDelta != 0) |
|||
{ |
|||
OnIndexesChanged(shiftIndex, shiftDelta); |
|||
} |
|||
|
|||
if (removed is object) |
|||
{ |
|||
OnSelectionChanged(removed); |
|||
} |
|||
} |
|||
|
|||
private protected struct CollectionChangeState |
|||
{ |
|||
public int ShiftIndex; |
|||
public int ShiftDelta; |
|||
public List<T>? RemovedItems; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Threading; |
|||
using Avalonia.Utilities; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
internal interface ICollectionChangedListener |
|||
{ |
|||
void PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e); |
|||
void Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e); |
|||
void PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e); |
|||
} |
|||
|
|||
internal class CollectionChangedEventManager : IWeakSubscriber<NotifyCollectionChangedEventArgs> |
|||
{ |
|||
public static CollectionChangedEventManager Instance { get; } = new CollectionChangedEventManager(); |
|||
|
|||
private ConditionalWeakTable<INotifyCollectionChanged, List<WeakReference<ICollectionChangedListener>>> _entries = |
|||
new ConditionalWeakTable<INotifyCollectionChanged, List<WeakReference<ICollectionChangedListener>>>(); |
|||
|
|||
private CollectionChangedEventManager() |
|||
{ |
|||
} |
|||
|
|||
public void AddListener(INotifyCollectionChanged collection, ICollectionChangedListener listener) |
|||
{ |
|||
collection = collection ?? throw new ArgumentNullException(nameof(collection)); |
|||
listener = listener ?? throw new ArgumentNullException(nameof(listener)); |
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
|
|||
if (!_entries.TryGetValue(collection, out var listeners)) |
|||
{ |
|||
listeners = new List<WeakReference<ICollectionChangedListener>>(); |
|||
_entries.Add(collection, listeners); |
|||
WeakSubscriptionManager.Subscribe( |
|||
collection, |
|||
nameof(INotifyCollectionChanged.CollectionChanged), |
|||
this); |
|||
} |
|||
|
|||
//if (listeners.Contains(listener))
|
|||
//{
|
|||
// throw new InvalidOperationException(
|
|||
// "Collection listener already added for this collection/listener combination.");
|
|||
//}
|
|||
|
|||
listeners.Add(new WeakReference<ICollectionChangedListener>(listener)); |
|||
} |
|||
|
|||
public void RemoveListener(INotifyCollectionChanged collection, ICollectionChangedListener listener) |
|||
{ |
|||
collection = collection ?? throw new ArgumentNullException(nameof(collection)); |
|||
listener = listener ?? throw new ArgumentNullException(nameof(listener)); |
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
|
|||
if (_entries.TryGetValue(collection, out var listeners)) |
|||
{ |
|||
for (var i = 0; i < listeners.Count; ++i) |
|||
{ |
|||
if (listeners[i].TryGetTarget(out var target) && target == listener) |
|||
{ |
|||
listeners.RemoveAt(i); |
|||
|
|||
if (listeners.Count == 0) |
|||
{ |
|||
WeakSubscriptionManager.Unsubscribe( |
|||
collection, |
|||
nameof(INotifyCollectionChanged.CollectionChanged), |
|||
this); |
|||
_entries.Remove(collection); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
} |
|||
} |
|||
|
|||
throw new InvalidOperationException( |
|||
"Collection listener not registered for this collection/listener combination."); |
|||
} |
|||
|
|||
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
static void Notify( |
|||
INotifyCollectionChanged incc, |
|||
NotifyCollectionChangedEventArgs args, |
|||
List<WeakReference<ICollectionChangedListener>> listeners) |
|||
{ |
|||
foreach (var l in listeners) |
|||
{ |
|||
if (l.TryGetTarget(out var target)) |
|||
{ |
|||
target.PreChanged(incc, args); |
|||
} |
|||
} |
|||
|
|||
foreach (var l in listeners) |
|||
{ |
|||
if (l.TryGetTarget(out var target)) |
|||
{ |
|||
target.Changed(incc, args); |
|||
} |
|||
} |
|||
|
|||
foreach (var l in listeners) |
|||
{ |
|||
if (l.TryGetTarget(out var target)) |
|||
{ |
|||
target.PostChanged(incc, args); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (sender is INotifyCollectionChanged incc && _entries.TryGetValue(incc, out var listeners)) |
|||
{ |
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
Notify(incc, e, listeners); |
|||
} |
|||
else |
|||
{ |
|||
var inccCapture = incc; |
|||
var eCapture = e; |
|||
var listenersCapture = listeners; |
|||
Dispatcher.UIThread.Post(() => Notify(inccCapture, eCapture, listenersCapture)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue