9 changed files with 3278 additions and 0 deletions
@ -0,0 +1,98 @@ |
|||
// 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; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public sealed class IndexPath : IComparable<IndexPath> |
|||
{ |
|||
private readonly List<int> _path = new List<int>(); |
|||
|
|||
internal IndexPath(int index) |
|||
{ |
|||
_path.Add(index); |
|||
} |
|||
|
|||
internal IndexPath(int groupIndex, int itemIndex) |
|||
{ |
|||
_path.Add(groupIndex); |
|||
_path.Add(itemIndex); |
|||
} |
|||
|
|||
internal IndexPath(IEnumerable<int> indices) |
|||
{ |
|||
if (indices != null) |
|||
{ |
|||
_path.AddRange(indices); |
|||
} |
|||
} |
|||
|
|||
public int GetSize() => _path.Count; |
|||
public int GetAt(int index) => _path[index]; |
|||
|
|||
public int CompareTo(IndexPath other) |
|||
{ |
|||
var rhsPath = other; |
|||
int compareResult = 0; |
|||
int lhsCount = _path.Count; |
|||
int rhsCount = rhsPath._path.Count; |
|||
|
|||
if (lhsCount == 0 || rhsCount == 0) |
|||
{ |
|||
// one of the paths are empty, compare based on size
|
|||
compareResult = (lhsCount - rhsCount); |
|||
} |
|||
else |
|||
{ |
|||
// both paths are non-empty, but can be of different size
|
|||
for (int i = 0; i < Math.Min(lhsCount, rhsCount); i++) |
|||
{ |
|||
if (_path[i] < rhsPath._path[i]) |
|||
{ |
|||
compareResult = -1; |
|||
break; |
|||
} |
|||
else if (_path[i] > rhsPath._path[i]) |
|||
{ |
|||
compareResult = 1; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
// if both match upto min(lhsCount, rhsCount), compare based on size
|
|||
compareResult = compareResult == 0 ? (lhsCount - rhsCount) : compareResult; |
|||
} |
|||
|
|||
if (compareResult != 0) |
|||
{ |
|||
compareResult = compareResult > 0 ? 1 : -1; |
|||
} |
|||
|
|||
return compareResult; |
|||
} |
|||
|
|||
public IndexPath CloneWithChildIndex(int childIndex) |
|||
{ |
|||
var newPath = new List<int>(_path); |
|||
newPath.Add(childIndex); |
|||
return new IndexPath(newPath); |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return "R." + string.Join(".", _path); |
|||
} |
|||
|
|||
public static IndexPath CreateFrom(int index) => new IndexPath(index); |
|||
|
|||
public static IndexPath CreateFrom(int groupIndex, int itemIndex) => new IndexPath(groupIndex, itemIndex); |
|||
|
|||
public static IndexPath CreateFromIndices(IList<int> indices) => new IndexPath(indices); |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
// 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; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal readonly struct IndexRange |
|||
{ |
|||
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 bool Contains(int index) |
|||
{ |
|||
return 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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
// 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; |
|||
using System.Collections.Generic; |
|||
using SelectedItemInfo = Avalonia.Controls.SelectionModel.SelectedItemInfo; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
internal class SelectedItems<T> : IReadOnlyList<T> |
|||
{ |
|||
private readonly List<SelectedItemInfo> _infos; |
|||
private readonly Func<List<SelectedItemInfo>, int, T> _getAtImpl; |
|||
private int _totalCount; |
|||
|
|||
public SelectedItems( |
|||
List<SelectedItemInfo> infos, |
|||
Func<List<SelectedItemInfo>, int, T> getAtImpl) |
|||
{ |
|||
_infos = infos; |
|||
_getAtImpl = getAtImpl; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var node = info.Node; |
|||
|
|||
if (node != null) |
|||
{ |
|||
_totalCount += node.SelectedCount; |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException("Selection changed after the SelectedIndices/Items property was read."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public T this[int index] => _getAtImpl(_infos, index); |
|||
|
|||
public int Count => _totalCount; |
|||
|
|||
public IEnumerator<T> GetEnumerator() |
|||
{ |
|||
for (var i = 0; i < _totalCount; ++i) |
|||
{ |
|||
yield return this[i]; |
|||
} |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,710 @@ |
|||
// 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; |
|||
using System.ComponentModel; |
|||
using Avalonia.Controls.Utils; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class SelectionModel : INotifyPropertyChanged, IDisposable |
|||
{ |
|||
private SelectionNode _rootNode; |
|||
private bool _singleSelect; |
|||
private IReadOnlyList<IndexPath> _selectedIndicesCached; |
|||
private IReadOnlyList<object> _selectedItemsCached; |
|||
private SelectionModelChildrenRequestedEventArgs _childrenRequestedEventArgs; |
|||
private SelectionModelSelectionChangedEventArgs _selectionChangedEventArgs; |
|||
|
|||
public event EventHandler<SelectionModelChildrenRequestedEventArgs> ChildrenRequested; |
|||
public event PropertyChangedEventHandler PropertyChanged; |
|||
public event EventHandler<SelectionModelSelectionChangedEventArgs> SelectionChanged; |
|||
|
|||
public SelectionModel() |
|||
{ |
|||
_rootNode = new SelectionNode(this, null); |
|||
SharedLeafNode = new SelectionNode(this, null); |
|||
} |
|||
|
|||
public object Source |
|||
{ |
|||
get => _rootNode.Source; |
|||
set |
|||
{ |
|||
ClearSelection(resetAnchor: true, raiseSelectionChanged: false); |
|||
_rootNode.Source = value; |
|||
OnSelectionChanged(); |
|||
RaisePropertyChanged("Source"); |
|||
} |
|||
} |
|||
|
|||
public bool SingleSelect |
|||
{ |
|||
get => _singleSelect; |
|||
set |
|||
{ |
|||
if (_singleSelect != value) |
|||
{ |
|||
_singleSelect = value; |
|||
var selectedIndices = SelectedIndices; |
|||
|
|||
if (value && selectedIndices != null && selectedIndices.Count > 0) |
|||
{ |
|||
// We want to be single select, so make sure there is only
|
|||
// one selected item.
|
|||
var firstSelectionIndexPath = selectedIndices[0]; |
|||
ClearSelection(resetAnchor: true, raiseSelectionChanged: false); |
|||
SelectWithPathImpl(firstSelectionIndexPath, select: true, raiseSelectionChanged: false); |
|||
// Setting SelectedIndex will raise SelectionChanged event.
|
|||
SelectedIndex = firstSelectionIndexPath; |
|||
} |
|||
|
|||
RaisePropertyChanged("SingleSelect"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
public IndexPath AnchorIndex |
|||
{ |
|||
get |
|||
{ |
|||
IndexPath anchor = null; |
|||
|
|||
if (_rootNode.AnchorIndex >= 0) |
|||
{ |
|||
var path = new List<int>(); |
|||
var current = _rootNode; |
|||
|
|||
while (current?.AnchorIndex >= 0) |
|||
{ |
|||
path.Add(current.AnchorIndex); |
|||
current = current.GetAt(current.AnchorIndex, false); |
|||
} |
|||
|
|||
anchor = new IndexPath(path); |
|||
} |
|||
|
|||
return anchor; |
|||
} |
|||
set |
|||
{ |
|||
if (value != null) |
|||
{ |
|||
SelectionTreeHelper.TraverseIndexPath( |
|||
_rootNode, |
|||
value, |
|||
realizeChildren: true, |
|||
(currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth)); |
|||
} |
|||
else |
|||
{ |
|||
_rootNode.AnchorIndex = -1; |
|||
} |
|||
|
|||
RaisePropertyChanged("AnchorIndex"); |
|||
} |
|||
} |
|||
|
|||
public IndexPath SelectedIndex |
|||
{ |
|||
get |
|||
{ |
|||
IndexPath selectedIndex = null; |
|||
var selectedIndices = SelectedIndices; |
|||
|
|||
if (selectedIndices?.Count > 0) |
|||
{ |
|||
selectedIndex = selectedIndices[0]; |
|||
} |
|||
|
|||
return selectedIndex; |
|||
} |
|||
set |
|||
{ |
|||
var isSelected = IsSelectedAt(value); |
|||
|
|||
if (!isSelected.HasValue || !isSelected.Value) |
|||
{ |
|||
ClearSelection(resetAnchor: true, raiseSelectionChanged: false); |
|||
SelectWithPathImpl(value, select: true, raiseSelectionChanged: false); |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public object SelectedItem |
|||
{ |
|||
get |
|||
{ |
|||
object item = null; |
|||
var selectedItems = SelectedItems; |
|||
|
|||
if (selectedItems?.Count > 0) |
|||
{ |
|||
item = selectedItems[0]; |
|||
} |
|||
|
|||
return item; |
|||
} |
|||
} |
|||
|
|||
public IReadOnlyList<object> SelectedItems |
|||
{ |
|||
get |
|||
{ |
|||
if (_selectedItemsCached == null) |
|||
{ |
|||
var selectedInfos = new List<SelectedItemInfo>(); |
|||
|
|||
if (_rootNode.Source != null) |
|||
{ |
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
realizeChildren: false, |
|||
currentInfo => |
|||
{ |
|||
if (currentInfo.Node.SelectedCount > 0) |
|||
{ |
|||
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path)); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// Instead of creating a dumb vector that takes up the space for all the selected items,
|
|||
// we create a custom VectorView implimentation that calls back using a delegate to find
|
|||
// the selected item at a particular index. This avoid having to create the storage and copying
|
|||
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
|
|||
// easier to consume flat vector view of objects.
|
|||
var selectedItems = new SelectedItems<object> ( |
|||
selectedInfos, |
|||
(infos, index) => |
|||
{ |
|||
var currentIndex = 0; |
|||
object item = null; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var node = info.Node; |
|||
|
|||
if (node != null) |
|||
{ |
|||
var currentCount = node.SelectedCount; |
|||
|
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
var targetIndex = node.SelectedIndices[index - currentIndex]; |
|||
item = node.ItemsSourceView.GetAt(targetIndex); |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException( |
|||
"Selection has changed since SelectedItems property was read."); |
|||
} |
|||
} |
|||
|
|||
return item; |
|||
}); |
|||
|
|||
_selectedItemsCached = selectedItems; |
|||
} |
|||
|
|||
return _selectedItemsCached; |
|||
} |
|||
} |
|||
|
|||
public IReadOnlyList<IndexPath> SelectedIndices |
|||
{ |
|||
get |
|||
{ |
|||
if (_selectedIndicesCached == null) |
|||
{ |
|||
var selectedInfos = new List<SelectedItemInfo>(); |
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
false, |
|||
currentInfo => |
|||
{ |
|||
if (currentInfo.Node.SelectedCount > 0) |
|||
{ |
|||
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path)); |
|||
} |
|||
}); |
|||
|
|||
// Instead of creating a dumb vector that takes up the space for all the selected indices,
|
|||
// we create a custom VectorView implimentation that calls back using a delegate to find
|
|||
// the IndexPath at a particular index. This avoid having to create the storage and copying
|
|||
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
|
|||
// easier to consume flat vector view of IndexPaths.
|
|||
var indices = new SelectedItems<IndexPath>( |
|||
selectedInfos, |
|||
(infos, index) => // callback for GetAt(index)
|
|||
{ |
|||
var currentIndex = 0; |
|||
IndexPath path = null; |
|||
|
|||
foreach (var info in infos) |
|||
{ |
|||
var node = info.Node; |
|||
|
|||
if (node != null) |
|||
{ |
|||
var currentCount = node.SelectedCount; |
|||
if (index >= currentIndex && index < currentIndex + currentCount) |
|||
{ |
|||
int targetIndex = node.SelectedIndices[index - currentIndex]; |
|||
path = info.Path.CloneWithChildIndex(targetIndex); |
|||
break; |
|||
} |
|||
|
|||
currentIndex += currentCount; |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException( |
|||
"Selection has changed since SelectedIndices property was read."); |
|||
} |
|||
} |
|||
|
|||
return path; |
|||
}); |
|||
|
|||
_selectedIndicesCached = indices; |
|||
} |
|||
|
|||
return _selectedIndicesCached; |
|||
} |
|||
} |
|||
|
|||
internal SelectionNode SharedLeafNode { get; private set; } |
|||
|
|||
public void Dispose() |
|||
{ |
|||
ClearSelection(resetAnchor: false, raiseSelectionChanged: false); |
|||
_rootNode?.Dispose(); |
|||
_rootNode = null; |
|||
SharedLeafNode = null; |
|||
_selectedIndicesCached = null; |
|||
_selectedItemsCached = null; |
|||
} |
|||
|
|||
public void SetAnchorIndex(int index) => AnchorIndex = new IndexPath(index); |
|||
|
|||
public void SetAnchorIndex(int groupIndex, int index) => AnchorIndex = new IndexPath(groupIndex, index); |
|||
|
|||
public void Select(int index) => SelectImpl(index, select: true); |
|||
|
|||
public void Select(int groupIndex, int itemIndex) => SelectWithGroupImpl(groupIndex, itemIndex, select: true); |
|||
|
|||
public void SelectAt(IndexPath index) => SelectWithPathImpl(index, select: true, raiseSelectionChanged: true); |
|||
|
|||
public void Deselect(int index) => SelectImpl(index, select: false); |
|||
|
|||
public void Deselect(int groupIndex, int itemIndex) => SelectWithGroupImpl(groupIndex, itemIndex, select: false); |
|||
|
|||
public void DeselectAt(IndexPath index) => SelectWithPathImpl(index, select: false, raiseSelectionChanged: true); |
|||
|
|||
public bool? IsSelected(int index) |
|||
{ |
|||
if (index < 0) |
|||
{ |
|||
throw new ArgumentException("Index must be >= 0", nameof(index)); |
|||
} |
|||
|
|||
var isSelected = _rootNode.IsSelectedWithPartial(index); |
|||
return isSelected; |
|||
} |
|||
|
|||
public bool? IsSelected(int groupIndex, int itemIndex) |
|||
{ |
|||
if (groupIndex < 0) |
|||
{ |
|||
throw new ArgumentException("Group index must be >= 0", nameof(groupIndex)); |
|||
} |
|||
|
|||
if (itemIndex < 0) |
|||
{ |
|||
throw new ArgumentException("Item index must be >= 0", nameof(itemIndex)); |
|||
} |
|||
|
|||
var isSelected = (bool?)false; |
|||
var childNode = _rootNode.GetAt(groupIndex, realizeChild: false); |
|||
|
|||
if (childNode != null) |
|||
{ |
|||
isSelected = childNode.IsSelectedWithPartial(itemIndex); |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
public bool? IsSelectedAt(IndexPath index) |
|||
{ |
|||
var path = index; |
|||
var isRealized = true; |
|||
var node = _rootNode; |
|||
|
|||
for (int i = 0; i < path.GetSize() - 1; i++) |
|||
{ |
|||
var childIndex = path.GetAt(i); |
|||
node = node.GetAt(childIndex, realizeChild: false); |
|||
|
|||
if (node == null) |
|||
{ |
|||
isRealized = false; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
var isSelected = (bool?)false; |
|||
|
|||
if (isRealized) |
|||
{ |
|||
var size = path.GetSize(); |
|||
if (size == 0) |
|||
{ |
|||
isSelected = SelectionNode.ConvertToNullableBool(node.EvaluateIsSelectedBasedOnChildrenNodes()); |
|||
} |
|||
else |
|||
{ |
|||
isSelected = node.IsSelectedWithPartial(path.GetAt(size - 1)); |
|||
} |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
public void SelectRangeFromAnchor(int index) |
|||
{ |
|||
SelectRangeFromAnchorImpl(index, select: true); |
|||
} |
|||
|
|||
public void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex) |
|||
{ |
|||
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, select: true); |
|||
} |
|||
|
|||
public void SelectRangeFromAnchorTo(IndexPath index) |
|||
{ |
|||
SelectRangeImpl(AnchorIndex, index, select: true); |
|||
} |
|||
|
|||
public void DeselectRangeFromAnchor(int index) |
|||
{ |
|||
SelectRangeFromAnchorImpl(index, select: false); |
|||
} |
|||
|
|||
public void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex) |
|||
{ |
|||
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, false /* select */); |
|||
} |
|||
|
|||
public void DeselectRangeFromAnchorTo(IndexPath index) |
|||
{ |
|||
SelectRangeImpl(AnchorIndex, index, select: false); |
|||
} |
|||
|
|||
public void SelectRange(IndexPath start, IndexPath end) |
|||
{ |
|||
SelectRangeImpl(start, end, select: true); |
|||
} |
|||
|
|||
public void DeselectRange(IndexPath start, IndexPath end) |
|||
{ |
|||
SelectRangeImpl(start, end, select: false); |
|||
} |
|||
|
|||
public void SelectAll() |
|||
{ |
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
realizeChildren: true, |
|||
info => |
|||
{ |
|||
if (info.Node.DataCount > 0) |
|||
{ |
|||
info.Node.SelectAll(); |
|||
} |
|||
}); |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
public void ClearSelection() |
|||
{ |
|||
ClearSelection(resetAnchor: true, raiseSelectionChanged: true); |
|||
} |
|||
|
|||
protected void OnPropertyChanged(string propertyName) |
|||
{ |
|||
RaisePropertyChanged(propertyName); |
|||
} |
|||
|
|||
private void RaisePropertyChanged(string propertyName) |
|||
{ |
|||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
|||
} |
|||
|
|||
public void OnSelectionInvalidatedDueToCollectionChange() |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
internal object ResolvePath(object data, SelectionNode sourceNode) |
|||
{ |
|||
object resolved = null; |
|||
|
|||
// Raise ChildrenRequested event if there is a handler
|
|||
if (ChildrenRequested != null) |
|||
{ |
|||
if (_childrenRequestedEventArgs == null) |
|||
{ |
|||
_childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, sourceNode); |
|||
} |
|||
else |
|||
{ |
|||
_childrenRequestedEventArgs.Initialize(data, sourceNode); |
|||
} |
|||
|
|||
ChildrenRequested(this, _childrenRequestedEventArgs); |
|||
resolved = _childrenRequestedEventArgs.Children; |
|||
|
|||
// Clear out the values in the args so that it cannot be used after the event handler call.
|
|||
_childrenRequestedEventArgs.Initialize(null, null); |
|||
} |
|||
else |
|||
{ |
|||
// No handlers for ChildrenRequested event. If data is of type ItemsSourceView
|
|||
// or a type that can be used to create a ItemsSourceView, then we can auto-resolve
|
|||
// that as the child. If not, then we consider the value as a leaf. This is to
|
|||
// avoid having to provide the event handler for the most common scenarios. If the
|
|||
// app dev does not want this default behavior, they can provide the handler to
|
|||
// override.
|
|||
if (data is IEnumerable<object>) |
|||
{ |
|||
resolved = data; |
|||
} |
|||
} |
|||
|
|||
return resolved; |
|||
} |
|||
|
|||
private void ClearSelection(bool resetAnchor, bool raiseSelectionChanged) |
|||
{ |
|||
SelectionTreeHelper.Traverse( |
|||
_rootNode, |
|||
realizeChildren: false, |
|||
info => info.Node.Clear()); |
|||
|
|||
if (resetAnchor) |
|||
{ |
|||
AnchorIndex = null; |
|||
} |
|||
|
|||
if (raiseSelectionChanged) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
|
|||
private void OnSelectionChanged() |
|||
{ |
|||
_selectedIndicesCached = null; |
|||
_selectedItemsCached = null; |
|||
|
|||
// Raise SelectionChanged event
|
|||
if (SelectionChanged != null) |
|||
{ |
|||
if (_selectionChangedEventArgs == null) |
|||
{ |
|||
_selectionChangedEventArgs = new SelectionModelSelectionChangedEventArgs(); |
|||
} |
|||
|
|||
SelectionChanged(this, _selectionChangedEventArgs); |
|||
} |
|||
|
|||
RaisePropertyChanged(nameof(SelectedIndex)); |
|||
RaisePropertyChanged(nameof(SelectedIndices)); |
|||
|
|||
if (_rootNode.Source != null) |
|||
{ |
|||
RaisePropertyChanged(nameof(SelectedItem)); |
|||
RaisePropertyChanged(nameof(SelectedItems)); |
|||
} |
|||
} |
|||
|
|||
private void SelectImpl(int index, bool select) |
|||
{ |
|||
if (_singleSelect) |
|||
{ |
|||
ClearSelection(resetAnchor: true, raiseSelectionChanged: false); |
|||
} |
|||
|
|||
var selected = _rootNode.Select(index, select); |
|||
|
|||
if (selected) |
|||
{ |
|||
AnchorIndex = new IndexPath(index); |
|||
} |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select) |
|||
{ |
|||
if (_singleSelect) |
|||
{ |
|||
ClearSelection(resetAnchor: true, raiseSelectionChanged: false); |
|||
} |
|||
|
|||
var childNode = _rootNode.GetAt(groupIndex, realizeChild: true); |
|||
var selected = childNode.Select(itemIndex, select); |
|||
|
|||
if (selected) |
|||
{ |
|||
AnchorIndex = new IndexPath(groupIndex, itemIndex); |
|||
} |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
private void SelectWithPathImpl(IndexPath index, bool select, bool raiseSelectionChanged) |
|||
{ |
|||
bool selected = false; |
|||
|
|||
if (_singleSelect) |
|||
{ |
|||
ClearSelection(resetAnchor: true, raiseSelectionChanged: false); |
|||
} |
|||
|
|||
SelectionTreeHelper.TraverseIndexPath( |
|||
_rootNode, |
|||
index, |
|||
true, |
|||
(currentNode, path, depth, childIndex) => |
|||
{ |
|||
if (depth == path.GetSize() - 1) |
|||
{ |
|||
selected = currentNode.Select(childIndex, select); |
|||
} |
|||
} |
|||
); |
|||
|
|||
if (selected) |
|||
{ |
|||
AnchorIndex = index; |
|||
} |
|||
|
|||
if (raiseSelectionChanged) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
|
|||
private void SelectRangeFromAnchorImpl(int index, bool select) |
|||
{ |
|||
int anchorIndex = 0; |
|||
var anchor = AnchorIndex; |
|||
|
|||
if (anchor != null) |
|||
{ |
|||
anchorIndex = anchor.GetAt(0); |
|||
} |
|||
|
|||
bool selected = _rootNode.SelectRange(new IndexRange(anchorIndex, index), select); |
|||
|
|||
if (selected) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
|
|||
private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select) |
|||
{ |
|||
var startGroupIndex = 0; |
|||
var startItemIndex = 0; |
|||
var anchorIndex = AnchorIndex; |
|||
|
|||
if (anchorIndex != null) |
|||
{ |
|||
startGroupIndex = anchorIndex.GetAt(0); |
|||
startItemIndex = anchorIndex.GetAt(1); |
|||
} |
|||
|
|||
// Make sure start > end
|
|||
if (startGroupIndex > endGroupIndex || |
|||
(startGroupIndex == endGroupIndex && startItemIndex > endItemIndex)) |
|||
{ |
|||
int temp = startGroupIndex; |
|||
startGroupIndex = endGroupIndex; |
|||
endGroupIndex = temp; |
|||
temp = startItemIndex; |
|||
startItemIndex = endItemIndex; |
|||
endItemIndex = temp; |
|||
} |
|||
|
|||
var selected = false; |
|||
for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++) |
|||
{ |
|||
var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true); |
|||
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0; |
|||
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1; |
|||
selected |= groupNode.SelectRange(new IndexRange(startIndex, endIndex), select); |
|||
} |
|||
|
|||
if (selected) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
|
|||
private void SelectRangeImpl(IndexPath start, IndexPath end, bool select) |
|||
{ |
|||
var winrtStart = start; |
|||
var winrtEnd = end; |
|||
|
|||
// Make sure start <= end
|
|||
if (winrtEnd.CompareTo(winrtStart) == -1) |
|||
{ |
|||
var temp = winrtStart; |
|||
winrtStart = winrtEnd; |
|||
winrtEnd = temp; |
|||
} |
|||
|
|||
// Note: Since we do not know the depth of the tree, we have to walk to each leaf
|
|||
SelectionTreeHelper.TraverseRangeRealizeChildren( |
|||
_rootNode, |
|||
winrtStart, |
|||
winrtEnd, |
|||
info => |
|||
{ |
|||
if (info.Node.DataCount == 0) |
|||
{ |
|||
// Select only leaf nodes
|
|||
info.ParentNode.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); |
|||
} |
|||
}); |
|||
|
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
internal class SelectedItemInfo |
|||
{ |
|||
public SelectedItemInfo(SelectionNode node, IndexPath path) |
|||
{ |
|||
Node = node; |
|||
Path = path; |
|||
} |
|||
|
|||
public SelectionNode Node { get; } |
|||
public IndexPath Path { get; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// 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; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class SelectionModelChildrenRequestedEventArgs : EventArgs |
|||
{ |
|||
private SelectionNode _sourceNode; |
|||
|
|||
internal SelectionModelChildrenRequestedEventArgs(object source, SelectionNode sourceNode) |
|||
{ |
|||
Initialize(source, sourceNode); |
|||
} |
|||
|
|||
public object Children { get; set; } |
|||
public object Source { get; private set; } |
|||
public IndexPath SourceIndex => _sourceNode.IndexPath; |
|||
|
|||
internal void Initialize(object source, SelectionNode sourceNode) |
|||
{ |
|||
Source = source; |
|||
_sourceNode = sourceNode; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
// 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; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class SelectionModelSelectionChangedEventArgs : EventArgs |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,811 @@ |
|||
// 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; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Tracks nested selection.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// SelectionNode is the internal tree data structure that we keep track of for selection in
|
|||
/// a nested scenario. This would map to one ItemsSourceView/Collection. This node reacts to
|
|||
/// collection changes and keeps the selected indices up to date. This can either be a leaf
|
|||
/// node or a non leaf node.
|
|||
/// </remarks>
|
|||
internal class SelectionNode : IDisposable |
|||
{ |
|||
private readonly SelectionModel _manager; |
|||
private readonly List<SelectionNode> _childrenNodes = new List<SelectionNode>(); |
|||
private readonly SelectionNode _parent; |
|||
private readonly List<IndexRange> _selected = new List<IndexRange>(); |
|||
private object _source; |
|||
private ItemsSourceView _dataSource; |
|||
private int _selectedCount; |
|||
private List<int> _selectedIndicesCached = new List<int>(); |
|||
private bool _selectedIndicesCacheIsValid; |
|||
private int _realizedChildrenNodeCount; |
|||
|
|||
public SelectionNode(SelectionModel manager, SelectionNode parent) |
|||
{ |
|||
_manager = manager; |
|||
_parent = parent; |
|||
} |
|||
|
|||
public int AnchorIndex { get; set; } = -1; |
|||
|
|||
public object Source |
|||
{ |
|||
get => _source; |
|||
set |
|||
{ |
|||
if (_source != value) |
|||
{ |
|||
ClearSelection(); |
|||
UnhookCollectionChangedHandler(); |
|||
|
|||
_source = value; |
|||
|
|||
// Setup ItemsSourceView
|
|||
var newDataSource = value as ItemsSourceView; |
|||
|
|||
if (value != null && newDataSource == null) |
|||
{ |
|||
newDataSource = new ItemsSourceView((IEnumerable)value); |
|||
} |
|||
|
|||
_dataSource = newDataSource; |
|||
|
|||
HookupCollectionChangedHandler(); |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public ItemsSourceView ItemsSourceView => _dataSource; |
|||
public int DataCount => _dataSource?.Count ?? 0; |
|||
public int ChildrenNodeCount => _childrenNodes.Count; |
|||
public int RealizedChildrenNodeCount => _realizedChildrenNodeCount; |
|||
|
|||
public IndexPath IndexPath |
|||
{ |
|||
get |
|||
{ |
|||
var path = new List<int>(); ; |
|||
var parent = _parent; |
|||
var child = this; |
|||
|
|||
while (parent != null) |
|||
{ |
|||
var childNodes = parent._childrenNodes; |
|||
var index = childNodes.IndexOf(child); |
|||
|
|||
// We are walking up to the parent, so the path will be backwards
|
|||
path.Insert(0, index); |
|||
child = parent; |
|||
parent = parent._parent; |
|||
} |
|||
|
|||
return new IndexPath(path); |
|||
} |
|||
} |
|||
|
|||
// For a genuine tree view, we dont know which node is leaf until we
|
|||
// actually walk to it, so currently the tree builds up to the leaf. I don't
|
|||
// create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid
|
|||
// an explosion of node objects. However, I'm still creating the m_childrenNodes
|
|||
// collection unfortunately.
|
|||
public SelectionNode GetAt(int index, bool realizeChild) |
|||
{ |
|||
SelectionNode child = null; |
|||
|
|||
if (realizeChild) |
|||
{ |
|||
if (_childrenNodes.Count == 0) |
|||
{ |
|||
if (_dataSource != null) |
|||
{ |
|||
for (int i = 0; i < _dataSource.Count; i++) |
|||
{ |
|||
_childrenNodes.Add(null); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (_childrenNodes[index] == null) |
|||
{ |
|||
var childData = _dataSource.GetAt(index); |
|||
|
|||
if (childData != null) |
|||
{ |
|||
var resolvedChild = _manager.ResolvePath(childData, this); |
|||
|
|||
if (resolvedChild != null) |
|||
{ |
|||
child = new SelectionNode(_manager, parent: this); |
|||
child.Source = resolvedChild; |
|||
} |
|||
else |
|||
{ |
|||
child = _manager.SharedLeafNode; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
child = _manager.SharedLeafNode; |
|||
} |
|||
|
|||
_childrenNodes[index] = child; |
|||
_realizedChildrenNodeCount++; |
|||
} |
|||
else |
|||
{ |
|||
child = _childrenNodes[index]; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (_childrenNodes.Count > 0) |
|||
{ |
|||
child = _childrenNodes[index]; |
|||
} |
|||
} |
|||
|
|||
return child; |
|||
} |
|||
|
|||
public int SelectedCount => _selectedCount; |
|||
|
|||
public bool IsSelected(int index) |
|||
{ |
|||
var isSelected = false; |
|||
|
|||
foreach (var range in _selected) |
|||
{ |
|||
if (range.Contains(index)) |
|||
{ |
|||
isSelected = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
// True -> Selected
|
|||
// False -> Not Selected
|
|||
// Null -> Some descendents are selected and some are not
|
|||
public bool? IsSelectedWithPartial() |
|||
{ |
|||
var isSelected = (bool?)false; |
|||
|
|||
if (_parent != null) |
|||
{ |
|||
var parentsChildren = _parent._childrenNodes; |
|||
|
|||
var myIndexInParent = parentsChildren.IndexOf(this); |
|||
|
|||
if (myIndexInParent != -1) |
|||
{ |
|||
isSelected = _parent.IsSelectedWithPartial(myIndexInParent); |
|||
} |
|||
} |
|||
|
|||
return isSelected; |
|||
} |
|||
|
|||
// True -> Selected
|
|||
// False -> Not Selected
|
|||
// Null -> Some descendents are selected and some are not
|
|||
public bool? IsSelectedWithPartial(int index) |
|||
{ |
|||
var selectionState = SelectionState.NotSelected; |
|||
|
|||
if (_childrenNodes.Count == 0 || // no nodes realized
|
|||
_childrenNodes.Count <= index || // target node is not realized
|
|||
_childrenNodes[index] == null || // target node is not realized
|
|||
_childrenNodes[index] == _manager.SharedLeafNode) // target node is a leaf node.
|
|||
{ |
|||
// Ask parent if the target node is selected.
|
|||
selectionState = IsSelected(index) ? SelectionState.Selected : SelectionState.NotSelected; |
|||
} |
|||
else |
|||
{ |
|||
// targetNode is the node representing the index. This node is the parent.
|
|||
// targetNode is a non-leaf node, containing one or many children nodes. Evaluate
|
|||
// based on children of targetNode.
|
|||
var targetNode = _childrenNodes[index]; |
|||
selectionState = targetNode.EvaluateIsSelectedBasedOnChildrenNodes(); |
|||
} |
|||
|
|||
return ConvertToNullableBool(selectionState); |
|||
} |
|||
|
|||
public int SelectedIndex |
|||
{ |
|||
get => SelectedCount > 0 ? SelectedIndices[0] : -1; |
|||
set |
|||
{ |
|||
if (IsValidIndex(value) && (SelectedCount != 1 || !IsSelected(value))) |
|||
{ |
|||
ClearSelection(); |
|||
|
|||
if (value != -1) |
|||
{ |
|||
Select(value, true); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public List<int> SelectedIndices |
|||
{ |
|||
get |
|||
{ |
|||
if (!_selectedIndicesCacheIsValid) |
|||
{ |
|||
_selectedIndicesCacheIsValid = true; |
|||
|
|||
foreach (var range in _selected) |
|||
{ |
|||
for (int index = range.Begin; index <= range.End; index++) |
|||
{ |
|||
// Avoid duplicates
|
|||
if (!_selectedIndicesCached.Contains(index)) |
|||
{ |
|||
_selectedIndicesCached.Add(index); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Sort the list for easy consumption
|
|||
_selectedIndicesCached.Sort(); |
|||
} |
|||
|
|||
return _selectedIndicesCached; |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_dataSource?.Dispose(); |
|||
UnhookCollectionChangedHandler(); |
|||
} |
|||
|
|||
public bool Select(int index, bool select) |
|||
{ |
|||
return Select(index, select, raiseOnSelectionChanged: true); |
|||
} |
|||
|
|||
public bool ToggleSelect(int index) |
|||
{ |
|||
return Select(index, !IsSelected(index)); |
|||
} |
|||
|
|||
public void SelectAll() |
|||
{ |
|||
if (_dataSource != null) |
|||
{ |
|||
var size = _dataSource.Count; |
|||
|
|||
if (size > 0) |
|||
{ |
|||
SelectRange(new IndexRange(0, size - 1), select: true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Clear() => ClearSelection(); |
|||
|
|||
public bool SelectRange(IndexRange range, bool select) |
|||
{ |
|||
if (IsValidIndex(range.Begin) && IsValidIndex(range.End)) |
|||
{ |
|||
if (select) |
|||
{ |
|||
AddRange(range, raiseOnSelectionChanged: true); |
|||
} |
|||
else |
|||
{ |
|||
RemoveRange(range, raiseOnSelectionChanged: true); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private void HookupCollectionChangedHandler() |
|||
{ |
|||
if (_dataSource != null) |
|||
{ |
|||
_dataSource.CollectionChanged += OnSourceListChanged; |
|||
} |
|||
} |
|||
|
|||
private void UnhookCollectionChangedHandler() |
|||
{ |
|||
if (_dataSource != null) |
|||
{ |
|||
_dataSource.CollectionChanged -= OnSourceListChanged; |
|||
} |
|||
} |
|||
|
|||
private bool IsValidIndex(int index) |
|||
{ |
|||
return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count); |
|||
} |
|||
|
|||
private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged) |
|||
{ |
|||
// TODO: Check for duplicates (Task 14107720)
|
|||
// TODO: Optimize by merging adjacent ranges (Task 14107720)
|
|||
var oldCount = SelectedCount; |
|||
|
|||
for (int i = addRange.Begin; i <= addRange.End; i++) |
|||
{ |
|||
if (!IsSelected(i)) |
|||
{ |
|||
_selectedCount++; |
|||
} |
|||
} |
|||
|
|||
if (oldCount != _selectedCount) |
|||
{ |
|||
_selected.Add(addRange); |
|||
|
|||
if (raiseOnSelectionChanged) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged) |
|||
{ |
|||
int oldCount = _selectedCount; |
|||
|
|||
// TODO: Prevent overlap of Ranges in _selected (Task 14107720)
|
|||
for (int i = removeRange.Begin; i <= removeRange.End; i++) |
|||
{ |
|||
if (IsSelected(i)) |
|||
{ |
|||
_selectedCount--; |
|||
} |
|||
} |
|||
|
|||
if (oldCount != _selectedCount) |
|||
{ |
|||
// Build up a both a list of Ranges to remove and ranges to add
|
|||
var toRemove = new List<IndexRange>(); |
|||
var toAdd = new List<IndexRange>(); |
|||
|
|||
foreach (var range in _selected) |
|||
{ |
|||
// If this range intersects the remove range, we have to do something
|
|||
if (removeRange.Intersects(range)) |
|||
{ |
|||
// Intersection with the beginning of the range
|
|||
// Anything to the left of the point (exclusive) stays
|
|||
// Anything to the right of the point (inclusive) gets clipped
|
|||
if (range.Contains(removeRange.Begin - 1)) |
|||
{ |
|||
range.Split(removeRange.Begin - 1, out var before, out _); |
|||
toAdd.Add(before); |
|||
} |
|||
|
|||
// Intersection with the end of the range
|
|||
// Anything to the left of the point (inclusive) gets clipped
|
|||
// Anything to the right of the point (exclusive) stays
|
|||
if (range.Contains(removeRange.End)) |
|||
{ |
|||
if (range.Split(removeRange.End, out _, out var after)) |
|||
{ |
|||
toAdd.Add(after); |
|||
} |
|||
} |
|||
|
|||
// Remove this Range from the collection
|
|||
// New ranges will be added for any remaining subsections
|
|||
toRemove.Add(range); |
|||
} |
|||
} |
|||
|
|||
bool change = ((toRemove.Count > 0) || (toAdd.Count > 0)); |
|||
|
|||
if (change) |
|||
{ |
|||
// Remove tagged ranges
|
|||
foreach (var remove in toRemove) |
|||
{ |
|||
_selected.Remove(remove); |
|||
} |
|||
|
|||
// Add new ranges
|
|||
_selected.AddRange(toAdd); |
|||
|
|||
if (raiseOnSelectionChanged) |
|||
{ |
|||
OnSelectionChanged(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void ClearSelection() |
|||
{ |
|||
// Deselect all items
|
|||
if (_selected.Count > 0) |
|||
{ |
|||
_selected.Clear(); |
|||
OnSelectionChanged(); |
|||
} |
|||
|
|||
_selectedCount = 0; |
|||
AnchorIndex = -1; |
|||
|
|||
// This will throw away all the children SelectionNodes
|
|||
// causing them to be unhooked from their data source. This
|
|||
// essentially cleans up the tree.
|
|||
foreach (var child in _childrenNodes) |
|||
{ |
|||
child?.Dispose(); |
|||
} |
|||
|
|||
_childrenNodes.Clear(); |
|||
} |
|||
|
|||
private bool Select(int index, bool select, bool raiseOnSelectionChanged) |
|||
{ |
|||
if (IsValidIndex(index)) |
|||
{ |
|||
// Ignore duplicate selection calls
|
|||
if (IsSelected(index) == select) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
var range = new IndexRange(index, index); |
|||
|
|||
if (select) |
|||
{ |
|||
AddRange(range, raiseOnSelectionChanged); |
|||
} |
|||
else |
|||
{ |
|||
RemoveRange(range, raiseOnSelectionChanged); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args) |
|||
{ |
|||
bool selectionInvalidated = false; |
|||
|
|||
switch (args.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
{ |
|||
selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Remove: |
|||
{ |
|||
selectionInvalidated = OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count); |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Reset: |
|||
{ |
|||
ClearSelection(); |
|||
selectionInvalidated = true; |
|||
break; |
|||
} |
|||
|
|||
case NotifyCollectionChangedAction.Replace: |
|||
{ |
|||
selectionInvalidated = OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count); |
|||
selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (selectionInvalidated) |
|||
{ |
|||
OnSelectionChanged(); |
|||
_manager.OnSelectionInvalidatedDueToCollectionChange(); |
|||
} |
|||
} |
|||
|
|||
private bool OnItemsAdded(int index, int count) |
|||
{ |
|||
var selectionInvalidated = false; |
|||
|
|||
// Update ranges for leaf items
|
|||
var toAdd = new List<IndexRange>(); |
|||
|
|||
for (int i = 0; i < _selected.Count; i++) |
|||
{ |
|||
var range = _selected[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.Add(before); |
|||
begin = index; |
|||
} |
|||
|
|||
// Shift the range to the right
|
|||
_selected[i] = new IndexRange(begin + count, range.End + count); |
|||
selectionInvalidated = true; |
|||
} |
|||
} |
|||
|
|||
// Add the left sides of the split ranges
|
|||
_selected.AddRange(toAdd); |
|||
|
|||
// Update for non-leaf if we are tracking non-leaf nodes
|
|||
if (_childrenNodes.Count > 0) |
|||
{ |
|||
selectionInvalidated = true; |
|||
for (int i = 0; i < count; i++) |
|||
{ |
|||
_childrenNodes.Insert(index, null); |
|||
} |
|||
} |
|||
|
|||
// Adjust the anchor
|
|||
if (AnchorIndex >= index) |
|||
{ |
|||
AnchorIndex = AnchorIndex + count; |
|||
} |
|||
|
|||
// Check if adding a node invalidated an ancestors
|
|||
// selection state. For example if parent was selected before
|
|||
// adding a new item makes the parent partially selected now.
|
|||
if (!selectionInvalidated) |
|||
{ |
|||
var parent = _parent; |
|||
|
|||
while (parent != null) |
|||
{ |
|||
var isSelected = parent.IsSelectedWithPartial(); |
|||
|
|||
// If a parent is selected, then it will become partially selected.
|
|||
// If it is not selected or partially selected - there is no change.
|
|||
if (isSelected == true) |
|||
{ |
|||
selectionInvalidated = true; |
|||
break; |
|||
} |
|||
|
|||
parent = parent._parent; |
|||
} |
|||
} |
|||
|
|||
return selectionInvalidated; |
|||
} |
|||
|
|||
private bool OnItemsRemoved(int index, int count) |
|||
{ |
|||
bool selectionInvalidated = false; |
|||
|
|||
// Remove the items from the selection for leaf
|
|||
if (ItemsSourceView.Count > 0) |
|||
{ |
|||
bool isSelected = false; |
|||
|
|||
for (int i = index; i <= index + count - 1; i++) |
|||
{ |
|||
if (IsSelected(i)) |
|||
{ |
|||
isSelected = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (isSelected) |
|||
{ |
|||
RemoveRange(new IndexRange(index, index + count - 1), raiseOnSelectionChanged: false); |
|||
selectionInvalidated = true; |
|||
} |
|||
|
|||
for (int i = 0; i < _selected.Count; i++) |
|||
{ |
|||
var range = _selected[i]; |
|||
|
|||
// The range is after the removed items, need to shift the range left
|
|||
if (range.End > index) |
|||
{ |
|||
// Shift the range to the left
|
|||
_selected[i] = new IndexRange(range.Begin - count, range.End - count); |
|||
selectionInvalidated = true; |
|||
} |
|||
} |
|||
|
|||
// Update for non-leaf if we are tracking non-leaf nodes
|
|||
if (_childrenNodes.Count > 0) |
|||
{ |
|||
selectionInvalidated = true; |
|||
for (int i = 0; i < count; i++) |
|||
{ |
|||
if (_childrenNodes[index] != null) |
|||
{ |
|||
_realizedChildrenNodeCount--; |
|||
} |
|||
_childrenNodes.RemoveAt(index); |
|||
} |
|||
} |
|||
|
|||
//Adjust the anchor
|
|||
if (AnchorIndex >= index) |
|||
{ |
|||
AnchorIndex = AnchorIndex - count; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// No more items in the list, clear
|
|||
ClearSelection(); |
|||
_realizedChildrenNodeCount = 0; |
|||
selectionInvalidated = true; |
|||
} |
|||
|
|||
// Check if removing a node invalidated an ancestors
|
|||
// selection state. For example if parent was partially selected before
|
|||
// removing an item, it could be selected now.
|
|||
if (!selectionInvalidated) |
|||
{ |
|||
var parent = _parent; |
|||
|
|||
while (parent != null) |
|||
{ |
|||
var isSelected = parent.IsSelectedWithPartial(); |
|||
// If a parent is partially selected, then it will become selected.
|
|||
// If it is selected or not selected - there is no change.
|
|||
if (!isSelected.HasValue) |
|||
{ |
|||
selectionInvalidated = true; |
|||
break; |
|||
} |
|||
|
|||
parent = parent._parent; |
|||
} |
|||
} |
|||
|
|||
return selectionInvalidated; |
|||
} |
|||
|
|||
private void OnSelectionChanged() |
|||
{ |
|||
_selectedIndicesCacheIsValid = false; |
|||
_selectedIndicesCached.Clear(); |
|||
} |
|||
|
|||
public static bool? ConvertToNullableBool(SelectionState isSelected) |
|||
{ |
|||
bool? result = null; // PartialySelected
|
|||
|
|||
if (isSelected == SelectionState.Selected) |
|||
{ |
|||
result = true; |
|||
} |
|||
else if (isSelected == SelectionState.NotSelected) |
|||
{ |
|||
result = false; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public SelectionState EvaluateIsSelectedBasedOnChildrenNodes() |
|||
{ |
|||
SelectionState selectionState = SelectionState.NotSelected; |
|||
int realizedChildrenNodeCount = RealizedChildrenNodeCount; |
|||
int selectedCount = SelectedCount; |
|||
|
|||
if (realizedChildrenNodeCount != 0 || selectedCount != 0) |
|||
{ |
|||
// There are realized children or some selected leaves.
|
|||
int dataCount = DataCount; |
|||
if (realizedChildrenNodeCount == 0 && selectedCount > 0) |
|||
{ |
|||
// All nodes are leaves under it - we didn't create children nodes as an optimization.
|
|||
// See if all/some or none of the leaves are selected.
|
|||
selectionState = dataCount != selectedCount ? |
|||
SelectionState.PartiallySelected : |
|||
dataCount == selectedCount ? SelectionState.Selected : SelectionState.NotSelected; |
|||
} |
|||
else |
|||
{ |
|||
// There are child nodes, walk them individually and evaluate based on each child
|
|||
// being selected/not selected or partially selected.
|
|||
bool isSelected = false; |
|||
selectedCount = 0; |
|||
int notSelectedCount = 0; |
|||
for (int i = 0; i < ChildrenNodeCount; i++) |
|||
{ |
|||
var child = GetAt(i, realizeChild: false); |
|||
|
|||
if (child != null) |
|||
{ |
|||
// child is realized, ask it.
|
|||
var isChildSelected = IsSelectedWithPartial(i); |
|||
if (isChildSelected == null) |
|||
{ |
|||
selectionState = SelectionState.PartiallySelected; |
|||
break; |
|||
} |
|||
else if (isChildSelected == true) |
|||
{ |
|||
selectedCount++; |
|||
} |
|||
else |
|||
{ |
|||
notSelectedCount++; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// not realized.
|
|||
if (IsSelected(i)) |
|||
{ |
|||
selectedCount++; |
|||
} |
|||
else |
|||
{ |
|||
notSelectedCount++; |
|||
} |
|||
} |
|||
|
|||
if (selectedCount > 0 && notSelectedCount > 0) |
|||
{ |
|||
selectionState = SelectionState.PartiallySelected; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (selectionState != SelectionState.PartiallySelected) |
|||
{ |
|||
if (selectedCount != 0 && selectedCount != dataCount) |
|||
{ |
|||
selectionState = SelectionState.PartiallySelected; |
|||
} |
|||
else |
|||
{ |
|||
selectionState = selectedCount == dataCount ? SelectionState.Selected : SelectionState.NotSelected; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return selectionState; |
|||
} |
|||
|
|||
public enum SelectionState |
|||
{ |
|||
Selected, |
|||
NotSelected, |
|||
PartiallySelected |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
// 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; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Controls.Utils |
|||
{ |
|||
internal static class SelectionTreeHelper |
|||
{ |
|||
public static void TraverseIndexPath( |
|||
SelectionNode root, |
|||
IndexPath path, |
|||
bool realizeChildren, |
|||
Action<SelectionNode, IndexPath, int, int> nodeAction) |
|||
{ |
|||
var node = root; |
|||
|
|||
for (int depth = 0; depth < path.GetSize(); depth++) |
|||
{ |
|||
int childIndex = path.GetAt(depth); |
|||
nodeAction(node, path, depth, childIndex); |
|||
|
|||
if (depth < path.GetSize() - 1) |
|||
{ |
|||
node = node.GetAt(childIndex, realizeChildren); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static void Traverse( |
|||
SelectionNode root, |
|||
bool realizeChildren, |
|||
Action<TreeWalkNodeInfo> nodeAction) |
|||
{ |
|||
var pendingNodes = new List<TreeWalkNodeInfo>(); |
|||
var current = new IndexPath(null); |
|||
|
|||
pendingNodes.Add(new TreeWalkNodeInfo(root, current)); |
|||
|
|||
while (pendingNodes.Count > 0) |
|||
{ |
|||
var nextNode = pendingNodes.Last(); |
|||
pendingNodes.RemoveAt(pendingNodes.Count - 1); |
|||
int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount; |
|||
for (int i = count - 1; i >= 0; i--) |
|||
{ |
|||
SelectionNode child = nextNode.Node.GetAt(i, realizeChildren); |
|||
var childPath = nextNode.Path.CloneWithChildIndex(i); |
|||
if (child != null) |
|||
{ |
|||
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, nextNode.Node)); |
|||
} |
|||
} |
|||
|
|||
// Queue the children first and then perform the action. This way
|
|||
// the action can remove the children in the action if necessary
|
|||
nodeAction(nextNode); |
|||
} |
|||
} |
|||
|
|||
public static void TraverseRangeRealizeChildren( |
|||
SelectionNode root, |
|||
IndexPath start, |
|||
IndexPath end, |
|||
Action<TreeWalkNodeInfo> nodeAction) |
|||
{ |
|||
var pendingNodes = new List<TreeWalkNodeInfo>(); |
|||
var current = start; |
|||
|
|||
// Build up the stack to account for the depth first walk up to the
|
|||
// start index path.
|
|||
TraverseIndexPath( |
|||
root, |
|||
start, |
|||
true, |
|||
(node, path, depth, childIndex) => |
|||
{ |
|||
var currentPath = StartPath(path, depth); |
|||
bool isStartPath = IsSubSet(start, currentPath); |
|||
bool isEndPath = IsSubSet(end, currentPath); |
|||
|
|||
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0; |
|||
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : node.DataCount - 1; |
|||
|
|||
for (int i = endIndex; i >= startIndex; i--) |
|||
{ |
|||
var child = node.GetAt(i, realizeChild: true); |
|||
if (child != null) |
|||
{ |
|||
var childPath = currentPath.CloneWithChildIndex(i); |
|||
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, node)); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
// From the start index path, do a depth first walk as long as the
|
|||
// current path is less than the end path.
|
|||
while (pendingNodes.Count > 0) |
|||
{ |
|||
var info = pendingNodes.Last(); |
|||
pendingNodes.RemoveAt(pendingNodes.Count - 1); |
|||
int depth = info.Path.GetSize(); |
|||
bool isStartPath = IsSubSet(start, info.Path); |
|||
bool isEndPath = IsSubSet(end, info.Path); |
|||
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0; |
|||
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1; |
|||
for (int i = endIndex; i >= startIndex; i--) |
|||
{ |
|||
var child = info.Node.GetAt(i, realizeChild: true); |
|||
if (child != null) |
|||
{ |
|||
var childPath = info.Path.CloneWithChildIndex(i); |
|||
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, info.Node)); |
|||
} |
|||
} |
|||
|
|||
nodeAction(info); |
|||
|
|||
if (info.Path.CompareTo(end) == 0) |
|||
{ |
|||
// We reached the end index path. stop iterating.
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static bool IsSubSet(IndexPath path, IndexPath subset) |
|||
{ |
|||
bool isSubset = true; |
|||
for (int i = 0; i < subset.GetSize(); i++) |
|||
{ |
|||
isSubset = path.GetAt(i) == subset.GetAt(i); |
|||
if (!isSubset) |
|||
break; |
|||
} |
|||
|
|||
return isSubset; |
|||
} |
|||
|
|||
private static IndexPath StartPath(IndexPath path, int length) |
|||
{ |
|||
var subPath = new List<int>(); |
|||
for (int i = 0; i < length; i++) |
|||
{ |
|||
subPath.Add(path.GetAt(i)); |
|||
} |
|||
|
|||
return new IndexPath(subPath); |
|||
} |
|||
|
|||
public struct TreeWalkNodeInfo |
|||
{ |
|||
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode parent) |
|||
{ |
|||
node = node ?? throw new ArgumentNullException(nameof(node)); |
|||
indexPath = indexPath ?? throw new ArgumentNullException(nameof(indexPath)); |
|||
|
|||
Node = node; |
|||
Path = indexPath; |
|||
ParentNode = parent; |
|||
} |
|||
|
|||
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath) |
|||
{ |
|||
node = node ?? throw new ArgumentNullException(nameof(node)); |
|||
indexPath = indexPath ?? throw new ArgumentNullException(nameof(indexPath)); |
|||
|
|||
Node = node; |
|||
Path = indexPath; |
|||
ParentNode = null; |
|||
} |
|||
|
|||
public SelectionNode Node { get; } |
|||
public IndexPath Path { get; } |
|||
public SelectionNode ParentNode { get; } |
|||
}; |
|||
|
|||
} |
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue