| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966 |
- // 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;
- using System.Linq;
- #nullable enable
- 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 readonly List<int> _selectedIndicesCached = new List<int>();
- private IDisposable? _childrenSubscription;
- private SelectionNodeOperation? _operation;
- private object? _source;
- private bool _selectedIndicesCacheIsValid;
- private bool _retainSelectionOnReset;
- private List<object?>? _selectedItems;
- public SelectionNode(SelectionModel manager, SelectionNode? parent)
- {
- _manager = manager;
- _parent = parent;
- }
- public int AnchorIndex { get; set; } = -1;
- public bool RetainSelectionOnReset
- {
- get => _retainSelectionOnReset;
- set
- {
- if (_retainSelectionOnReset != value)
- {
- _retainSelectionOnReset = value;
- if (_retainSelectionOnReset)
- {
- _selectedItems = new List<object?>();
- PopulateSelectedItemsFromSelectedIndices();
- }
- else
- {
- _selectedItems = null;
- }
- foreach (var child in _childrenNodes)
- {
- if (child != null)
- {
- child.RetainSelectionOnReset = value;
- }
- }
- }
- }
- }
- public object? Source
- {
- get => _source;
- set
- {
- if (_source != value)
- {
- if (_source != null)
- {
- ClearSelection();
- ClearChildNodes();
- UnhookCollectionChangedHandler();
- }
- _source = value;
- // Setup ItemsSourceView
- var newDataSource = value as ItemsSourceView;
- if (value != null && newDataSource == null)
- {
- newDataSource = new ItemsSourceView((IEnumerable)value);
- }
- ItemsSourceView = newDataSource;
- PopulateSelectedItemsFromSelectedIndices();
- HookupCollectionChangedHandler();
- OnSelectionChanged();
- }
- }
- }
- public ItemsSourceView? ItemsSourceView { get; private set; }
- public int DataCount => ItemsSourceView?.Count ?? 0;
- public int ChildrenNodeCount => _childrenNodes.Count;
- public int RealizedChildrenNodeCount { get; private set; }
- 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 (ItemsSourceView == null || index < 0 || index >= ItemsSourceView.Count)
- {
- throw new IndexOutOfRangeException();
- }
- if (_childrenNodes.Count == 0)
- {
- if (ItemsSourceView != null)
- {
- for (int i = 0; i < ItemsSourceView.Count; i++)
- {
- _childrenNodes.Add(null);
- }
- }
- }
- if (_childrenNodes[index] == null)
- {
- var childData = ItemsSourceView!.GetAt(index);
- IObservable<object?>? resolver = null;
-
- if (childData != null)
- {
- var childDataIndexPath = IndexPath.CloneWithChildIndex(index);
- resolver = _manager.ResolvePath(childData, childDataIndexPath);
- }
- if (resolver != null)
- {
- child = new SelectionNode(_manager, parent: this);
- child.SetChildrenObservable(resolver);
- }
- else if (childData is IEnumerable<object> || childData is IList)
- {
- child = new SelectionNode(_manager, parent: this);
- child.Source = childData;
- }
- else
- {
- child = _manager.SharedLeafNode;
- }
- if (_operation != null && child != _manager.SharedLeafNode)
- {
- child.BeginOperation();
- }
- _childrenNodes[index] = child;
- RealizedChildrenNodeCount++;
- }
- else
- {
- child = _childrenNodes[index];
- }
- }
- else
- {
- if (_childrenNodes.Count > 0)
- {
- child = _childrenNodes[index];
- }
- }
- return child;
- }
- public void SetChildrenObservable(IObservable<object?> resolver)
- {
- _childrenSubscription = resolver.Subscribe(x => Source = x);
- }
- public int SelectedCount { get; private set; }
- 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)
- {
- SelectionState selectionState;
- 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 IEnumerable<object> SelectedItems
- {
- get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x));
- }
- public void Dispose()
- {
- _childrenSubscription?.Dispose();
- ItemsSourceView?.Dispose();
- ClearChildNodes();
- UnhookCollectionChangedHandler();
- }
- public void BeginOperation()
- {
- if (_operation != null)
- {
- throw new AvaloniaInternalException("Selection operation already in progress.");
- }
- _operation = new SelectionNodeOperation(this);
- for (var i = 0; i < _childrenNodes.Count; ++i)
- {
- var child = _childrenNodes[i];
- if (child != null && child != _manager.SharedLeafNode)
- {
- child.BeginOperation();
- }
- }
- }
- public void EndOperation(List<SelectionNodeOperation> changes)
- {
- if (_operation == null)
- {
- throw new AvaloniaInternalException("No selection operation in progress.");
- }
- if (_operation.HasChanges)
- {
- changes.Add(_operation);
- }
- _operation = null;
- for (var i = 0; i < _childrenNodes.Count; ++i)
- {
- var child = _childrenNodes[i];
- if (child != null && child != _manager.SharedLeafNode)
- {
- child.EndOperation(changes);
- }
- }
- }
- public bool Cleanup()
- {
- var result = SelectedCount == 0;
- for (var i = 0; i < _childrenNodes.Count; ++i)
- {
- var child = _childrenNodes[i];
- if (child != null)
- {
- if (child.Cleanup())
- {
- child.Dispose();
- _childrenNodes[i] = null;
- }
- else
- {
- result = false;
- }
- }
- }
- return result;
- }
- 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 (ItemsSourceView != null)
- {
- var size = ItemsSourceView.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 (ItemsSourceView != null)
- {
- ItemsSourceView.CollectionChanged += OnSourceListChanged;
- }
- }
- private void UnhookCollectionChangedHandler()
- {
- if (ItemsSourceView != null)
- {
- ItemsSourceView.CollectionChanged -= OnSourceListChanged;
- }
- }
- private bool IsValidIndex(int index)
- {
- return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count);
- }
- private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged)
- {
- var selected = new List<IndexRange>();
- SelectedCount += IndexRange.Add(_selected, addRange, selected);
- if (selected.Count > 0)
- {
- _operation?.Selected(selected);
- if (_selectedItems != null && ItemsSourceView != null)
- {
- for (var i = addRange.Begin; i <= addRange.End; ++i)
- {
- _selectedItems.Add(ItemsSourceView!.GetAt(i));
- }
- }
- if (raiseOnSelectionChanged)
- {
- OnSelectionChanged();
- }
- }
- }
- private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged)
- {
- var removed = new List<IndexRange>();
- SelectedCount -= IndexRange.Remove(_selected, removeRange, removed);
- if (removed.Count > 0)
- {
- _operation?.Deselected(removed);
- if (_selectedItems != null)
- {
- for (var i = removeRange.Begin; i <= removeRange.End; ++i)
- {
- _selectedItems.Remove(ItemsSourceView!.GetAt(i));
- }
- }
- if (raiseOnSelectionChanged)
- {
- OnSelectionChanged();
- }
- }
- }
- private void ClearSelection()
- {
- // Deselect all items
- if (_selected.Count > 0)
- {
- _operation?.Deselected(_selected);
- _selected.Clear();
- OnSelectionChanged();
- }
- _selectedItems?.Clear();
- SelectedCount = 0;
- AnchorIndex = -1;
- }
- private void ClearChildNodes()
- {
- foreach (var child in _childrenNodes)
- {
- if (child != null && child != _manager.SharedLeafNode)
- {
- child.Dispose();
- }
- }
- RealizedChildrenNodeCount = 0;
- }
- 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;
- List<object?>? removed = null;
- switch (args.Action)
- {
- case NotifyCollectionChangedAction.Add:
- {
- selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
- break;
- }
- case NotifyCollectionChangedAction.Remove:
- {
- (selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
- break;
- }
- case NotifyCollectionChangedAction.Reset:
- {
- if (_selectedItems == null)
- {
- ClearSelection();
- }
- else
- {
- removed = RecreateSelectionFromSelectedItems();
- }
- selectionInvalidated = true;
- break;
- }
- case NotifyCollectionChangedAction.Replace:
- {
- (selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
- selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
- break;
- }
- }
- if (selectionInvalidated)
- {
- OnSelectionChanged();
- }
- _manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed);
- }
- 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 += 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, List<object?>) OnItemsRemoved(int index, IList items)
- {
- var selectionInvalidated = false;
- var removed = new List<object?>();
- var count = items.Count;
-
- // Remove the items from the selection for leaf
- if (ItemsSourceView!.Count > 0)
- {
- bool isSelected = false;
- for (int i = 0; i <= count - 1; i++)
- {
- if (IsSelected(index + i))
- {
- isSelected = true;
- removed.Add(items[i]);
- }
- }
- if (isSelected)
- {
- var removeRange = new IndexRange(index, index + count - 1);
- SelectedCount -= IndexRange.Remove(_selected, removeRange);
- selectionInvalidated = true;
- if (_selectedItems != null)
- {
- foreach (var i in items)
- {
- _selectedItems.Remove(i);
- }
- }
- }
- 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)
- {
- removed.AddRange(_childrenNodes[index]!.SelectedItems);
- RealizedChildrenNodeCount--;
- _childrenNodes[index]!.Dispose();
- }
- _childrenNodes.RemoveAt(index);
- }
- }
- //Adjust the anchor
- if (AnchorIndex >= index)
- {
- 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, removed);
- }
- 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()
- {
- var 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.
- 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;
- }
- private void PopulateSelectedItemsFromSelectedIndices()
- {
- if (_selectedItems != null)
- {
- _selectedItems.Clear();
- foreach (var i in SelectedIndices)
- {
- _selectedItems.Add(ItemsSourceView!.GetAt(i));
- }
- }
- }
- private List<object?> RecreateSelectionFromSelectedItems()
- {
- var removed = new List<object?>();
- _selected.Clear();
- SelectedCount = 0;
- for (var i = 0; i < _selectedItems!.Count; ++i)
- {
- var item = _selectedItems[i];
- var index = ItemsSourceView!.IndexOf(item);
- if (index != -1)
- {
- IndexRange.Add(_selected, new IndexRange(index, index));
- ++SelectedCount;
- }
- else
- {
- removed.Add(item);
- _selectedItems.RemoveAt(i--);
- }
- }
- return removed;
- }
- public enum SelectionState
- {
- Selected,
- NotSelected,
- PartiallySelected
- }
- }
- }
|