csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
316 lines
10 KiB
316 lines
10 KiB
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using Avalonia.Controls.Utils;
|
|
|
|
namespace Avalonia.Controls.Selection
|
|
{
|
|
public abstract class SelectionNodeBase<T> : ICollectionChangedListener
|
|
{
|
|
private IEnumerable? _source;
|
|
private bool _rangesEnabled;
|
|
private List<IndexRange>? _ranges;
|
|
private int _collectionChanging;
|
|
|
|
protected IEnumerable? 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)
|
|
{
|
|
removed.Add((T)items[i - index]!);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
if (!IsValidCollectionChange(e))
|
|
{
|
|
return;
|
|
}
|
|
|
|
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 virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
|
|
{
|
|
// If the selection is modified in a CollectionChanged handler before the selection
|
|
// model's CollectionChanged handler has had chance to run then we can end up with
|
|
// a selected index that refers to the *new* state of the Source intermixed with
|
|
// indexes that reference an old state of the source.
|
|
//
|
|
// There's not much we can do in this situation, so detect whether shifting the
|
|
// current selected indexes would result in an invalid index in the source, and if
|
|
// so bail.
|
|
//
|
|
// See unit test Handles_Selection_Made_In_CollectionChanged for more details.
|
|
if (ItemsView is not null &&
|
|
RangesEnabled &&
|
|
Ranges.Count > 0 &&
|
|
e.Action == NotifyCollectionChangedAction.Add)
|
|
{
|
|
var lastIndex = Ranges[Ranges.Count - 1].End;
|
|
|
|
if (e.NewStartingIndex <= lastIndex)
|
|
{
|
|
return lastIndex + e.NewItems!.Count < ItemsView.Count;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private protected struct CollectionChangeState
|
|
{
|
|
public int ShiftIndex;
|
|
public int ShiftDelta;
|
|
public List<T>? RemovedItems;
|
|
}
|
|
}
|
|
}
|
|
|