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