| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Collections.Specialized;
- using System.ComponentModel;
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- #nullable enable
- namespace Avalonia.Controls.Selection
- {
- public class SelectionModel<T> : SelectionNodeBase<T>, ISelectionModel
- {
- private bool _singleSelect = true;
- private int _anchorIndex = -1;
- private int _selectedIndex = -1;
- private Operation? _operation;
- private SelectedIndexes<T>? _selectedIndexes;
- private SelectedItems<T>? _selectedItems;
- private SelectedItems<T>.Untyped? _selectedItemsUntyped;
- private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
- private IList? _initSelectedItems;
- public SelectionModel()
- {
- }
- public SelectionModel(IEnumerable<T>? source)
- {
- Source = source;
- }
- public new IEnumerable<T>? Source
- {
- get => base.Source as IEnumerable<T>;
- set => SetSource(value);
- }
- public bool SingleSelect
- {
- get => _singleSelect;
- set
- {
- if (_singleSelect != value)
- {
- if (value == true)
- {
- using var update = BatchUpdate();
- var selectedIndex = SelectedIndex;
- Clear();
- SelectedIndex = selectedIndex;
- }
- _singleSelect = value;
- RangesEnabled = !value;
- if (RangesEnabled && _selectedIndex >= 0)
- {
- CommitSelect(new IndexRange(_selectedIndex));
- }
- RaisePropertyChanged(nameof(SingleSelect));
- }
- }
- }
- public int SelectedIndex
- {
- get => _selectedIndex;
- set
- {
- using var update = BatchUpdate();
- Clear();
- Select(value);
- }
- }
- public IReadOnlyList<int> SelectedIndexes => _selectedIndexes ??= new SelectedIndexes<T>(this);
- [MaybeNull, AllowNull]
- public T SelectedItem
- {
- get
- {
- if (ItemsView is object)
- {
- return GetItemAt(_selectedIndex);
- }
- else if (_initSelectedItems is object && _initSelectedItems.Count > 0)
- {
- return (T)_initSelectedItems[0];
- }
- return default;
- }
- set
- {
- if (ItemsView is object)
- {
- SelectedIndex = ItemsView.IndexOf(value!);
- }
- else
- {
- Clear();
- #pragma warning disable CS8601
- SetInitSelectedItems(new T[] { value });
- #pragma warning restore CS8601
- }
- }
- }
- public IReadOnlyList<T> SelectedItems
- {
- get
- {
- if (ItemsView is null && _initSelectedItems is object)
- {
- return _initSelectedItems is IReadOnlyList<T> i ?
- i : _initSelectedItems.Cast<T>().ToList();
- }
- return _selectedItems ??= new SelectedItems<T>(this);
- }
- }
- public int AnchorIndex
- {
- get => _anchorIndex;
- set
- {
- using var update = BatchUpdate();
- var index = CoerceIndex(value);
- update.Operation.AnchorIndex = index;
- }
- }
- public int Count
- {
- get
- {
- if (SingleSelect)
- {
- return _selectedIndex >= 0 ? 1 : 0;
- }
- else
- {
- return IndexRange.GetCount(Ranges);
- }
- }
- }
- IEnumerable? ISelectionModel.Source
- {
- get => Source;
- set => SetSource(value);
- }
- object? ISelectionModel.SelectedItem
- {
- get => SelectedItem;
- set
- {
- if (value is T t)
- {
- SelectedItem = t;
- }
- else
- {
- SelectedIndex = -1;
- }
- }
-
- }
- IReadOnlyList<object?> ISelectionModel.SelectedItems
- {
- get => _selectedItemsUntyped ??= new SelectedItems<T>.Untyped(SelectedItems);
- }
- public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged;
- public event EventHandler<SelectionModelSelectionChangedEventArgs<T>>? SelectionChanged;
- public event EventHandler? LostSelection;
- public event EventHandler? SourceReset;
- public event PropertyChangedEventHandler? PropertyChanged;
- event EventHandler<SelectionModelSelectionChangedEventArgs>? ISelectionModel.SelectionChanged
- {
- add => _untypedSelectionChanged += value;
- remove => _untypedSelectionChanged -= value;
- }
- public BatchUpdateOperation BatchUpdate() => new BatchUpdateOperation(this);
- public void BeginBatchUpdate()
- {
- _operation ??= new Operation(this);
- ++_operation.UpdateCount;
- }
- public void EndBatchUpdate()
- {
- if (_operation is null || _operation.UpdateCount == 0)
- {
- throw new InvalidOperationException("No batch update in progress.");
- }
- if (--_operation.UpdateCount == 0)
- {
- // If the collection is currently changing, commit the update when the
- // collection change finishes.
- if (!IsSourceCollectionChanging)
- {
- CommitOperation(_operation);
- }
- }
- }
- public bool IsSelected(int index)
- {
- if (index < 0)
- {
- return false;
- }
- else if (SingleSelect)
- {
- return _selectedIndex == index;
- }
- else
- {
- return IndexRange.Contains(Ranges, index);
- }
- }
- public void Select(int index) => SelectRange(index, index, false, true);
- public void Deselect(int index) => DeselectRange(index, index);
- public void SelectRange(int start, int end) => SelectRange(start, end, false, false);
- public void DeselectRange(int start, int end)
- {
- using var update = BatchUpdate();
- var o = update.Operation;
- var range = new IndexRange(Math.Max(0, start), end);
- if (RangesEnabled)
- {
- var selected = Ranges.ToList();
- var deselected = new List<IndexRange>();
- var operationDeselected = new List<IndexRange>();
- o.DeselectedRanges ??= new List<IndexRange>();
- IndexRange.Remove(o.SelectedRanges, range, operationDeselected);
- IndexRange.Remove(selected, range, deselected);
- IndexRange.Add(o.DeselectedRanges, deselected);
- if (IndexRange.Contains(deselected, o.SelectedIndex) ||
- IndexRange.Contains(operationDeselected, o.SelectedIndex))
- {
- o.SelectedIndex = GetFirstSelectedIndexFromRanges(except: deselected);
- }
- }
- else if(range.Contains(_selectedIndex))
- {
- o.SelectedIndex = -1;
- }
- _initSelectedItems = null;
- }
- public void SelectAll() => SelectRange(0, int.MaxValue);
- public void Clear() => DeselectRange(0, int.MaxValue);
- protected void RaisePropertyChanged(string propertyName)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- private protected virtual void SetSource(IEnumerable? value)
- {
- if (base.Source != value)
- {
- if (_operation is object)
- {
- throw new InvalidOperationException("Cannot change source while update is in progress.");
- }
- if (base.Source is object && value is object)
- {
- using var update = BatchUpdate();
- update.Operation.SkipLostSelection = true;
- Clear();
- }
- base.Source = value;
- using (var update = BatchUpdate())
- {
- update.Operation.IsSourceUpdate = true;
- if (_initSelectedItems is object && ItemsView is object)
- {
- foreach (T i in _initSelectedItems)
- {
- Select(ItemsView.IndexOf(i));
- }
- _initSelectedItems = null;
- }
- else
- {
- TrimInvalidSelections(update.Operation);
- }
- RaisePropertyChanged(nameof(Source));
- }
- }
- }
- private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
- {
- IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta));
- }
- private protected override void OnSourceReset()
- {
- _selectedIndex = _anchorIndex = -1;
- CommitDeselect(new IndexRange(0, int.MaxValue));
- if (SourceReset is object)
- {
- SourceReset.Invoke(this, EventArgs.Empty);
- }
- else
- {
- //Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
- // this,
- // "SelectionModel received Reset but no SourceReset handler was registered to handle it. " +
- // "Selection may be out of sync.",
- // typeof(SelectionModel));
- }
- }
- private protected override void OnSelectionChanged(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 commited by normal means: we have to commit it manually.
- var update = BatchUpdate();
- update.Operation.DeselectedItems = deselectedItems;
- if (_selectedIndex == -1 && LostSelection is object)
- {
- LostSelection(this, EventArgs.Empty);
- }
- // Don't raise PropertyChanged events here as the OnSourceCollectionChanged event that
- // let to this method being called will raise them if necessary.
- CommitOperation(update.Operation, raisePropertyChanged: false);
- }
- private protected override CollectionChangeState OnItemsAdded(int index, IList items)
- {
- var count = items.Count;
- var shifted = SelectedIndex >= index;
- var shiftCount = shifted ? count : 0;
- _selectedIndex += shiftCount;
- _anchorIndex += shiftCount;
- var baseResult = base.OnItemsAdded(index, items);
- shifted |= baseResult.ShiftDelta != 0;
- return new CollectionChangeState
- {
- ShiftIndex = index,
- ShiftDelta = shifted ? count : 0,
- };
- }
- private protected override CollectionChangeState OnItemsRemoved(int index, IList items)
- {
- var count = items.Count;
- var removedRange = new IndexRange(index, index + count - 1);
- var shifted = false;
- List<T>? removed;
- var baseResult = base.OnItemsRemoved(index, items);
- shifted |= baseResult.ShiftDelta != 0;
- removed = baseResult.RemovedItems;
- if (removedRange.Contains(SelectedIndex))
- {
- if (SingleSelect)
- {
- #pragma warning disable CS8604
- removed = new List<T> { (T)items[SelectedIndex - index] };
- #pragma warning restore CS8604
- }
- _selectedIndex = GetFirstSelectedIndexFromRanges();
- }
- else if (SelectedIndex >= index)
- {
- _selectedIndex -= count;
- shifted = true;
- }
- if (removedRange.Contains(AnchorIndex))
- {
- _anchorIndex = GetFirstSelectedIndexFromRanges();
- }
- else if (AnchorIndex >= index)
- {
- _anchorIndex -= count;
- shifted = true;
- }
- return new CollectionChangeState
- {
- ShiftIndex = index,
- ShiftDelta = shifted ? -count : 0,
- RemovedItems = removed,
- };
- }
- private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
- {
- if (_operation?.UpdateCount > 0)
- {
- throw new InvalidOperationException("Source collection was modified during selection update.");
- }
- var oldAnchorIndex = _anchorIndex;
- var oldSelectedIndex = _selectedIndex;
- base.OnSourceCollectionChanged(e);
- if (oldSelectedIndex != _selectedIndex)
- {
- RaisePropertyChanged(nameof(SelectedIndex));
- }
- if ((e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex) ||
- (e.Action == NotifyCollectionChangedAction.Replace && e.OldStartingIndex == oldSelectedIndex) ||
- e.Action == NotifyCollectionChangedAction.Reset)
- {
- RaisePropertyChanged(nameof(SelectedItem));
- }
- if (oldAnchorIndex != _anchorIndex)
- {
- RaisePropertyChanged(nameof(AnchorIndex));
- }
- }
- private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
- {
- if (!base.IsValidCollectionChange(e))
- {
- return false;
- }
- if (ItemsView is object && e.Action == NotifyCollectionChangedAction.Add)
- {
- if (e.NewStartingIndex <= _selectedIndex)
- {
- return _selectedIndex + e.NewItems.Count < ItemsView.Count;
- }
- if (e.NewStartingIndex <= _anchorIndex)
- {
- return _anchorIndex + e.NewItems.Count < ItemsView.Count;
- }
- }
- 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)
- {
- CommitOperation(_operation);
- }
- }
- private int GetFirstSelectedIndexFromRanges(List<IndexRange>? except = null)
- {
- if (RangesEnabled)
- {
- var count = IndexRange.GetCount(Ranges);
- var index = 0;
- while (index < count)
- {
- var result = IndexRange.GetAt(Ranges, index++);
- if (!IndexRange.Contains(except, result))
- {
- return result;
- }
- }
- }
- return -1;
- }
- private void SelectRange(
- int start,
- int end,
- bool forceSelectedIndex,
- bool forceAnchorIndex)
- {
- if (SingleSelect && start != end)
- {
- throw new InvalidOperationException("Cannot select range with single selection.");
- }
- var range = CoerceRange(start, end);
- if (range.Begin == -1)
- {
- return;
- }
- using var update = BatchUpdate();
- var o = update.Operation;
- var selected = new List<IndexRange>();
- if (RangesEnabled)
- {
- o.SelectedRanges ??= new List<IndexRange>();
- IndexRange.Remove(o.DeselectedRanges, range);
- IndexRange.Add(o.SelectedRanges, range);
- IndexRange.Remove(o.SelectedRanges, Ranges);
- if (o.SelectedIndex == -1 || forceSelectedIndex)
- {
- o.SelectedIndex = range.Begin;
- }
- if (o.AnchorIndex == -1 || forceAnchorIndex)
- {
- o.AnchorIndex = range.Begin;
- }
- }
- else
- {
- o.SelectedIndex = o.AnchorIndex = start;
- }
- _initSelectedItems = null;
- }
- [return: MaybeNull]
- private T GetItemAt(int index)
- {
- if (ItemsView is null || index < 0 || index >= ItemsView.Count)
- {
- return default;
- }
- return ItemsView[index];
- }
- private int CoerceIndex(int index)
- {
- index = Math.Max(index, -1);
- if (ItemsView is object && index >= ItemsView.Count)
- {
- index = -1;
- }
- return index;
- }
- private IndexRange CoerceRange(int start, int end)
- {
- var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue;
- if (start > max || (start < 0 && end < 0))
- {
- return new IndexRange(-1);
- }
- start = Math.Max(start, 0);
- end = Math.Min(end, max);
- return new IndexRange(start, end);
- }
- private void TrimInvalidSelections(Operation operation)
- {
- if (ItemsView is null)
- {
- return;
- }
- var max = ItemsView.Count - 1;
- if (operation.SelectedIndex > max)
- {
- operation.SelectedIndex = GetFirstSelectedIndexFromRanges();
- }
- if (operation.AnchorIndex > max)
- {
- operation.AnchorIndex = GetFirstSelectedIndexFromRanges();
- }
- if (RangesEnabled && Ranges.Count > 0)
- {
- var selected = Ranges.ToList();
-
- if (max < 0)
- {
- operation.DeselectedRanges = selected;
- }
- else
- {
- var valid = new IndexRange(0, max);
- var removed = new List<IndexRange>();
- IndexRange.Intersect(selected, valid, removed);
- operation.DeselectedRanges = removed;
- }
- }
- }
- private void CommitOperation(Operation operation, bool raisePropertyChanged = true)
- {
- try
- {
- var oldAnchorIndex = _anchorIndex;
- var oldSelectedIndex = _selectedIndex;
- var indexesChanged = false;
- if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection)
- {
- operation.UpdateCount++;
- LostSelection?.Invoke(this, EventArgs.Empty);
- }
- _selectedIndex = operation.SelectedIndex;
- _anchorIndex = operation.AnchorIndex;
- if (operation.SelectedRanges is object)
- {
- indexesChanged |= CommitSelect(operation.SelectedRanges) > 0;
- }
- if (operation.DeselectedRanges is object)
- {
- indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0;
- }
- if (SelectionChanged is object || _untypedSelectionChanged is object)
- {
- IReadOnlyList<IndexRange>? deselected = operation.DeselectedRanges;
- IReadOnlyList<IndexRange>? selected = operation.SelectedRanges;
- if (SingleSelect && oldSelectedIndex != _selectedIndex)
- {
- if (oldSelectedIndex != -1)
- {
- deselected = new[] { new IndexRange(oldSelectedIndex) };
- }
- if (_selectedIndex != -1)
- {
- selected = new[] { new IndexRange(_selectedIndex) };
- }
- }
- if (deselected?.Count > 0 || selected?.Count > 0 || operation.DeselectedItems is object)
- {
- // If the operation was caused by Source being updated, then use a null source
- // so that the items will appear as nulls.
- var deselectedSource = operation.IsSourceUpdate ? null : ItemsView;
- // If the operation contains DeselectedItems then we're notifying a source
- // 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 ??
- SelectedItems<T>.Create(deselected, deselectedSource);
- var e = new SelectionModelSelectionChangedEventArgs<T>(
- SelectedIndexes<T>.Create(deselected),
- SelectedIndexes<T>.Create(selected),
- deselectedItems,
- SelectedItems<T>.Create(selected, ItemsView));
- SelectionChanged?.Invoke(this, e);
- _untypedSelectionChanged?.Invoke(this, e);
- }
- }
- if (raisePropertyChanged)
- {
- if (oldSelectedIndex != _selectedIndex)
- {
- indexesChanged = true;
- RaisePropertyChanged(nameof(SelectedIndex));
- }
- if (oldSelectedIndex != _selectedIndex || operation.IsSourceUpdate)
- {
- RaisePropertyChanged(nameof(SelectedItem));
- }
- if (oldAnchorIndex != _anchorIndex)
- {
- indexesChanged = true;
- RaisePropertyChanged(nameof(AnchorIndex));
- }
- if (indexesChanged)
- {
- RaisePropertyChanged(nameof(SelectedIndexes));
- }
- if (indexesChanged || operation.IsSourceUpdate)
- {
- RaisePropertyChanged(nameof(SelectedItems));
- }
- }
- }
- finally
- {
- _operation = null;
- }
- }
- public struct BatchUpdateOperation : IDisposable
- {
- private readonly SelectionModel<T> _owner;
- private bool _isDisposed;
- public BatchUpdateOperation(SelectionModel<T> owner)
- {
- _owner = owner;
- _isDisposed = false;
- owner.BeginBatchUpdate();
- }
- internal Operation Operation => _owner._operation!;
- public void Dispose()
- {
- if (!_isDisposed)
- {
- _owner?.EndBatchUpdate();
- _isDisposed = true;
- }
- }
- }
- internal class Operation
- {
- public Operation(SelectionModel<T> owner)
- {
- AnchorIndex = owner.AnchorIndex;
- SelectedIndex = owner.SelectedIndex;
- }
- public int UpdateCount { get; set; }
- public bool IsSourceUpdate { get; set; }
- public bool SkipLostSelection { get; set; }
- public int AnchorIndex { get; set; }
- public int SelectedIndex { get; set; }
- public List<IndexRange>? SelectedRanges { get; set; }
- public List<IndexRange>? DeselectedRanges { get; set; }
- public IReadOnlyList<T>? DeselectedItems { get; set; }
- }
- }
- }
|