|
|
@@ -2,37 +2,62 @@
|
|
|
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
|
|
|
+ /// <summary>
|
|
|
+ /// Base class for selection models.
|
|
|
+ /// </summary>
|
|
|
+ /// <typeparam name="T">The type of the element being selected.</typeparam>
|
|
|
+ public abstract class SelectionNodeBase<T>
|
|
|
{
|
|
|
private IEnumerable? _source;
|
|
|
private bool _rangesEnabled;
|
|
|
private List<IndexRange>? _ranges;
|
|
|
- private int _collectionChanging;
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets the source collection.
|
|
|
+ /// </summary>
|
|
|
protected IEnumerable? Source
|
|
|
{
|
|
|
get => _source;
|
|
|
set
|
|
|
{
|
|
|
+ void OnPreChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeStarted();
|
|
|
+ void OnChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChanged(e);
|
|
|
+ void OnPostChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeFinished();
|
|
|
+
|
|
|
if (_source != value)
|
|
|
{
|
|
|
- if (ItemsView?.Inner is INotifyCollectionChanged inccOld)
|
|
|
- CollectionChangedEventManager.Instance.RemoveListener(inccOld, this);
|
|
|
+ if (ItemsView is not null)
|
|
|
+ {
|
|
|
+ ItemsView.PreCollectionChanged -= OnPreChanged;
|
|
|
+ ItemsView.CollectionChanged -= OnChanged;
|
|
|
+ ItemsView.PostCollectionChanged -= OnPostChanged;
|
|
|
+ }
|
|
|
+
|
|
|
_source = value;
|
|
|
- ItemsView = value is object ? ItemsSourceView.GetOrCreate<T>(value) : null;
|
|
|
- if (ItemsView?.Inner is INotifyCollectionChanged inccNew)
|
|
|
- CollectionChangedEventManager.Instance.AddListener(inccNew, this);
|
|
|
+ ItemsView = value is not null ? ItemsSourceView.GetOrCreate<T>(value) : null;
|
|
|
+
|
|
|
+ if (ItemsView is not null)
|
|
|
+ {
|
|
|
+ ItemsView.PreCollectionChanged += OnPreChanged;
|
|
|
+ ItemsView.CollectionChanged += OnChanged;
|
|
|
+ ItemsView.PostCollectionChanged += OnPostChanged;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- protected bool IsSourceCollectionChanging => _collectionChanging > 0;
|
|
|
+ /// <summary>
|
|
|
+ /// Gets an <see cref="ItemsSourceView{T}"/> of the <see cref="Source"/>.
|
|
|
+ /// </summary>
|
|
|
+ protected internal ItemsSourceView<T>? ItemsView { get; set; }
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets a value indicating whether range selection is currently enabled for
|
|
|
+ /// the selection node.
|
|
|
+ /// </summary>
|
|
|
protected bool RangesEnabled
|
|
|
{
|
|
|
get => _rangesEnabled;
|
|
|
@@ -50,8 +75,6 @@ namespace Avalonia.Controls.Selection
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- internal ItemsSourceView<T>? ItemsView { get; set; }
|
|
|
-
|
|
|
internal IReadOnlyList<IndexRange> Ranges
|
|
|
{
|
|
|
get
|
|
|
@@ -65,81 +88,170 @@ namespace Avalonia.Controls.Selection
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
|
|
|
+ /// <summary>
|
|
|
+ /// Called when the source collection starts changing.
|
|
|
+ /// </summary>
|
|
|
+ protected virtual void OnSourceCollectionChangeStarted()
|
|
|
{
|
|
|
- ++_collectionChanging;
|
|
|
}
|
|
|
|
|
|
- void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
|
|
|
+ /// <summary>
|
|
|
+ /// Called when the <see cref="Source"/> collection changes.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="e">The details of the collection change.</param>
|
|
|
+ /// <remarks>
|
|
|
+ /// The implementation in <see cref="SelectionNodeBase{T}"/> calls
|
|
|
+ /// <see cref="OnItemsAdded(int, IList)"/> and <see cref="OnItemsRemoved(int, IList)"/>
|
|
|
+ /// in order to calculate how the collection change affects the currently selected items.
|
|
|
+ /// It then calls <see cref="OnIndexesChanged(int, int)"/> and
|
|
|
+ /// <see cref="OnSelectionRemoved(int, int, IReadOnlyList{T})"/> if necessary, according
|
|
|
+ /// to the <see cref="CollectionChangeState"/> returned by those methods.
|
|
|
+ ///
|
|
|
+ /// Override this method and <see cref="OnSourceCollectionChangeFinished"/> to provide
|
|
|
+ /// custom handling of source collection changes.
|
|
|
+ /// </remarks>
|
|
|
+ protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
|
|
|
{
|
|
|
- OnSourceCollectionChanged(e);
|
|
|
- }
|
|
|
+ var shiftDelta = 0;
|
|
|
+ var shiftIndex = -1;
|
|
|
+ List<T>? removed = null;
|
|
|
|
|
|
- void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
|
|
|
- {
|
|
|
- if (--_collectionChanging == 0)
|
|
|
+ if (!IsValidCollectionChange(e))
|
|
|
{
|
|
|
- OnSourceCollectionChangeFinished();
|
|
|
+ return;
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- 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)
|
|
|
+ switch (e.Action)
|
|
|
{
|
|
|
- _ranges ??= new List<IndexRange>();
|
|
|
- return IndexRange.Add(_ranges, range);
|
|
|
+ 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:
|
|
|
+ case NotifyCollectionChangedAction.Move:
|
|
|
+ {
|
|
|
+ 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;
|
|
|
}
|
|
|
|
|
|
- return 0;
|
|
|
+ if (shiftDelta != 0)
|
|
|
+ OnIndexesChanged(shiftIndex, shiftDelta);
|
|
|
+ if (removed is not null)
|
|
|
+ OnSelectionRemoved(shiftIndex, -shiftDelta, removed);
|
|
|
}
|
|
|
|
|
|
- private protected int CommitSelect(IReadOnlyList<IndexRange> ranges)
|
|
|
+ /// <summary>
|
|
|
+ /// Called when the source collection has finished changing, and all CollectionChanged
|
|
|
+ /// handlers have run.
|
|
|
+ /// </summary>
|
|
|
+ /// <remarks>
|
|
|
+ /// Override this method to respond to the end of a collection change instead of acting at
|
|
|
+ /// the end of <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
|
|
|
+ /// in order to ensure that all UI subscribers to the source collection change event have
|
|
|
+ /// had chance to run.
|
|
|
+ /// </remarks>
|
|
|
+ protected virtual void OnSourceCollectionChangeFinished()
|
|
|
{
|
|
|
- if (RangesEnabled)
|
|
|
- {
|
|
|
- _ranges ??= new List<IndexRange>();
|
|
|
- return IndexRange.Add(_ranges, ranges);
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- return 0;
|
|
|
+ /// <summary>
|
|
|
+ /// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
|
|
|
+ /// detailing the indexes changed by the collection changing.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="shiftIndex">The first index that was shifted.</param>
|
|
|
+ /// <param name="shiftDelta">
|
|
|
+ /// If positive, the number of items inserted, or if negative the number of items removed.
|
|
|
+ /// </param>
|
|
|
+ protected virtual void OnIndexesChanged(int shiftIndex, int shiftDelta)
|
|
|
+ {
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
|
|
|
+ /// on collection reset.
|
|
|
+ /// </summary>
|
|
|
+ protected abstract void OnSourceReset();
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
|
|
|
+ /// detailing the items removed by a collection change.
|
|
|
+ /// </summary>
|
|
|
+ protected virtual void OnSelectionRemoved(int index, int count, IReadOnlyList<T> deselectedItems)
|
|
|
+ {
|
|
|
}
|
|
|
|
|
|
- private protected int CommitDeselect(IndexRange range)
|
|
|
+ /// <summary>
|
|
|
+ /// If <see cref="RangesEnabled"/>, adds the specified range to the selection.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="begin">The inclusive index of the start of the range to select.</param>
|
|
|
+ /// <param name="end">The inclusive index of the end of the range to select.</param>
|
|
|
+ /// <returns>The number of items selected.</returns>
|
|
|
+ protected int CommitSelect(int begin, int end)
|
|
|
{
|
|
|
if (RangesEnabled)
|
|
|
{
|
|
|
_ranges ??= new List<IndexRange>();
|
|
|
- return IndexRange.Remove(_ranges, range);
|
|
|
+ return IndexRange.Add(_ranges, new IndexRange(begin, end));
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
- private protected int CommitDeselect(IReadOnlyList<IndexRange> ranges)
|
|
|
+ /// <summary>
|
|
|
+ /// If <see cref="RangesEnabled"/>, removes the specified range from the selection.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="begin">The inclusive index of the start of the range to deselect.</param>
|
|
|
+ /// <param name="end">The inclusive index of the end of the range to deselect.</param>
|
|
|
+ /// <returns>The number of items selected.</returns>
|
|
|
+ protected int CommitDeselect(int begin, int end)
|
|
|
{
|
|
|
- if (RangesEnabled && _ranges is object)
|
|
|
+ if (RangesEnabled)
|
|
|
{
|
|
|
- return IndexRange.Remove(_ranges, ranges);
|
|
|
+ _ranges ??= new List<IndexRange>();
|
|
|
+ return IndexRange.Remove(_ranges, new IndexRange(begin, end));
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
- private protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
|
|
|
+ /// <summary>
|
|
|
+ /// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
|
|
|
+ /// when items are added to the source collection.
|
|
|
+ /// </summary>
|
|
|
+ /// <returns>
|
|
|
+ /// A <see cref="CollectionChangeState"/> struct containing the details of the adjusted
|
|
|
+ /// selection.
|
|
|
+ /// </returns>
|
|
|
+ /// <remarks>
|
|
|
+ /// The implementation in <see cref="SelectionNodeBase{T}"/> adjusts the selected ranges,
|
|
|
+ /// assigning new indexes. Override this method to carry out additional computation when
|
|
|
+ /// items are added.
|
|
|
+ /// </remarks>
|
|
|
+ protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
|
|
|
{
|
|
|
var count = items.Count;
|
|
|
var shifted = false;
|
|
|
|
|
|
- if (_ranges is object)
|
|
|
+ if (_ranges is not null)
|
|
|
{
|
|
|
List<IndexRange>? toAdd = null;
|
|
|
|
|
|
@@ -150,7 +262,7 @@ namespace Avalonia.Controls.Selection
|
|
|
// The range is after the inserted items, need to shift the range right
|
|
|
if (range.End >= index)
|
|
|
{
|
|
|
- int begin = range.Begin;
|
|
|
+ var begin = range.Begin;
|
|
|
|
|
|
// If the index left of newIndex is inside the range,
|
|
|
// Split the range and remember the left piece to add later
|
|
|
@@ -167,7 +279,7 @@ namespace Avalonia.Controls.Selection
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (toAdd is object)
|
|
|
+ if (toAdd is not null)
|
|
|
{
|
|
|
foreach (var range in toAdd)
|
|
|
{
|
|
|
@@ -183,14 +295,27 @@ namespace Avalonia.Controls.Selection
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
|
|
|
+ /// when items are removed from the source collection.
|
|
|
+ /// </summary>
|
|
|
+ /// <returns>
|
|
|
+ /// A <see cref="CollectionChangeState"/> struct containing the details of the adjusted
|
|
|
+ /// selection.
|
|
|
+ /// </returns>
|
|
|
+ /// <remarks>
|
|
|
+ /// The implementation in <see cref="SelectionNodeBase{T}"/> adjusts the selected ranges,
|
|
|
+ /// assigning new indexes. Override this method to carry out additional computation when
|
|
|
+ /// items are removed.
|
|
|
+ /// </remarks>
|
|
|
private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items)
|
|
|
{
|
|
|
var count = items.Count;
|
|
|
var removedRange = new IndexRange(index, index + count - 1);
|
|
|
- bool shifted = false;
|
|
|
+ var shifted = false;
|
|
|
List<T>? removed = null;
|
|
|
|
|
|
- if (_ranges is object)
|
|
|
+ if (_ranges is not null)
|
|
|
{
|
|
|
var deselected = new List<IndexRange>();
|
|
|
|
|
|
@@ -227,60 +352,6 @@ namespace Avalonia.Controls.Selection
|
|
|
};
|
|
|
}
|
|
|
|
|
|
- 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:
|
|
|
- case NotifyCollectionChangedAction.Move:
|
|
|
- {
|
|
|
- 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
|
|
|
@@ -309,11 +380,27 @@ namespace Avalonia.Controls.Selection
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- private protected struct CollectionChangeState
|
|
|
+ /// <summary>
|
|
|
+ /// Details the results of a collection change on the current selection;
|
|
|
+ /// </summary>
|
|
|
+ protected class CollectionChangeState
|
|
|
{
|
|
|
- public int ShiftIndex;
|
|
|
- public int ShiftDelta;
|
|
|
- public List<T>? RemovedItems;
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets the first index that was shifted as a result of the collection
|
|
|
+ /// changing.
|
|
|
+ /// </summary>
|
|
|
+ public int ShiftIndex { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets a value indicating how the indexes after <see cref="ShiftIndex"/>
|
|
|
+ /// were shifted.
|
|
|
+ /// </summary>
|
|
|
+ public int ShiftDelta { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets the items removed by the collection change, if any.
|
|
|
+ /// </summary>
|
|
|
+ public List<T>? RemovedItems { get; set; }
|
|
|
}
|
|
|
}
|
|
|
}
|