Browse Source

Merge pull request #10589 from AvaloniaUI/refactor/selection-model

Port SelectionModel changes from TreeDataGrid.
gh-readonly-queue/master/pr-10572-4d7e453d584c5197d53c7549fe4359325154a3c0
Steven Kirk 3 years ago
committed by GitHub
parent
commit
ef129ec0bc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 23
      src/Avalonia.Controls/ItemsSourceView.cs
  3. 2
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  4. 17
      src/Avalonia.Controls/Selection/SelectedItems.cs
  5. 91
      src/Avalonia.Controls/Selection/SelectionModel.cs
  6. 8
      src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs
  7. 311
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs

1
.gitignore

@ -102,6 +102,7 @@ csx
AppPackages/
# NCrunch
.NCrunch_*/
_NCrunch_*/
*.ncrunchsolution.user
nCrunchTemp_*

23
src/Avalonia.Controls/ItemsSourceView.cs

@ -27,6 +27,7 @@ namespace Avalonia.Controls
private readonly IList _inner;
private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler? _preCollectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged;
private bool _listening;
@ -70,7 +71,7 @@ namespace Avalonia.Controls
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </remarks>
internal bool HasKeyIndexMapping => false;
@ -92,6 +93,25 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
/// </summary>
internal event NotifyCollectionChangedEventHandler? PreCollectionChanged
{
add
{
AddListenerIfNecessary();
_preCollectionChanged += value;
}
remove
{
_preCollectionChanged -= value;
RemoveListenerIfNecessary();
}
}
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
@ -229,6 +249,7 @@ namespace Avalonia.Controls
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
_preCollectionChanged?.Invoke(this, e);
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)

2
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@ -203,7 +203,7 @@ namespace Avalonia.Controls.Selection
}
}
private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{

17
src/Avalonia.Controls/Selection/SelectedItems.cs

@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Controls.Selection
{
internal class SelectedItems<T> : IReadOnlyList<T>
internal class SelectedItems<T> : IReadOnlyList<T?>
{
private readonly SelectionModel<T>? _owner;
private readonly ItemsSourceView<T>? _items;
@ -19,12 +19,9 @@ namespace Avalonia.Controls.Selection
_items = items;
}
[MaybeNull]
public T this[int index]
public T? this[int index]
{
#pragma warning disable CS8766
get
#pragma warning restore CS8766
{
if (index >= Count)
{
@ -64,15 +61,13 @@ namespace Avalonia.Controls.Selection
private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView;
private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges;
public IEnumerator<T> GetEnumerator()
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
@ -83,9 +78,7 @@ namespace Avalonia.Controls.Selection
{
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
}
}
}
@ -102,8 +95,8 @@ namespace Avalonia.Controls.Selection
public class Untyped : IReadOnlyList<object?>
{
private readonly IReadOnlyList<T> _source;
public Untyped(IReadOnlyList<T> source) => _source = source;
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();

91
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -19,6 +19,7 @@ namespace Avalonia.Controls.Selection
private SelectedItems<T>.Untyped? _selectedItemsUntyped;
private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
private IList? _initSelectedItems;
private bool _isSourceCollectionChanging;
public SelectionModel()
{
@ -55,7 +56,7 @@ namespace Avalonia.Controls.Selection
if (RangesEnabled && _selectedIndex >= 0)
{
CommitSelect(new IndexRange(_selectedIndex));
CommitSelect(_selectedIndex, _selectedIndex);
}
RaisePropertyChanged(nameof(SingleSelect));
@ -80,7 +81,7 @@ namespace Avalonia.Controls.Selection
{
get
{
if (ItemsView is object)
if (ItemsView is not null)
{
return GetItemAt(_selectedIndex);
}
@ -93,21 +94,19 @@ namespace Avalonia.Controls.Selection
}
set
{
if (ItemsView is object)
if (ItemsView is not null)
{
SelectedIndex = ItemsView.IndexOf(value!);
}
else
{
Clear();
#pragma warning disable CS8601
SetInitSelectedItems(new T[] { value });
#pragma warning restore CS8601
SetInitSelectedItems(new T[] { value! });
}
}
}
public IReadOnlyList<T> SelectedItems
public IReadOnlyList<T?> SelectedItems
{
get
{
@ -206,7 +205,7 @@ namespace Avalonia.Controls.Selection
{
// If the collection is currently changing, commit the update when the
// collection change finishes.
if (!IsSourceCollectionChanging)
if (!_isSourceCollectionChanging)
{
CommitOperation(_operation);
}
@ -278,7 +277,7 @@ namespace Avalonia.Controls.Selection
{
if (base.Source != value)
{
if (_operation is object)
if (_operation is not null)
{
throw new InvalidOperationException("Cannot change source while update is in progress.");
}
@ -296,7 +295,7 @@ namespace Avalonia.Controls.Selection
{
update.Operation.IsSourceUpdate = true;
if (_initSelectedItems is object && ItemsView is object)
if (_initSelectedItems is object && ItemsView is not null)
{
foreach (T i in _initSelectedItems)
{
@ -315,17 +314,23 @@ namespace Avalonia.Controls.Selection
}
}
private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
{
IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta));
}
private protected override void OnSourceReset()
protected override void OnSourceCollectionChangeStarted()
{
base.OnSourceCollectionChangeStarted();
_isSourceCollectionChanging = true;
}
protected override void OnSourceReset()
{
_selectedIndex = _anchorIndex = -1;
CommitDeselect(new IndexRange(0, int.MaxValue));
CommitDeselect(0, int.MaxValue);
if (SourceReset is object)
if (SourceReset is not null)
{
SourceReset.Invoke(this, EventArgs.Empty);
}
@ -339,7 +344,7 @@ namespace Avalonia.Controls.Selection
}
}
private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems)
protected override void OnSelectionRemoved(int index, int count, IReadOnlyList<T> deselectedItems)
{
// Note: We're *not* putting this in a using scope. A collection update is still in progress
// so the operation won't get committed by normal means: we have to commit it manually.
@ -347,7 +352,7 @@ namespace Avalonia.Controls.Selection
update.Operation.DeselectedItems = deselectedItems;
if (_selectedIndex == -1 && LostSelection is object)
if (_selectedIndex == -1 && LostSelection is not null)
{
LostSelection(this, EventArgs.Empty);
}
@ -357,7 +362,7 @@ namespace Avalonia.Controls.Selection
CommitOperation(update.Operation, raisePropertyChanged: false);
}
private protected override CollectionChangeState OnItemsAdded(int index, IList items)
protected override CollectionChangeState OnItemsAdded(int index, IList items)
{
var count = items.Count;
var shifted = SelectedIndex >= index;
@ -420,7 +425,7 @@ namespace Avalonia.Controls.Selection
};
}
private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (_operation?.UpdateCount > 0)
{
@ -451,6 +456,16 @@ namespace Avalonia.Controls.Selection
}
}
private protected void SetInitSelectedItems(IList items)
{
if (Source is object)
{
throw new InvalidOperationException("Cannot set init selected items when Source is set.");
}
_initSelectedItems = items;
}
private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
if (!base.IsValidCollectionChange(e))
@ -474,19 +489,11 @@ namespace Avalonia.Controls.Selection
return true;
}
private protected void SetInitSelectedItems(IList items)
{
if (Source is object)
{
throw new InvalidOperationException("Cannot set init selected items when Source is set.");
}
_initSelectedItems = items;
}
protected override void OnSourceCollectionChangeFinished()
{
if (_operation is object)
_isSourceCollectionChanging = false;
if (_operation is not null)
{
CommitOperation(_operation);
}
@ -575,7 +582,7 @@ namespace Avalonia.Controls.Selection
{
index = Math.Max(index, -1);
if (ItemsView is object && index >= ItemsView.Count)
if (ItemsView is not null && index >= ItemsView.Count)
{
index = -1;
}
@ -585,7 +592,7 @@ namespace Avalonia.Controls.Selection
private IndexRange CoerceRange(int start, int end)
{
var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue;
var max = ItemsView is not null ? ItemsView.Count - 1 : int.MaxValue;
if (start > max || (start < 0 && end < 0))
{
@ -643,7 +650,7 @@ namespace Avalonia.Controls.Selection
var oldSelectedIndex = _selectedIndex;
var indexesChanged = false;
if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection)
if (operation.SelectedIndex == -1 && LostSelection is not null && !operation.SkipLostSelection)
{
operation.UpdateCount++;
LostSelection?.Invoke(this, EventArgs.Empty);
@ -652,17 +659,23 @@ namespace Avalonia.Controls.Selection
_selectedIndex = operation.SelectedIndex;
_anchorIndex = operation.AnchorIndex;
if (operation.SelectedRanges is object)
if (operation.SelectedRanges is not null)
{
indexesChanged |= CommitSelect(operation.SelectedRanges) > 0;
foreach (var range in operation.SelectedRanges)
{
indexesChanged |= CommitSelect(range.Begin, range.End) > 0;
}
}
if (operation.DeselectedRanges is object)
if (operation.DeselectedRanges is not null)
{
indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0;
foreach (var range in operation.DeselectedRanges)
{
indexesChanged |= CommitDeselect(range.Begin, range.End) > 0;
}
}
if (SelectionChanged is object || _untypedSelectionChanged is object)
if (SelectionChanged is not null || _untypedSelectionChanged is not null)
{
IReadOnlyList<IndexRange>? deselected = operation.DeselectedRanges;
IReadOnlyList<IndexRange>? selected = operation.SelectedRanges;
@ -690,14 +703,14 @@ namespace Avalonia.Controls.Selection
// CollectionChanged event. LostFocus may have caused another item to have been
// selected, but it can't have caused a deselection (as it was called due to
// selection being lost) so we're ok to discard `deselected` here.
var deselectedItems = operation.DeselectedItems ??
var deselectedItems = (IReadOnlyList<T?>?)operation.DeselectedItems ??
SelectedItems<T>.Create(deselected, deselectedSource);
var e = new SelectionModelSelectionChangedEventArgs<T>(
SelectedIndexes<T>.Create(deselected),
SelectedIndexes<T>.Create(selected),
deselectedItems,
SelectedItems<T>.Create(selected, ItemsView));
SelectedItems<T>.Create(selected, Source is not null ? ItemsView : null));
SelectionChanged?.Invoke(this, e);
_untypedSelectionChanged?.Invoke(this, e);
}

8
src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs

@ -39,8 +39,8 @@ namespace Avalonia.Controls.Selection
public SelectionModelSelectionChangedEventArgs(
IReadOnlyList<int>? deselectedIndices = null,
IReadOnlyList<int>? selectedIndices = null,
IReadOnlyList<T>? deselectedItems = null,
IReadOnlyList<T>? selectedItems = null)
IReadOnlyList<T?>? deselectedItems = null,
IReadOnlyList<T?>? selectedItems = null)
{
DeselectedIndexes = deselectedIndices ?? Array.Empty<int>();
SelectedIndexes = selectedIndices ?? Array.Empty<int>();
@ -61,12 +61,12 @@ namespace Avalonia.Controls.Selection
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public new IReadOnlyList<T> DeselectedItems { get; }
public new IReadOnlyList<T?> DeselectedItems { get; }
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public new IReadOnlyList<T> SelectedItems { get; }
public new IReadOnlyList<T?> SelectedItems { get; }
protected override IReadOnlyList<object?> GetUntypedDeselectedItems()
{

311
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -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; }
}
}
}

Loading…
Cancel
Save