diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index 5e2fb32243..49f2c8d2f6 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -490,13 +490,9 @@ namespace Avalonia.Controls } public void OnSelectionInvalidatedDueToCollectionChange( - IEnumerable? removedItems) + IReadOnlyList? removedItems) { - var e = new SelectionModelSelectionChangedEventArgs( - Enumerable.Empty(), - Enumerable.Empty(), - removedItems ?? Enumerable.Empty(), - Enumerable.Empty()); + var e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null); OnSelectionChanged(e); } @@ -706,50 +702,19 @@ namespace Avalonia.Controls }); } - private void BeginOperation() - { - if (SelectionChanged != null) - { - _rootNode.BeginOperation(); - } - } + private void BeginOperation() => _rootNode.BeginOperation(); private void EndOperation() { - static IEnumerable? Concat(IEnumerable? a, IEnumerable b) - { - return a == null ? b : a.Concat(b); - } + var changes = new List(); + _rootNode.EndOperation(changes); SelectionModelSelectionChangedEventArgs? e = null; - - if (SelectionChanged != null) + + if (changes.Count > 0) { - IEnumerable? selectedIndices = null; - IEnumerable? deselectedIndices = null; - IEnumerable? selectedItems = null; - IEnumerable? deselectedItems = null; - - foreach (var changes in _rootNode.EndOperation()) - { - if (changes.HasChanges) - { - selectedIndices = Concat(selectedIndices, changes.SelectedIndices); - deselectedIndices = Concat(deselectedIndices, changes.DeselectedIndices); - selectedItems = Concat(selectedItems, changes.SelectedItems); - deselectedItems = Concat(deselectedItems, changes.DeselectedItems); - } - } - - if (selectedIndices != null || deselectedIndices != null || - selectedItems != null || deselectedItems != null) - { - e = new SelectionModelSelectionChangedEventArgs( - deselectedIndices ?? Enumerable.Empty(), - selectedIndices ?? Enumerable.Empty(), - deselectedItems ?? Enumerable.Empty(), - selectedItems ?? Enumerable.Empty()); - } + var changeSet = new SelectionModelChangeSet(changes); + e = changeSet.CreateEventArgs(); } OnSelectionChanged(e); diff --git a/src/Avalonia.Controls/SelectionModelChangeSet.cs b/src/Avalonia.Controls/SelectionModelChangeSet.cs index 989136ac8d..b195117ae6 100644 --- a/src/Avalonia.Controls/SelectionModelChangeSet.cs +++ b/src/Avalonia.Controls/SelectionModelChangeSet.cs @@ -1,144 +1,80 @@ using System; using System.Collections.Generic; -using System.Linq; - -#nullable enable namespace Avalonia.Controls { internal class SelectionModelChangeSet { - private SelectionNode _owner; - private List? _selected; - private List? _deselected; - - public SelectionModelChangeSet(SelectionNode owner) => _owner = owner; - - public bool IsTracking { get; private set; } - public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0; - public IEnumerable SelectedIndices => EnumerateIndices(_selected); - public IEnumerable DeselectedIndices => EnumerateIndices(_deselected); - public IEnumerable SelectedItems => EnumerateItems(_selected); - public IEnumerable DeselectedItems => EnumerateItems(_deselected); + private List _changes; - public void BeginOperation() + public SelectionModelChangeSet(List changes) { - if (IsTracking) - { - throw new AvaloniaInternalException("SelectionModel change operation already in progress."); - } - - IsTracking = true; - _selected?.Clear(); - _deselected?.Clear(); + _changes = changes; } - public void EndOperation() => IsTracking = false; - - public void Selected(IndexRange range) + public SelectionModelSelectionChangedEventArgs CreateEventArgs() { - if (!IsTracking) - { - return; - } - - Add(range, ref _selected, _deselected); + return new SelectionModelSelectionChangedEventArgs( + CreateIndices(x => x.DeselectedRanges), + CreateIndices(x => x.SelectedRanges), + CreateItems(x => x.DeselectedRanges), + CreateItems(x => x.SelectedRanges)); } - public void Selected(IEnumerable ranges) + private IReadOnlyList CreateIndices(Func?> selector) { - if (!IsTracking) + if (_changes == null) { - return; + return Array.Empty(); } - foreach (var range in ranges) - { - Selected(range); - } - } - - public void Deselected(IndexRange range) - { - if (!IsTracking) - { - return; - } - - Add(range, ref _deselected, _selected); - } + var result = new List(); - public void Deselected(IEnumerable ranges) - { - if (!IsTracking) + foreach (var i in _changes) { - return; - } + var ranges = selector(i); - foreach (var range in ranges) - { - Deselected(range); - } - } - - private static void Add( - IndexRange range, - ref List? add, - List? remove) - { - if (remove != null) - { - var removed = new List(); - IndexRange.Remove(remove, range, removed); - var selected = IndexRange.Subtract(range, removed); - - if (selected.Any()) + if (ranges != null) { - add ??= new List(); - - foreach (var r in selected) + foreach (var j in ranges) { - IndexRange.Add(add, r); + for (var k = j.Begin; k <= j.End; ++k) + { + result.Add(i.Path.CloneWithChildIndex(k)); + } } } } - else - { - add ??= new List(); - IndexRange.Add(add, range); - } + + return result; } - private IEnumerable EnumerateIndices(IEnumerable? ranges) + private IReadOnlyList CreateItems(Func?> selector) { - var path = _owner.IndexPath; - - if (ranges != null) + if (_changes == null) { - foreach (var range in ranges) - { - for (var i = range.Begin; i <= range.End; ++i) - { - yield return path.CloneWithChildIndex(i); - } - } + return Array.Empty(); } - } - private IEnumerable EnumerateItems(IEnumerable? ranges) - { - var items = _owner.ItemsSourceView; + var result = new List(); - if (ranges != null && items != null) + foreach (var i in _changes) { - foreach (var range in ranges) + var ranges = selector(i); + + if (ranges != null && i.Items != null) { - for (var i = range.Begin; i <= range.End; ++i) + foreach (var j in ranges) { - yield return items.GetAt(i); + for (var k = j.Begin; k <= j.End; ++k) + { + result.Add(i.Items.GetAt(k)); + } } } } + + return result; } } } diff --git a/src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs b/src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs index 4976bf1827..ae98f6a1ce 100644 --- a/src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs +++ b/src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs @@ -12,49 +12,73 @@ namespace Avalonia.Controls { public class SelectionModelSelectionChangedEventArgs : EventArgs { - private readonly IEnumerable _selectedIndicesSource; - private readonly IEnumerable _deselectedIndicesSource; - private readonly IEnumerable _selectedItemsSource; - private readonly IEnumerable _deselectedItemsSource; - private List? _selectedIndices; - private List? _deselectedIndices; - private List? _selectedItems; - private List? _deselectedItems; + private readonly IEnumerable? _deselectedIndicesSource; + private readonly IEnumerable? _selectedIndicesSource; + private readonly IEnumerable? _deselectedItemsSource; + private readonly IEnumerable? _selectedItemsSource; + private IReadOnlyList? _deselectedIndices; + private IReadOnlyList? _selectedIndices; + private IReadOnlyList? _deselectedItems; + private IReadOnlyList? _selectedItems; public SelectionModelSelectionChangedEventArgs( - IEnumerable deselectedIndices, - IEnumerable selectedIndices, - IEnumerable deselectedItems, - IEnumerable selectedItems) + IReadOnlyList? deselectedIndices, + IReadOnlyList? selectedIndices, + IReadOnlyList? deselectedItems, + IReadOnlyList? selectedItems) { - _selectedIndicesSource = selectedIndices; - _deselectedIndicesSource = deselectedIndices; - _selectedItemsSource = selectedItems; - _deselectedItemsSource = deselectedItems; + _deselectedIndices = deselectedIndices ?? Array.Empty(); + _selectedIndices = selectedIndices ?? Array.Empty(); + _deselectedItems = deselectedItems ?? Array.Empty(); + _selectedItems= selectedItems ?? Array.Empty(); } - /// - /// Gets the indices of the items that were added to the selection. - /// - public IReadOnlyList SelectedIndices => - _selectedIndices ?? (_selectedIndices = new List(_selectedIndicesSource)); + public SelectionModelSelectionChangedEventArgs( + IEnumerable? deselectedIndices, + IEnumerable? selectedIndices, + IEnumerable? deselectedItems, + IEnumerable? selectedItems) + { + static void Set(IEnumerable? source, ref IEnumerable? sourceField, ref IReadOnlyList? field) + { + if (source != null) + { + sourceField = source; + } + else + { + field = Array.Empty(); + } + } + + Set(deselectedIndices, ref _deselectedIndicesSource, ref _deselectedIndices); + Set(selectedIndices, ref _selectedIndicesSource, ref _selectedIndices); + Set(deselectedItems, ref _deselectedItemsSource, ref _deselectedItems); + Set(selectedItems, ref _selectedItemsSource, ref _selectedItems); + } /// /// Gets the indices of the items that were removed from the selection. /// - public IReadOnlyList DeselectedIndices => - _deselectedIndices ?? (_deselectedIndices = new List(_deselectedIndicesSource)); + public IReadOnlyList DeselectedIndices + => _deselectedIndices ??= new List(_deselectedIndicesSource); /// - /// Gets the items that were added to the selection. + /// Gets the indices of the items that were added to the selection. /// - public IReadOnlyList SelectedItems => - _selectedItems ?? (_selectedItems = new List(_selectedItemsSource)); + public IReadOnlyList SelectedIndices + => _selectedIndices ??= new List(_selectedIndicesSource); /// /// Gets the items that were removed from the selection. /// - public IReadOnlyList DeselectedItems => - _deselectedItems ?? (_deselectedItems = new List(_deselectedItemsSource)); + public IReadOnlyList DeselectedItems + => _deselectedItems ??= new List(_deselectedItemsSource); + + /// + /// Gets the items that were added to the selection. + /// + public IReadOnlyList SelectedItems + => _selectedItems ??= new List(_selectedItemsSource); } } diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index d462a51228..85db801100 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls private readonly SelectionNode? _parent; private readonly List _selected = new List(); private readonly List _selectedIndicesCached = new List(); - private SelectionModelChangeSet? _changes; + private SelectionNodeOperation? _operation; private object? _source; private bool _selectedIndicesCacheIsValid; @@ -137,7 +137,7 @@ namespace Avalonia.Controls child = new SelectionNode(_manager, parent: this); child.Source = resolvedChild; - if (_changes?.IsTracking == true) + if (_operation != null) { child.BeginOperation(); } @@ -296,33 +296,45 @@ namespace Avalonia.Controls public void BeginOperation() { - _changes ??= new SelectionModelChangeSet(this); - _changes.BeginOperation(); + if (_operation != null) + { + throw new AvaloniaInternalException("Selection operation already in progress."); + } + + _operation = new SelectionNodeOperation(this); for (var i = 0; i < _childrenNodes.Count; ++i) { - _childrenNodes[i]?.BeginOperation(); + var child = _childrenNodes[i]; + + if (child != null && child != _manager.SharedLeafNode) + { + child.BeginOperation(); + } } } - public IEnumerable EndOperation() + public void EndOperation(List changes) { - if (_changes != null) + if (_operation == null) { - _changes.EndOperation(); - yield return _changes; + throw new AvaloniaInternalException("No selection operation in progress."); + } - for (var i = 0; i < _childrenNodes.Count; ++i) - { - var child = _childrenNodes[i]; + if (_operation.HasChanges) + { + changes.Add(_operation); + } - if (child != null) - { - foreach (var changes in child.EndOperation()) - { - yield return changes; - } - } + _operation = null; + + for (var i = 0; i < _childrenNodes.Count; ++i) + { + var child = _childrenNodes[i]; + + if (child != null && child != _manager.SharedLeafNode) + { + child.EndOperation(changes); } } } @@ -400,7 +412,7 @@ namespace Avalonia.Controls if (selected.Count > 0) { - _changes?.Selected(selected); + _operation?.Selected(selected); if (raiseOnSelectionChanged) { @@ -417,7 +429,7 @@ namespace Avalonia.Controls if (removed.Count > 0) { - _changes?.Deselected(removed); + _operation?.Deselected(removed); if (raiseOnSelectionChanged) { @@ -431,7 +443,7 @@ namespace Avalonia.Controls // Deselect all items if (_selected.Count > 0) { - _changes?.Deselected(_selected); + _operation?.Deselected(_selected); _selected.Clear(); OnSelectionChanged(); } @@ -480,7 +492,7 @@ namespace Avalonia.Controls private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args) { bool selectionInvalidated = false; - IList? removed = null; + List? removed = null; switch (args.Action) { @@ -594,7 +606,7 @@ namespace Avalonia.Controls return selectionInvalidated; } - private (bool, IList) OnItemsRemoved(int index, IList items) + private (bool, List) OnItemsRemoved(int index, IList items) { var selectionInvalidated = false; var removed = new List(); diff --git a/src/Avalonia.Controls/SelectionNodeOperation.cs b/src/Avalonia.Controls/SelectionNodeOperation.cs new file mode 100644 index 0000000000..04b8554f7c --- /dev/null +++ b/src/Avalonia.Controls/SelectionNodeOperation.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +#nullable enable + +namespace Avalonia.Controls +{ + internal class SelectionNodeOperation + { + private readonly SelectionNode _owner; + private List? _selected; + private List? _deselected; + + public SelectionNodeOperation(SelectionNode owner) + { + _owner = owner; + } + + public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0; + public List? SelectedRanges => _selected; + public List? DeselectedRanges => _deselected; + public IndexPath Path => _owner.IndexPath; + public ItemsSourceView? Items => _owner.ItemsSourceView; + + public void Selected(IndexRange range) + { + Add(range, ref _selected, _deselected); + } + + public void Selected(IEnumerable ranges) + { + foreach (var range in ranges) + { + Selected(range); + } + } + + public void Deselected(IndexRange range) + { + Add(range, ref _deselected, _selected); + } + + public void Deselected(IEnumerable ranges) + { + foreach (var range in ranges) + { + Deselected(range); + } + } + + private static void Add( + IndexRange range, + ref List? add, + List? remove) + { + if (remove != null) + { + var removed = new List(); + IndexRange.Remove(remove, range, removed); + var selected = IndexRange.Subtract(range, removed); + + if (selected.Any()) + { + add ??= new List(); + + foreach (var r in selected) + { + IndexRange.Add(add, r); + } + } + } + else + { + add ??= new List(); + IndexRange.Add(add, range); + } + } + } +}