diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index bbef48050e..d7d04c7971 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs
index e74d4cbcc8..f0241cad48 100644
--- a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs
+++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs
@@ -8,6 +8,7 @@ using System.Threading;
using ReactiveUI;
using Avalonia.Controls;
using Avalonia.Metadata;
+using Avalonia.Controls.Selection;
namespace BindingDemo.ViewModels
{
@@ -29,7 +30,7 @@ namespace BindingDemo.ViewModels
Detail = "Item " + x + " details",
}));
- Selection = new SelectionModel();
+ Selection = new SelectionModel { SingleSelect = false };
ShuffleItems = ReactiveCommand.Create(() =>
{
@@ -58,7 +59,7 @@ namespace BindingDemo.ViewModels
}
public ObservableCollection Items { get; }
- public SelectionModel Selection { get; }
+ public SelectionModel Selection { get; }
public ReactiveCommand ShuffleItems { get; }
public string BooleanString
diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml
index 789b45e62c..6d99132680 100644
--- a/samples/ControlCatalog/Pages/TreeViewPage.xaml
+++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml
@@ -10,7 +10,7 @@
HorizontalAlignment="Center"
Spacing="16">
-
+
diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
index 6bdb5c0103..d088576998 100644
--- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
@@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
+using Avalonia.Controls.Selection;
using ReactiveUI;
namespace ControlCatalog.ViewModels
@@ -15,16 +16,16 @@ namespace ControlCatalog.ViewModels
public ListBoxPageViewModel()
{
Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
- Selection = new SelectionModel();
+ Selection = new SelectionModel();
Selection.Select(1);
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() =>
{
- while (Selection.SelectedItems.Count > 0)
+ while (Selection.Count > 0)
{
- Items.Remove((string)Selection.SelectedItems.First());
+ Items.Remove(Selection.SelectedItems.First());
}
});
@@ -32,9 +33,9 @@ namespace ControlCatalog.ViewModels
{
var random = new Random();
- using (Selection.Update())
+ using (Selection.BatchUpdate())
{
- Selection.ClearSelection();
+ Selection.Clear();
Selection.Select(random.Next(Items.Count - 1));
}
});
@@ -42,7 +43,7 @@ namespace ControlCatalog.ViewModels
public ObservableCollection Items { get; }
- public SelectionModel Selection { get; }
+ public SelectionModel Selection { get; }
public ReactiveCommand AddItemCommand { get; }
@@ -55,7 +56,7 @@ namespace ControlCatalog.ViewModels
get => _selectionMode;
set
{
- Selection.ClearSelection();
+ Selection.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
index 5bc23e2fe5..210e281ed6 100644
--- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
@@ -18,8 +17,7 @@ namespace ControlCatalog.ViewModels
_root = new Node();
Items = _root.Children;
- Selection = new SelectionModel();
- Selection.SelectionChanged += SelectionChanged;
+ SelectedItems = new ObservableCollection();
AddItemCommand = ReactiveCommand.Create(AddItem);
RemoveItemCommand = ReactiveCommand.Create(RemoveItem);
@@ -27,7 +25,7 @@ namespace ControlCatalog.ViewModels
}
public ObservableCollection Items { get; }
- public SelectionModel Selection { get; }
+ public ObservableCollection SelectedItems { get; }
public ReactiveCommand AddItemCommand { get; }
public ReactiveCommand RemoveItemCommand { get; }
public ReactiveCommand SelectRandomItemCommand { get; }
@@ -37,24 +35,24 @@ namespace ControlCatalog.ViewModels
get => _selectionMode;
set
{
- Selection.ClearSelection();
+ SelectedItems.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
private void AddItem()
{
- var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root;
+ var parentItem = SelectedItems.Count > 0 ? (Node)SelectedItems[0] : _root;
parentItem.AddItem();
}
private void RemoveItem()
{
- while (Selection.SelectedItems.Count > 0)
+ while (SelectedItems.Count > 0)
{
- Node lastItem = (Node)Selection.SelectedItems[0];
+ Node lastItem = (Node)SelectedItems[0];
RecursiveRemove(Items, lastItem);
- Selection.DeselectAt(Selection.SelectedIndices[0]);
+ SelectedItems.RemoveAt(0);
}
bool RecursiveRemove(ObservableCollection items, Node selectedItem)
@@ -80,16 +78,16 @@ namespace ControlCatalog.ViewModels
{
var random = new Random();
var depth = random.Next(4);
- var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10));
- var path = new IndexPath(indexes);
- Selection.SelectedIndex = path;
- }
+ var indexes = Enumerable.Range(0, depth).Select(x => random.Next(10));
+ var node = _root;
- private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
- {
- var selected = string.Join(",", e.SelectedIndices);
- var deselected = string.Join(",", e.DeselectedIndices);
- System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'");
+ foreach (var i in indexes)
+ {
+ node = node.Children[i];
+ }
+
+ SelectedItems.Clear();
+ SelectedItems.Add(node);
}
public class Node
diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
index 3a6ed88fcd..852c01399f 100644
--- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
+++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
@@ -7,6 +7,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ReactiveUI;
using Avalonia.Layout;
+using Avalonia.Controls.Selection;
namespace VirtualizationDemo.ViewModels
{
@@ -48,7 +49,7 @@ namespace VirtualizationDemo.ViewModels
set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
}
- public SelectionModel Selection { get; } = new SelectionModel();
+ public SelectionModel Selection { get; } = new SelectionModel();
public AvaloniaList Items
{
@@ -137,9 +138,9 @@ namespace VirtualizationDemo.ViewModels
{
var index = Items.Count;
- if (Selection.SelectedIndices.Count > 0)
+ if (Selection.SelectedItems.Count > 0)
{
- index = Selection.SelectedIndex.GetAt(0);
+ index = Selection.SelectedIndex;
}
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
@@ -149,7 +150,7 @@ namespace VirtualizationDemo.ViewModels
{
if (Selection.SelectedItems.Count > 0)
{
- Items.RemoveAll(Selection.SelectedItems.Cast().ToList());
+ Items.RemoveAll(Selection.SelectedItems.ToList());
}
}
@@ -163,7 +164,7 @@ namespace VirtualizationDemo.ViewModels
private void SelectItem(int index)
{
- Selection.SelectedIndex = new IndexPath(index);
+ Selection.SelectedIndex = index;
}
}
}
diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt
new file mode 100644
index 0000000000..11708b360f
--- /dev/null
+++ b/src/Avalonia.Controls/ApiCompatBaseline.txt
@@ -0,0 +1,18 @@
+Compat issues with assembly Avalonia.Controls:
+TypesMustExist : Type 'Avalonia.Controls.IndexPath' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.ISelectedItemInfo' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.ISelectionModel' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.ListBox.SelectionProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.ListBox.Selection.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Controls.ListBox.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.SelectionModel' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.SelectionModelChildrenRequestedEventArgs' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.SelectionModelSelectionChangedEventArgs' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.TreeView.SelectionProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
+Total Issues: 16
diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 480dcfcb85..7f1f4bc8f3 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -2,6 +2,9 @@
netstandard2.0
+
+
+
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index 9bb4cc4816..c4df5c1815 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -279,6 +279,11 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
}
+ if (PlacementTarget is null && _popup.PlacementTarget != control)
+ {
+ _popup.PlacementTarget = control;
+ }
+
_popup.Child = this;
IsOpen = true;
_popup.IsOpen = true;
diff --git a/src/Avalonia.Controls/ISelectionModel.cs b/src/Avalonia.Controls/ISelectionModel.cs
deleted file mode 100644
index 6570921c03..0000000000
--- a/src/Avalonia.Controls/ISelectionModel.cs
+++ /dev/null
@@ -1,249 +0,0 @@
-// 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;
-
-namespace Avalonia.Controls
-{
- ///
- /// Holds the selected items for a control.
- ///
- public interface ISelectionModel : INotifyPropertyChanged
- {
- ///
- /// Gets or sets the anchor index.
- ///
- IndexPath AnchorIndex { get; set; }
-
- ///
- /// Gets or set the index of the first selected item.
- ///
- IndexPath SelectedIndex { get; set; }
-
- ///
- /// Gets or set the indexes of the selected items.
- ///
- IReadOnlyList SelectedIndices { get; }
-
- ///
- /// Gets the first selected item.
- ///
- object SelectedItem { get; }
-
- ///
- /// Gets the selected items.
- ///
- IReadOnlyList SelectedItems { get; }
-
- ///
- /// Gets a value indicating whether the model represents a single or multiple selection.
- ///
- bool SingleSelect { get; set; }
-
- ///
- /// Gets a value indicating whether to always keep an item selected where possible.
- ///
- bool AutoSelect { get; set; }
-
- ///
- /// Gets or sets the collection that contains the items that can be selected.
- ///
- object Source { get; set; }
-
- ///
- /// Raised when the children of a selection are required.
- ///
- event EventHandler ChildrenRequested;
-
- ///
- /// Raised when the selection has changed.
- ///
- event EventHandler SelectionChanged;
-
- ///
- /// Clears the selection.
- ///
- void ClearSelection();
-
- ///
- /// Deselects an item.
- ///
- /// The index of the item.
- void Deselect(int index);
-
- ///
- /// Deselects an item.
- ///
- /// The index of the item group.
- /// The index of the item in the group.
- void Deselect(int groupIndex, int itemIndex);
-
- ///
- /// Deselects an item.
- ///
- /// The index of the item.
- void DeselectAt(IndexPath index);
-
- ///
- /// Deselects a range of items.
- ///
- /// The start index of the range.
- /// The end index of the range.
- void DeselectRange(IndexPath start, IndexPath end);
-
- ///
- /// Deselects a range of items, starting at .
- ///
- /// The end index of the range.
- void DeselectRangeFromAnchor(int index);
-
- ///
- /// Deselects a range of items, starting at .
- ///
- ///
- /// The index of the item group that represents the end of the selection.
- ///
- ///
- /// The index of the item in the group that represents the end of the selection.
- ///
- void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex);
-
- ///
- /// Deselects a range of items, starting at .
- ///
- /// The end index of the range.
- void DeselectRangeFromAnchorTo(IndexPath index);
-
- ///
- /// Disposes the object and clears the selection.
- ///
- void Dispose();
-
- ///
- /// Checks whether an item is selected.
- ///
- /// The index of the item
- bool IsSelected(int index);
-
- ///
- /// Checks whether an item is selected.
- ///
- /// The index of the item group.
- /// The index of the item in the group.
- bool IsSelected(int groupIndex, int itemIndex);
-
- ///
- /// Checks whether an item is selected.
- ///
- /// The index of the item
- public bool IsSelectedAt(IndexPath index);
-
- ///
- /// Checks whether an item or its descendents are selected.
- ///
- /// The index of the item
- ///
- /// True if the item and all its descendents are selected, false if the item and all its
- /// descendents are deselected, or null if a combination of selected and deselected.
- ///
- bool? IsSelectedWithPartial(int index);
-
- ///
- /// Checks whether an item or its descendents are selected.
- ///
- /// The index of the item group.
- /// The index of the item in the group.
- ///
- /// True if the item and all its descendents are selected, false if the item and all its
- /// descendents are deselected, or null if a combination of selected and deselected.
- ///
- bool? IsSelectedWithPartial(int groupIndex, int itemIndex);
-
- ///
- /// Checks whether an item or its descendents are selected.
- ///
- /// The index of the item
- ///
- /// True if the item and all its descendents are selected, false if the item and all its
- /// descendents are deselected, or null if a combination of selected and deselected.
- ///
- bool? IsSelectedWithPartialAt(IndexPath index);
-
- ///
- /// Selects an item.
- ///
- /// The index of the item
- void Select(int index);
-
- ///
- /// Selects an item.
- ///
- /// The index of the item group.
- /// The index of the item in the group.
- void Select(int groupIndex, int itemIndex);
-
- ///
- /// Selects an item.
- ///
- /// The index of the item
- void SelectAt(IndexPath index);
-
- ///
- /// Selects all items.
- ///
- void SelectAll();
-
- ///
- /// Selects a range of items.
- ///
- /// The start index of the range.
- /// The end index of the range.
- void SelectRange(IndexPath start, IndexPath end);
-
- ///
- /// Selects a range of items, starting at .
- ///
- /// The end index of the range.
- void SelectRangeFromAnchor(int index);
-
- ///
- /// Selects a range of items, starting at .
- ///
- ///
- /// The index of the item group that represents the end of the selection.
- ///
- ///
- /// The index of the item in the group that represents the end of the selection.
- ///
- void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex);
-
- ///
- /// Selects a range of items, starting at .
- ///
- /// The end index of the range.
- void SelectRangeFromAnchorTo(IndexPath index);
-
- ///
- /// Sets the .
- ///
- /// The anchor index.
- void SetAnchorIndex(int index);
-
- ///
- /// Sets the .
- ///
- /// The index of the item group.
- /// The index of the item in the group.
- void SetAnchorIndex(int groupIndex, int index);
-
- ///
- /// Begins a batch update of the selection.
- ///
- /// An that finishes the batch update.
- IDisposable Update();
- }
-}
diff --git a/src/Avalonia.Controls/IndexPath.cs b/src/Avalonia.Controls/IndexPath.cs
deleted file mode 100644
index 73b75bc23d..0000000000
--- a/src/Avalonia.Controls/IndexPath.cs
+++ /dev/null
@@ -1,200 +0,0 @@
-// 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;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
- public readonly struct IndexPath : IComparable, IEquatable
- {
- public static readonly IndexPath Unselected = default;
-
- private readonly int _index;
- private readonly int[]? _path;
-
- public IndexPath(int index)
- {
- _index = index + 1;
- _path = null;
- }
-
- public IndexPath(int groupIndex, int itemIndex)
- {
- _index = 0;
- _path = new[] { groupIndex, itemIndex };
- }
-
- public IndexPath(IEnumerable? indices)
- {
- if (indices != null)
- {
- _index = 0;
- _path = indices.ToArray();
- }
- else
- {
- _index = 0;
- _path = null;
- }
- }
-
- private IndexPath(int[] basePath, int index)
- {
- basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
-
- _index = 0;
- _path = new int[basePath.Length + 1];
- Array.Copy(basePath, _path, basePath.Length);
- _path[basePath.Length] = index;
- }
-
- public int GetSize() => _path?.Length ?? (_index == 0 ? 0 : 1);
-
- public int GetAt(int index)
- {
- if (index >= GetSize())
- {
- throw new IndexOutOfRangeException();
- }
-
- return _path?[index] ?? (_index - 1);
- }
-
- public int CompareTo(IndexPath other)
- {
- var rhsPath = other;
- int compareResult = 0;
- int lhsCount = GetSize();
- int rhsCount = rhsPath.GetSize();
-
- 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 (GetAt(i) < rhsPath.GetAt(i))
- {
- compareResult = -1;
- break;
- }
- else if (GetAt(i) > rhsPath.GetAt(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)
- {
- if (_path != null)
- {
- return new IndexPath(_path, childIndex);
- }
- else if (_index != 0)
- {
- return new IndexPath(_index - 1, childIndex);
- }
- else
- {
- return new IndexPath(childIndex);
- }
- }
-
- public bool IsAncestorOf(in IndexPath other)
- {
- if (other.GetSize() <= GetSize())
- {
- return false;
- }
-
- var size = GetSize();
-
- for (int i = 0; i < size; i++)
- {
- if (GetAt(i) != other.GetAt(i))
- {
- return false;
- }
- }
-
- return true;
- }
-
- public override string ToString()
- {
- if (_path != null)
- {
- return "R" + string.Join(".", _path);
- }
- else if (_index != 0)
- {
- return "R" + (_index - 1);
- }
- else
- {
- return "R";
- }
- }
-
- 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 indices) => new IndexPath(indices);
-
- public override bool Equals(object obj) => obj is IndexPath other && Equals(other);
-
- public bool Equals(IndexPath other) => CompareTo(other) == 0;
-
- public override int GetHashCode()
- {
- var hashCode = -504981047;
-
- if (_path != null)
- {
- foreach (var i in _path)
- {
- hashCode = hashCode * -1521134295 + i.GetHashCode();
- }
- }
- else
- {
- hashCode = hashCode * -1521134295 + _index.GetHashCode();
- }
-
- return hashCode;
- }
-
- public static bool operator <(IndexPath x, IndexPath y) { return x.CompareTo(y) < 0; }
- public static bool operator >(IndexPath x, IndexPath y) { return x.CompareTo(y) > 0; }
- public static bool operator <=(IndexPath x, IndexPath y) { return x.CompareTo(y) <= 0; }
- public static bool operator >=(IndexPath x, IndexPath y) { return x.CompareTo(y) >= 0; }
- public static bool operator ==(IndexPath x, IndexPath y) { return x.CompareTo(y) == 0; }
- public static bool operator !=(IndexPath x, IndexPath y) { return x.CompareTo(y) != 0; }
- public static bool operator ==(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) == 0; }
- public static bool operator !=(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) != 0; }
- }
-}
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 1aa7945901..a3dfe33641 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Controls
///
/// Displays a collection of items.
///
- public class ItemsControl : TemplatedControl, IItemsPresenterHost
+ public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
{
///
/// The default value for the property.
@@ -53,7 +53,6 @@ namespace Avalonia.Controls
private IEnumerable _items = new AvaloniaList();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
- private IDisposable _itemsCollectionChangedSubscription;
///
/// Initializes static members of the class.
@@ -150,6 +149,19 @@ namespace Avalonia.Controls
ItemContainerGenerator.Clear();
}
+ void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+ {
+ }
+
+ void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+ {
+ }
+
+ void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+ {
+ ItemsCollectionChanged(sender, e);
+ }
+
///
/// Gets the item at the specified index in a collection.
///
@@ -315,12 +327,14 @@ namespace Avalonia.Controls
/// The event args.
protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
- _itemsCollectionChangedSubscription?.Dispose();
- _itemsCollectionChangedSubscription = null;
-
var oldValue = e.OldValue as IEnumerable;
var newValue = e.NewValue as IEnumerable;
+ if (oldValue is INotifyCollectionChanged incc)
+ {
+ CollectionChangedEventManager.Instance.RemoveListener(incc, this);
+ }
+
UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
@@ -418,11 +432,9 @@ namespace Avalonia.Controls
PseudoClasses.Set(":empty", items == null || items.Count() == 0);
PseudoClasses.Set(":singleitem", items != null && items.Count() == 1);
- var incc = items as INotifyCollectionChanged;
-
- if (incc != null)
+ if (items is INotifyCollectionChanged incc)
{
- _itemsCollectionChangedSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
+ CollectionChangedEventManager.Instance.AddListener(incc, this);
}
}
diff --git a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs
similarity index 52%
rename from src/Avalonia.Controls/Repeater/ItemsSourceView.cs
rename to src/Avalonia.Controls/ItemsSourceView.cs
index def9301e2d..b2663f3213 100644
--- a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs
+++ b/src/Avalonia.Controls/ItemsSourceView.cs
@@ -7,7 +7,11 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using Avalonia.Controls.Utils;
+
+#nullable enable
namespace Avalonia.Controls
{
@@ -23,8 +27,13 @@ namespace Avalonia.Controls
///
public class ItemsSourceView : INotifyCollectionChanged, IDisposable
{
- private readonly IList _inner;
- private INotifyCollectionChanged _notifyCollectionChanged;
+ ///
+ /// Gets an empty
+ ///
+ public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty());
+
+ private protected readonly IList _inner;
+ private INotifyCollectionChanged? _notifyCollectionChanged;
///
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
@@ -32,7 +41,7 @@ namespace Avalonia.Controls
/// The data source.
public ItemsSourceView(IEnumerable source)
{
- Contract.Requires(source != null);
+ source = source ?? throw new ArgumentNullException(nameof(source));
if (source is IList list)
{
@@ -63,10 +72,17 @@ namespace Avalonia.Controls
///
public bool HasKeyIndexMapping => false;
+ ///
+ /// Retrieves the item at the specified index.
+ ///
+ /// The index.
+ /// The item.
+ public object? this[int index] => GetAt(index);
+
///
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
///
- public event NotifyCollectionChangedEventHandler CollectionChanged;
+ public event NotifyCollectionChangedEventHandler? CollectionChanged;
///
public void Dispose()
@@ -81,10 +97,26 @@ namespace Avalonia.Controls
/// Retrieves the item at the specified index.
///
/// The index.
- /// the item.
- public object GetAt(int index) => _inner[index];
+ /// The item.
+ public object? GetAt(int index) => _inner[index];
- public int IndexOf(object item) => _inner.IndexOf(item);
+ public int IndexOf(object? item) => _inner.IndexOf(item);
+
+ public static ItemsSourceView GetOrCreate(IEnumerable? items)
+ {
+ if (items is ItemsSourceView isv)
+ {
+ return isv;
+ }
+ else if (items is null)
+ {
+ return Empty;
+ }
+ else
+ {
+ return new ItemsSourceView(items);
+ }
+ }
///
/// Retrieves the index of the item that has the specified unique identifier (key).
@@ -112,6 +144,22 @@ namespace Avalonia.Controls
throw new NotImplementedException();
}
+ internal void AddListener(ICollectionChangedListener listener)
+ {
+ if (_inner is INotifyCollectionChanged incc)
+ {
+ CollectionChangedEventManager.Instance.AddListener(incc, listener);
+ }
+ }
+
+ internal void RemoveListener(ICollectionChangedListener listener)
+ {
+ if (_inner is INotifyCollectionChanged incc)
+ {
+ CollectionChangedEventManager.Instance.RemoveListener(incc, listener);
+ }
+ }
+
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
@@ -131,4 +179,62 @@ namespace Avalonia.Controls
OnItemsSourceChanged(e);
}
}
+
+ public class ItemsSourceView : ItemsSourceView, IReadOnlyList
+ {
+ ///
+ /// Gets an empty
+ ///
+ public new static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty());
+
+ ///
+ /// Initializes a new instance of the ItemsSourceView class for the specified data source.
+ ///
+ /// The data source.
+ public ItemsSourceView(IEnumerable source)
+ : base(source)
+ {
+ }
+
+ private ItemsSourceView(IEnumerable source)
+ : base(source)
+ {
+ }
+
+ ///
+ /// Retrieves the item at the specified index.
+ ///
+ /// The index.
+ /// The item.
+#pragma warning disable CS8603
+ public new T this[int index] => GetAt(index);
+#pragma warning restore CS8603
+
+ ///
+ /// Retrieves the item at the specified index.
+ ///
+ /// The index.
+ /// The item.
+ [return: MaybeNull]
+ public new T GetAt(int index) => (T)_inner[index];
+
+ public IEnumerator GetEnumerator() => _inner.Cast().GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
+
+ public static new ItemsSourceView GetOrCreate(IEnumerable? items)
+ {
+ if (items is ItemsSourceView isv)
+ {
+ return isv;
+ }
+ else if (items is null)
+ {
+ return Empty;
+ }
+ else
+ {
+ return new ItemsSourceView(items);
+ }
+ }
+ }
}
diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs
index a085bfb6bc..f7e86d697a 100644
--- a/src/Avalonia.Controls/ListBox.cs
+++ b/src/Avalonia.Controls/ListBox.cs
@@ -2,6 +2,7 @@ using System.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.VisualTree;
@@ -76,9 +77,7 @@ namespace Avalonia.Controls
set => base.SelectedItems = value;
}
- ///
- /// Gets or sets a model holding the current selection.
- ///
+ ///
public new ISelectionModel Selection
{
get => base.Selection;
@@ -115,7 +114,7 @@ namespace Avalonia.Controls
///
/// Deselects all items in the .
///
- public void UnselectAll() => Selection.ClearSelection();
+ public void UnselectAll() => Selection.Clear();
///
protected override IItemContainerGenerator CreateItemContainerGenerator()
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 59b7777b1b..5f8c5da2f8 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -3,9 +3,9 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
-using System.Diagnostics;
using System.Linq;
using Avalonia.Controls.Generators;
+using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
@@ -13,6 +13,8 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia.Controls.Primitives
{
///
@@ -24,8 +26,8 @@ namespace Avalonia.Controls.Primitives
/// that maintain a selection (single or multiple). By default only its
/// and properties are visible; the
/// current multiple and together with the
- /// and properties are protected, however a derived class can
- /// expose these if it wishes to support multiple selection.
+ /// properties are protected, however a derived class can expose
+ /// these if it wishes to support multiple selection.
///
///
/// maintains a selection respecting the current
@@ -58,8 +60,8 @@ namespace Avalonia.Controls.Primitives
///
/// Defines the property.
///
- public static readonly DirectProperty SelectedItemProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly DirectProperty SelectedItemProperty =
+ AvaloniaProperty.RegisterDirect(
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v,
@@ -77,7 +79,7 @@ namespace Avalonia.Controls.Primitives
///
/// Defines the property.
///
- public static readonly DirectProperty SelectionProperty =
+ protected static readonly DirectProperty SelectionProperty =
AvaloniaProperty.RegisterDirect(
nameof(Selection),
o => o.Selection,
@@ -109,21 +111,12 @@ namespace Avalonia.Controls.Primitives
RoutingStrategies.Bubble);
private static readonly IList Empty = Array.Empty();
- private readonly SelectedItemsSync _selectedItems;
- private ISelectionModel _selection;
- private int _selectedIndex = -1;
- private object _selectedItem;
+ private SelectedItemsSync? _selectedItemsSync;
+ private ISelectionModel? _selection;
+ private int _oldSelectedIndex;
+ private object? _oldSelectedItem;
+ private int _initializing;
private bool _ignoreContainerSelectionChanged;
- private int _updateCount;
- private int _updateSelectedIndex;
- private object _updateSelectedItem;
-
- public SelectingItemsControl()
- {
- // Setting Selection to null causes a default SelectionModel to be created.
- Selection = null;
- _selectedItems = new SelectedItemsSync(Selection);
- }
///
/// Initializes static members of the class.
@@ -156,42 +149,17 @@ namespace Avalonia.Controls.Primitives
///
public int SelectedIndex
{
- get => Selection.SelectedIndex != default ? Selection.SelectedIndex.GetAt(0) : -1;
- set
- {
- if (_updateCount == 0)
- {
- if (value != SelectedIndex)
- {
- Selection.SelectedIndex = new IndexPath(value);
- }
- }
- else
- {
- _updateSelectedIndex = value;
- _updateSelectedItem = null;
- }
- }
+ get => Selection.SelectedIndex;
+ set => Selection.SelectedIndex = value;
}
///
/// Gets or sets the selected item.
///
- public object SelectedItem
+ public object? SelectedItem
{
get => Selection.SelectedItem;
- set
- {
- if (_updateCount == 0)
- {
- SelectedIndex = IndexOf(Items, value);
- }
- else
- {
- _updateSelectedItem = value;
- _updateSelectedIndex = int.MinValue;
- }
- }
+ set => Selection.SelectedItem = value;
}
///
@@ -199,46 +167,40 @@ namespace Avalonia.Controls.Primitives
///
protected IList SelectedItems
{
- get => _selectedItems.GetOrCreateItems();
- set => _selectedItems.SetItems(value);
+ get => SelectedItemsSync.SelectedItems;
+ set => SelectedItemsSync.SelectedItems = value;
}
///
- /// Gets or sets a model holding the current selection.
+ /// Gets or sets the model that holds the current selection.
///
- protected ISelectionModel Selection
+ protected ISelectionModel Selection
{
- get => _selection;
- set
+ get
{
- value ??= new SelectionModel
+ if (_selection is null)
{
- SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
- AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
- RetainSelectionOnReset = true,
- };
+ _selection = CreateDefaultSelectionModel();
+ InitializeSelectionModel(_selection);
+ }
+
+ return _selection;
+ }
+ set
+ {
+ value ??= CreateDefaultSelectionModel();
if (_selection != value)
{
- if (value == null)
- {
- throw new ArgumentNullException(nameof(value), "Cannot set Selection to null.");
- }
- else if (value.Source != null && value.Source != Items)
- {
- throw new ArgumentException("Selection has invalid Source.");
- }
-
- List oldSelection = null;
-
- if (_selection != null)
+ if (value.Source != null && value.Source != Items)
{
- oldSelection = Selection.SelectedItems.ToList();
- _selection.PropertyChanged -= OnSelectionModelPropertyChanged;
- _selection.SelectionChanged -= OnSelectionModelSelectionChanged;
- MarkContainersUnselected();
+ throw new ArgumentException(
+ "The supplied ISelectionModel already has an assigned Source but this " +
+ "collection is different to the Items on the control.");
}
+ var oldSelection = _selection?.SelectedItems.ToList();
+ DeinitializeSelectionModel(_selection);
_selection = value;
if (oldSelection?.Count > 0)
@@ -249,55 +211,7 @@ namespace Avalonia.Controls.Primitives
Array.Empty()));
}
- if (_selection != null)
- {
- _selection.Source = Items;
- _selection.PropertyChanged += OnSelectionModelPropertyChanged;
- _selection.SelectionChanged += OnSelectionModelSelectionChanged;
-
- if (_selection.SingleSelect)
- {
- SelectionMode &= ~SelectionMode.Multiple;
- }
- else
- {
- SelectionMode |= SelectionMode.Multiple;
- }
-
- if (_selection.AutoSelect)
- {
- SelectionMode |= SelectionMode.AlwaysSelected;
- }
- else
- {
- SelectionMode &= ~SelectionMode.AlwaysSelected;
- }
-
- UpdateContainerSelection();
-
- var selectedIndex = SelectedIndex;
- var selectedItem = SelectedItem;
-
- if (_selectedIndex != selectedIndex)
- {
- RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, selectedIndex);
- _selectedIndex = selectedIndex;
- }
-
- if (_selectedItem != selectedItem)
- {
- RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem);
- _selectedItem = selectedItem;
- }
-
- if (selectedIndex != -1)
- {
- RaiseEvent(new SelectionChangedEventArgs(
- SelectionChangedEvent,
- Array.Empty(),
- Selection.SelectedItems.ToList()));
- }
- }
+ InitializeSelectionModel(_selection);
}
}
}
@@ -320,20 +234,20 @@ namespace Avalonia.Controls.Primitives
///
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
+ private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection);
+
///
public override void BeginInit()
{
base.BeginInit();
-
- InternalBeginInit();
+ ++_initializing;
}
///
public override void EndInit()
{
- InternalEndInit();
-
base.EndInit();
+ --_initializing;
}
///
@@ -353,7 +267,7 @@ namespace Avalonia.Controls.Primitives
///
/// The control that raised the event.
/// The container or null if the event did not originate in a container.
- protected IControl GetContainerFromEventSource(IInteractive eventSource)
+ protected IControl? GetContainerFromEventSource(IInteractive eventSource)
{
var parent = (IVisual)eventSource;
@@ -371,21 +285,14 @@ namespace Avalonia.Controls.Primitives
return null;
}
- ///
- protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
- {
- if (_updateCount == 0)
- {
- Selection.Source = e.NewValue;
- }
-
- base.ItemsChanged(e);
- }
-
- ///
protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
base.ItemsCollectionChanged(sender, e);
+
+ if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
+ {
+ SelectedIndex = 0;
+ }
}
///
@@ -400,9 +307,10 @@ namespace Avalonia.Controls.Primitives
Selection.Select(container.Index);
MarkContainerSelected(container.ContainerControl, true);
}
- else if (Selection.IsSelected(container.Index) == true)
+ else
{
- MarkContainerSelected(container.ContainerControl, true);
+ var selected = Selection.IsSelected(container.Index);
+ MarkContainerSelected(container.ContainerControl, selected);
}
}
}
@@ -433,7 +341,7 @@ namespace Avalonia.Controls.Primitives
{
if (i.ContainerControl != null && i.Item != null)
{
- bool selected = Selection.IsSelected(i.Index) == true;
+ bool selected = Selection.IsSelected(i.Index);
MarkContainerSelected(i.ContainerControl, selected);
}
}
@@ -443,27 +351,39 @@ namespace Avalonia.Controls.Primitives
protected override void OnDataContextBeginUpdate()
{
base.OnDataContextBeginUpdate();
+ ++_initializing;
- InternalBeginInit();
+ if (_selection is object)
+ {
+ _selection.Source = null;
+ }
}
///
protected override void OnDataContextEndUpdate()
{
base.OnDataContextEndUpdate();
+ --_initializing;
+
+ if (_selection is object && _initializing == 0)
+ {
+ _selection.Source = Items;
- InternalEndInit();
+ if (Items is null)
+ {
+ _selection.Clear();
+ _selectedItemsSync?.SelectedItems?.Clear();
+ }
+ }
}
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ protected override void OnInitialized()
{
- base.OnPropertyChanged(change);
+ base.OnInitialized();
- if (change.Property == SelectionModeProperty)
+ if (_selection is object)
{
- var mode = change.NewValue.GetValueOrDefault();
- Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
- Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
+ _selection.Source = Items;
}
}
@@ -487,6 +407,29 @@ namespace Avalonia.Controls.Primitives
}
}
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == ItemsProperty &&
+ _initializing == 0 &&
+ _selection is object)
+ {
+ var newValue = change.NewValue.GetValueOrDefault();
+ _selection.Source = newValue;
+
+ if (newValue is null)
+ {
+ _selection.Clear();
+ }
+ }
+ else if (change.Property == SelectionModeProperty && _selection is object)
+ {
+ var newValue = change.NewValue.GetValueOrDefault();
+ _selection.SingleSelect = !newValue.HasFlagCustom(SelectionMode.Multiple);
+ }
+ }
+
///
/// Moves the selection in the specified direction relative to the current selection.
///
@@ -506,7 +449,7 @@ namespace Avalonia.Controls.Primitives
/// The direction to move.
/// Whether to wrap when the selection reaches the first or last item.
/// True if the selection was moved; otherwise false.
- protected bool MoveSelection(IControl from, NavigationDirection direction, bool wrap)
+ protected bool MoveSelection(IControl? from, NavigationDirection direction, bool wrap)
{
if (Presenter?.Panel is INavigableContainer container &&
GetNextControl(container, direction, from, wrap) is IControl next)
@@ -538,71 +481,62 @@ namespace Avalonia.Controls.Primitives
bool toggleModifier = false,
bool rightButton = false)
{
- if (index != -1)
+ if (index < 0 || index >= ItemCount)
{
- if (select)
- {
- var mode = SelectionMode;
- var multi = (mode & SelectionMode.Multiple) != 0;
- var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
- var range = multi && rangeModifier;
-
- if (rightButton)
- {
- if (Selection.IsSelected(index) == false)
- {
- SelectedIndex = index;
- }
- }
- else if (range)
- {
- using var operation = Selection.Update();
- var anchor = Selection.AnchorIndex;
-
- if (anchor.GetSize() == 0)
- {
- anchor = new IndexPath(0);
- }
+ return;
+ }
- Selection.ClearSelection();
- Selection.AnchorIndex = anchor;
- Selection.SelectRangeFromAnchor(index);
- }
- else if (multi && toggle)
- {
- if (Selection.IsSelected(index) == true)
- {
- Selection.Deselect(index);
- }
- else
- {
- Selection.Select(index);
- }
- }
- else if (toggle)
- {
- SelectedIndex = (SelectedIndex == index) ? -1 : index;
- }
- else
- {
- using var operation = Selection.Update();
- Selection.ClearSelection();
- Selection.Select(index);
- }
+ var mode = SelectionMode;
+ var multi = (mode & SelectionMode.Multiple) != 0;
+ var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
+ var range = multi && rangeModifier;
- if (Presenter?.Panel != null)
- {
- var container = ItemContainerGenerator.ContainerFromIndex(index);
- KeyboardNavigation.SetTabOnceActiveElement(
- (InputElement)Presenter.Panel,
- container);
- }
+ if (!select)
+ {
+ Selection.Deselect(index);
+ }
+ else if (rightButton)
+ {
+ if (Selection.IsSelected(index) == false)
+ {
+ SelectedIndex = index;
+ }
+ }
+ else if (range)
+ {
+ using var operation = Selection.BatchUpdate();
+ Selection.Clear();
+ Selection.SelectRange(Selection.AnchorIndex, index);
+ }
+ else if (multi && toggle)
+ {
+ if (Selection.IsSelected(index) == true)
+ {
+ Selection.Deselect(index);
}
else
{
- LostSelection();
+ Selection.Select(index);
}
}
+ else if (toggle)
+ {
+ SelectedIndex = (SelectedIndex == index) ? -1 : index;
+ }
+ else
+ {
+ using var operation = Selection.BatchUpdate();
+ Selection.Clear();
+ Selection.Select(index);
+ }
+
+ if (Presenter?.Panel != null)
+ {
+ var container = ItemContainerGenerator.ContainerFromIndex(index);
+ KeyboardNavigation.SetTabOnceActiveElement(
+ (InputElement)Presenter.Panel,
+ container);
+ }
}
///
@@ -660,23 +594,35 @@ namespace Avalonia.Controls.Primitives
}
///
- /// Called when is raised.
+ /// Called when is raised on
+ /// .
///
/// The sender.
/// The event args.
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
+ if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
{
- if (Selection.AnchorIndex.GetSize() > 0)
+ if (Selection.AnchorIndex > 0)
{
- ScrollIntoView(Selection.AnchorIndex.GetAt(0));
+ ScrollIntoView(Selection.AnchorIndex);
}
}
+ else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex))
+ {
+ RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex);
+ _oldSelectedIndex = SelectedIndex;
+ }
+ else if (e.PropertyName == nameof(ISelectionModel.SelectedItem))
+ {
+ RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem);
+ _oldSelectedItem = SelectedItem;
+ }
}
///
- /// Called when is raised.
+ /// Called when event is raised on
+ /// .
///
/// The sender.
/// The event args.
@@ -692,46 +638,40 @@ namespace Avalonia.Controls.Primitives
}
}
- if (e.SelectedIndices.Count > 0 || e.DeselectedIndices.Count > 0)
+ foreach (var i in e.SelectedIndexes)
{
- foreach (var i in e.SelectedIndices)
- {
- Mark(i.GetAt(0), true);
- }
-
- foreach (var i in e.DeselectedIndices)
- {
- Mark(i.GetAt(0), false);
- }
+ Mark(i, true);
}
- else if (e.DeselectedItems.Count > 0)
+
+ foreach (var i in e.DeselectedIndexes)
{
- // (De)selected indices being empty means that a selected item was removed from
- // the Items (it can't tell us the index of the item because the index is no longer
- // valid). In this case, we just update the selection state of all containers.
- UpdateContainerSelection();
+ Mark(i, false);
}
- var newSelectedIndex = SelectedIndex;
- var newSelectedItem = SelectedItem;
+ var route = BuildEventRoute(SelectionChangedEvent);
- if (newSelectedIndex != _selectedIndex)
+ if (route.HasHandlers)
{
- RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, newSelectedIndex);
- _selectedIndex = newSelectedIndex;
+ var ev = new SelectionChangedEventArgs(
+ SelectionChangedEvent,
+ e.DeselectedItems.ToList(),
+ e.SelectedItems.ToList());
+ RaiseEvent(ev);
}
+ }
- if (newSelectedItem != _selectedItem)
+ ///
+ /// Called when event is raised on
+ /// .
+ ///
+ /// The sender.
+ /// The event args.
+ private void OnSelectionModelLostSelection(object sender, EventArgs e)
+ {
+ if (AlwaysSelected && Items is object)
{
- RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem);
- _selectedItem = newSelectedItem;
+ SelectedIndex = 0;
}
-
- var ev = new SelectionChangedEventArgs(
- SelectionChangedEvent,
- e.DeselectedItems.ToList(),
- e.SelectedItems.ToList());
- RaiseEvent(ev);
}
///
@@ -760,23 +700,6 @@ namespace Avalonia.Controls.Primitives
}
}
- ///
- /// Called when the currently selected item is lost and the selection must be changed
- /// depending on the property.
- ///
- private void LostSelection()
- {
- var items = Items?.Cast();
- var index = -1;
-
- if (items != null && AlwaysSelected)
- {
- index = Math.Min(SelectedIndex, items.Count() - 1);
- }
-
- SelectedIndex = index;
- }
-
///
/// Sets a container's 'selected' class or .
///
@@ -819,16 +742,6 @@ namespace Avalonia.Controls.Primitives
}
}
- private void UpdateContainerSelection()
- {
- foreach (var container in ItemContainerGenerator.Containers)
- {
- MarkContainerSelected(
- container.ContainerControl,
- Selection.IsSelected(container.Index) != false);
- }
- }
-
///
/// Sets an item container's 'selected' class or .
///
@@ -844,52 +757,92 @@ namespace Avalonia.Controls.Primitives
}
}
- private void UpdateFinished()
+ ///
+ /// Sets an item container's 'selected' class or .
+ ///
+ /// The item.
+ /// Whether the item should be selected or deselected.
+ private int MarkItemSelected(object item, bool selected)
{
- Selection.Source = Items;
+ var index = IndexOf(Items, item);
- if (_updateSelectedItem != null)
+ if (index != -1)
{
- SelectedItem = _updateSelectedItem;
+ MarkItemSelected(index, selected);
}
- else
+
+ return index;
+ }
+
+ private void UpdateContainerSelection()
+ {
+ if (Presenter?.Panel is IPanel panel)
{
- if (ItemCount == 0 && SelectedIndex != -1)
+ foreach (var container in panel.Children)
{
- SelectedIndex = -1;
- }
- else
- {
- if (_updateSelectedIndex != int.MinValue)
- {
- SelectedIndex = _updateSelectedIndex;
- }
-
- if (AlwaysSelected && SelectedIndex == -1)
- {
- SelectedIndex = 0;
- }
+ MarkContainerSelected(
+ container,
+ Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container)));
}
}
}
- private void InternalBeginInit()
+ private ISelectionModel CreateDefaultSelectionModel()
+ {
+ return new SelectionModel
+ {
+ SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
+ };
+ }
+
+ private void InitializeSelectionModel(ISelectionModel model)
{
- if (_updateCount == 0)
+ if (_initializing == 0)
{
- _updateSelectedIndex = int.MinValue;
+ model.Source = Items;
}
- ++_updateCount;
+ model.PropertyChanged += OnSelectionModelPropertyChanged;
+ model.SelectionChanged += OnSelectionModelSelectionChanged;
+ model.LostSelection += OnSelectionModelLostSelection;
+
+ if (model.SingleSelect)
+ {
+ SelectionMode &= ~SelectionMode.Multiple;
+ }
+ else
+ {
+ SelectionMode |= SelectionMode.Multiple;
+ }
+
+ _oldSelectedIndex = model.SelectedIndex;
+ _oldSelectedItem = model.SelectedItem;
+
+ if (AlwaysSelected && model.Count == 0)
+ {
+ model.SelectedIndex = 0;
+ }
+
+ UpdateContainerSelection();
+
+ _selectedItemsSync ??= new SelectedItemsSync(model);
+ _selectedItemsSync.SelectionModel = model;
+
+ if (SelectedIndex != -1)
+ {
+ RaiseEvent(new SelectionChangedEventArgs(
+ SelectionChangedEvent,
+ Array.Empty(),
+ Selection.SelectedItems.ToList()));
+ }
}
- private void InternalEndInit()
+ private void DeinitializeSelectionModel(ISelectionModel? model)
{
- Debug.Assert(_updateCount > 0);
-
- if (--_updateCount == 0)
+ if (model is object)
{
- UpdateFinished();
+ model.PropertyChanged -= OnSelectionModelPropertyChanged;
+ model.SelectionChanged -= OnSelectionModelSelectionChanged;
}
}
}
diff --git a/src/Avalonia.Controls/SelectedItems.cs b/src/Avalonia.Controls/SelectedItems.cs
deleted file mode 100644
index a3acb48765..0000000000
--- a/src/Avalonia.Controls/SelectedItems.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// 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;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
- public interface ISelectedItemInfo
- {
- public IndexPath Path { get; }
- }
-
- internal class SelectedItems : IReadOnlyList
- where Tinfo : ISelectedItemInfo
- {
- private readonly List _infos;
- private readonly Func, int, TValue> _getAtImpl;
-
- public SelectedItems(
- List infos,
- int count,
- Func, int, TValue> getAtImpl)
- {
- _infos = infos;
- _getAtImpl = getAtImpl;
- Count = count;
- }
-
- public TValue this[int index] => _getAtImpl(_infos, index);
-
- public int Count { get; }
-
- public IEnumerator GetEnumerator()
- {
- for (var i = 0; i < Count; ++i)
- {
- yield return this[i];
- }
- }
-
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
- }
-}
diff --git a/src/Avalonia.Controls/Selection/ISelectionModel.cs b/src/Avalonia.Controls/Selection/ISelectionModel.cs
new file mode 100644
index 0000000000..3b8fd0c8b7
--- /dev/null
+++ b/src/Avalonia.Controls/Selection/ISelectionModel.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+ public interface ISelectionModel : INotifyPropertyChanged
+ {
+ IEnumerable? Source { get; set; }
+ bool SingleSelect { get; set; }
+ int SelectedIndex { get; set; }
+ IReadOnlyList SelectedIndexes { get; }
+ object? SelectedItem { get; set; }
+ IReadOnlyList SelectedItems { get; }
+ int AnchorIndex { get; set; }
+ int Count { get; }
+
+ public event EventHandler? IndexesChanged;
+ public event EventHandler? SelectionChanged;
+ public event EventHandler? LostSelection;
+ public event EventHandler? SourceReset;
+
+ public void BeginBatchUpdate();
+ public void EndBatchUpdate();
+ bool IsSelected(int index);
+ void Select(int index);
+ void Deselect(int index);
+ void SelectRange(int start, int end);
+ void DeselectRange(int start, int end);
+ void SelectAll();
+ void Clear();
+ }
+
+ public static class SelectionModelExtensions
+ {
+ public static IDisposable BatchUpdate(this ISelectionModel model)
+ {
+ return new BatchUpdateOperation(model);
+ }
+
+ public struct BatchUpdateOperation : IDisposable
+ {
+ private readonly ISelectionModel _owner;
+ private bool _isDisposed;
+
+ public BatchUpdateOperation(ISelectionModel owner)
+ {
+ _owner = owner;
+ _isDisposed = false;
+ owner.BeginBatchUpdate();
+ }
+
+ public void Dispose()
+ {
+ if (!_isDisposed)
+ {
+ _owner?.EndBatchUpdate();
+ _isDisposed = true;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/IndexRange.cs b/src/Avalonia.Controls/Selection/IndexRange.cs
similarity index 81%
rename from src/Avalonia.Controls/IndexRange.cs
rename to src/Avalonia.Controls/Selection/IndexRange.cs
index e45d013af4..fa7b44faea 100644
--- a/src/Avalonia.Controls/IndexRange.cs
+++ b/src/Avalonia.Controls/Selection/IndexRange.cs
@@ -8,12 +8,18 @@ using System.Collections.Generic;
#nullable enable
-namespace Avalonia.Controls
+namespace Avalonia.Controls.Selection
{
internal readonly struct IndexRange : IEquatable
{
private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue);
+ public IndexRange(int index)
+ {
+ Begin = index;
+ End = index;
+ }
+
public IndexRange(int begin, int end)
{
// Accept out of order begin/end pairs, just swap them.
@@ -87,6 +93,43 @@ namespace Avalonia.Controls
public static bool operator ==(IndexRange left, IndexRange right) => left.Equals(right);
public static bool operator !=(IndexRange left, IndexRange right) => !(left == right);
+ public static bool Contains(IReadOnlyList? ranges, int index)
+ {
+ if (ranges is null || index < 0)
+ {
+ return false;
+ }
+
+ foreach (var range in ranges)
+ {
+ if (range.Contains(index))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static int GetAt(IReadOnlyList ranges, int index)
+ {
+ var currentIndex = 0;
+
+ foreach (var range in ranges)
+ {
+ var currentCount = range.Count;
+
+ if (index >= currentIndex && index < currentIndex + currentCount)
+ {
+ return range.Begin + (index - currentIndex);
+ }
+
+ currentIndex += currentCount;
+ }
+
+ throw new IndexOutOfRangeException("The index was out of range.");
+ }
+
public static int Add(
IList ranges,
IndexRange range,
@@ -132,6 +175,21 @@ namespace Avalonia.Controls
return result;
}
+ public static int Add(
+ IList destination,
+ IReadOnlyList source,
+ IList? added = null)
+ {
+ var result = 0;
+
+ foreach (var range in source)
+ {
+ result += Add(destination, range, added);
+ }
+
+ return result;
+ }
+
public static int Intersect(
IList ranges,
IndexRange range,
@@ -180,10 +238,15 @@ namespace Avalonia.Controls
}
public static int Remove(
- IList ranges,
+ IList? ranges,
IndexRange range,
IList? removed = null)
{
+ if (ranges is null)
+ {
+ return 0;
+ }
+
var result = 0;
for (var i = 0; i < ranges.Count; ++i)
@@ -224,15 +287,16 @@ namespace Avalonia.Controls
return result;
}
- public static IEnumerable Subtract(
- IndexRange lhs,
- IEnumerable rhs)
+ public static int Remove(
+ IList destination,
+ IReadOnlyList source,
+ IList? added = null)
{
- var result = new List { lhs };
-
- foreach (var range in rhs)
+ var result = 0;
+
+ foreach (var range in source)
{
- Remove(result, range);
+ result += Remove(destination, range, added);
}
return result;
diff --git a/src/Avalonia.Controls/Selection/SelectedIndexes.cs b/src/Avalonia.Controls/Selection/SelectedIndexes.cs
new file mode 100644
index 0000000000..36df175ed2
--- /dev/null
+++ b/src/Avalonia.Controls/Selection/SelectedIndexes.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+ internal class SelectedIndexes : IReadOnlyList
+ {
+ private readonly SelectionModel? _owner;
+ private readonly IReadOnlyList? _ranges;
+
+ public SelectedIndexes(SelectionModel owner) => _owner = owner;
+ public SelectedIndexes(IReadOnlyList ranges) => _ranges = ranges;
+
+ public int this[int index]
+ {
+ get
+ {
+ if (index >= Count)
+ {
+ throw new IndexOutOfRangeException("The index was out of range.");
+ }
+
+ if (_owner?.SingleSelect == true)
+ {
+ return _owner.SelectedIndex;
+ }
+ else
+ {
+ return IndexRange.GetAt(Ranges!, index);
+ }
+ }
+ }
+
+ public int Count
+ {
+ get
+ {
+ if (_owner?.SingleSelect == true)
+ {
+ return _owner.SelectedIndex == -1 ? 0 : 1;
+ }
+ else
+ {
+ return IndexRange.GetCount(Ranges!);
+ }
+ }
+ }
+
+ private IReadOnlyList Ranges => _ranges ?? _owner!.Ranges!;
+
+ public IEnumerator GetEnumerator()
+ {
+ IEnumerator SingleSelect()
+ {
+ if (_owner.SelectedIndex >= 0)
+ {
+ yield return _owner.SelectedIndex;
+ }
+ }
+
+ if (_owner?.SingleSelect == true)
+ {
+ return SingleSelect();
+ }
+ else
+ {
+ return IndexRange.EnumerateIndices(Ranges).GetEnumerator();
+ }
+ }
+
+ public static SelectedIndexes? Create(IReadOnlyList? ranges)
+ {
+ return ranges is object ? new SelectedIndexes(ranges) : null;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/src/Avalonia.Controls/Selection/SelectedItems.cs b/src/Avalonia.Controls/Selection/SelectedItems.cs
new file mode 100644
index 0000000000..92781fd54a
--- /dev/null
+++ b/src/Avalonia.Controls/Selection/SelectedItems.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+ internal class SelectedItems : IReadOnlyList
+ {
+ private readonly SelectionModel? _owner;
+ private readonly ItemsSourceView? _items;
+ private readonly IReadOnlyList? _ranges;
+
+ public SelectedItems(SelectionModel owner) => _owner = owner;
+
+ public SelectedItems(IReadOnlyList ranges, ItemsSourceView? items)
+ {
+ _ranges = ranges ?? throw new ArgumentNullException(nameof(ranges));
+ _items = items;
+ }
+
+ [MaybeNull]
+ public T this[int index]
+ {
+#pragma warning disable CS8766
+ get
+#pragma warning restore CS8766
+ {
+ if (index >= Count)
+ {
+ throw new IndexOutOfRangeException("The index was out of range.");
+ }
+
+ if (_owner?.SingleSelect == true)
+ {
+ return _owner.SelectedItem;
+ }
+ else if (Items is object)
+ {
+ return Items[index];
+ }
+ else
+ {
+ return default;
+ }
+ }
+ }
+
+ public int Count
+ {
+ get
+ {
+ if (_owner?.SingleSelect == true)
+ {
+ return _owner.SelectedIndex == -1 ? 0 : 1;
+ }
+ else
+ {
+ return Ranges is object ? IndexRange.GetCount(Ranges) : 0;
+ }
+ }
+ }
+
+ private ItemsSourceView? Items => _items ?? _owner?.ItemsView;
+ private IReadOnlyList? Ranges => _ranges ?? _owner!.Ranges;
+
+ public IEnumerator GetEnumerator()
+ {
+ if (_owner?.SingleSelect == true)
+ {
+ if (_owner.SelectedIndex >= 0)
+ {
+#pragma warning disable CS8603
+ yield return _owner.SelectedItem;
+#pragma warning restore CS8603
+ }
+ }
+ else
+ {
+ var items = Items;
+
+ foreach (var range in Ranges!)
+ {
+ for (var i = range.Begin; i <= range.End; ++i)
+ {
+#pragma warning disable CS8603
+ yield return items is object ? items[i] : default;
+#pragma warning restore CS8603
+ }
+ }
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ public static SelectedItems? Create(
+ IReadOnlyList? ranges,
+ ItemsSourceView? items)
+ {
+ return ranges is object ? new SelectedItems(ranges, items) : null;
+ }
+
+ public class Untyped : IReadOnlyList
+ {
+ private readonly IReadOnlyList _source;
+ public Untyped(IReadOnlyList source) => _source = source;
+ public object? this[int index] => _source[index];
+ public int Count => _source.Count;
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ public IEnumerator GetEnumerator()
+ {
+ foreach (var i in _source)
+ {
+ yield return i;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs
new file mode 100644
index 0000000000..7ce2624d02
--- /dev/null
+++ b/src/Avalonia.Controls/Selection/SelectionModel.cs
@@ -0,0 +1,726 @@
+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 : SelectionNodeBase, ISelectionModel
+ {
+ private bool _singleSelect = true;
+ private int _anchorIndex = -1;
+ private int _selectedIndex = -1;
+ private Operation? _operation;
+ private SelectedIndexes? _selectedIndexes;
+ private SelectedItems? _selectedItems;
+ private SelectedItems.Untyped? _selectedItemsUntyped;
+ private EventHandler? _untypedSelectionChanged;
+ [AllowNull] private T _initSelectedItem = default;
+ private bool _hasInitSelectedItem;
+
+ public SelectionModel()
+ {
+ }
+
+ public SelectionModel(IEnumerable? source)
+ {
+ Source = source;
+ }
+
+ public new IEnumerable? Source
+ {
+ get => base.Source as IEnumerable;
+ 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 SelectedIndexes => _selectedIndexes ??= new SelectedIndexes(this);
+
+ [MaybeNull, AllowNull]
+ public T SelectedItem
+ {
+ get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem;
+ set
+ {
+ if (ItemsView is object)
+ {
+ SelectedIndex = ItemsView.IndexOf(value!);
+ }
+ else
+ {
+ Clear();
+ _initSelectedItem = value;
+ _hasInitSelectedItem = true;
+ }
+ }
+ }
+
+ public IReadOnlyList SelectedItems
+ {
+ get
+ {
+ if (ItemsView is null && _hasInitSelectedItem)
+ {
+ return new[] { _initSelectedItem };
+ }
+
+ return _selectedItems ??= new SelectedItems(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 ISelectionModel.SelectedItems
+ {
+ get => _selectedItemsUntyped ??= new SelectedItems.Untyped(SelectedItems);
+ }
+
+ public event EventHandler? IndexesChanged;
+ public event EventHandler>? SelectionChanged;
+ public event EventHandler? LostSelection;
+ public event EventHandler? SourceReset;
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ event EventHandler? 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 = CoerceRange(start, end);
+
+ if (range.Begin == -1)
+ {
+ return;
+ }
+
+ if (RangesEnabled)
+ {
+ var selected = Ranges.ToList();
+ var deselected = new List();
+ var operationDeselected = new List();
+
+ o.DeselectedRanges ??= new List();
+ 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;
+ }
+
+ _initSelectedItem = default;
+ _hasInitSelectedItem = false;
+ }
+
+ 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 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 (_hasInitSelectedItem)
+ {
+ SelectedItem = _initSelectedItem;
+ _initSelectedItem = default;
+ _hasInitSelectedItem = false;
+ }
+ 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 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);
+ }
+
+ CommitOperation(update.Operation);
+ }
+
+ 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? 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)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 (oldAnchorIndex != _anchorIndex)
+ {
+ RaisePropertyChanged(nameof(AnchorIndex));
+ }
+ }
+
+ protected override void OnSourceCollectionChangeFinished()
+ {
+ if (_operation is object)
+ {
+ CommitOperation(_operation);
+ }
+ }
+
+ private int GetFirstSelectedIndexFromRanges(List? 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();
+
+ if (RangesEnabled)
+ {
+ o.SelectedRanges ??= new List();
+ 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;
+ }
+
+ _initSelectedItem = default;
+ _hasInitSelectedItem = false;
+ }
+
+ [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.Intersect(selected, valid, removed);
+ operation.DeselectedRanges = removed;
+ }
+ }
+ }
+
+ private void CommitOperation(Operation operation)
+ {
+ 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? deselected = operation.DeselectedRanges;
+ IReadOnlyList? 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.Create(deselected, deselectedSource);
+
+ var e = new SelectionModelSelectionChangedEventArgs(
+ SelectedIndexes.Create(deselected),
+ SelectedIndexes.Create(selected),
+ deselectedItems,
+ SelectedItems.Create(selected, ItemsView));
+ SelectionChanged?.Invoke(this, e);
+ _untypedSelectionChanged?.Invoke(this, e);
+ }
+ }
+
+ if (oldSelectedIndex != _selectedIndex)
+ {
+ indexesChanged = true;
+ RaisePropertyChanged(nameof(SelectedIndex));
+ RaisePropertyChanged(nameof(SelectedItem));
+ }
+
+ if (oldAnchorIndex != _anchorIndex)
+ {
+ indexesChanged = true;
+ RaisePropertyChanged(nameof(AnchorIndex));
+ }
+
+ if (indexesChanged)
+ {
+ RaisePropertyChanged(nameof(SelectedIndexes));
+ RaisePropertyChanged(nameof(SelectedItems));
+ }
+ }
+ finally
+ {
+ _operation = null;
+ }
+ }
+
+ public struct BatchUpdateOperation : IDisposable
+ {
+ private readonly SelectionModel _owner;
+ private bool _isDisposed;
+
+ public BatchUpdateOperation(SelectionModel 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 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? SelectedRanges { get; set; }
+ public List? DeselectedRanges { get; set; }
+ public IReadOnlyList? DeselectedItems { get; set; }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Selection/SelectionModelIndexesChangedEventArgs.cs b/src/Avalonia.Controls/Selection/SelectionModelIndexesChangedEventArgs.cs
new file mode 100644
index 0000000000..a1fef578a2
--- /dev/null
+++ b/src/Avalonia.Controls/Selection/SelectionModelIndexesChangedEventArgs.cs
@@ -0,0 +1,18 @@
+using System;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+ public class SelectionModelIndexesChangedEventArgs : EventArgs
+ {
+ public SelectionModelIndexesChangedEventArgs(int startIndex, int delta)
+ {
+ StartIndex = startIndex;
+ Delta = delta;
+ }
+
+ public int StartIndex { get; }
+ public int Delta { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs b/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs
new file mode 100644
index 0000000000..396943592d
--- /dev/null
+++ b/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Controls.Selection;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+ public abstract class SelectionModelSelectionChangedEventArgs : EventArgs
+ {
+ ///
+ /// Gets the indexes of the items that were removed from the selection.
+ ///
+ public abstract IReadOnlyList DeselectedIndexes { get; }
+
+ ///
+ /// Gets the indexes of the items that were added to the selection.
+ ///
+ public abstract IReadOnlyList SelectedIndexes { get; }
+
+ ///
+ /// Gets the items that were removed from the selection.
+ ///
+ public IReadOnlyList DeselectedItems => GetUntypedDeselectedItems();
+
+ ///
+ /// Gets the items that were added to the selection.
+ ///
+ public IReadOnlyList SelectedItems => GetUntypedSelectedItems();
+
+ protected abstract IReadOnlyList GetUntypedDeselectedItems();
+ protected abstract IReadOnlyList GetUntypedSelectedItems();
+ }
+
+ public class SelectionModelSelectionChangedEventArgs : SelectionModelSelectionChangedEventArgs
+ {
+ private IReadOnlyList? _deselectedItems;
+ private IReadOnlyList? _selectedItems;
+
+ public SelectionModelSelectionChangedEventArgs(
+ IReadOnlyList? deselectedIndices = null,
+ IReadOnlyList? selectedIndices = null,
+ IReadOnlyList? deselectedItems = null,
+ IReadOnlyList? selectedItems = null)
+ {
+ DeselectedIndexes = deselectedIndices ?? Array.Empty();
+ SelectedIndexes = selectedIndices ?? Array.Empty();
+ DeselectedItems = deselectedItems ?? Array.Empty();
+ SelectedItems = selectedItems ?? Array.Empty();
+ }
+
+ ///
+ /// Gets the indexes of the items that were removed from the selection.
+ ///
+ public override IReadOnlyList DeselectedIndexes { get; }
+
+ ///
+ /// Gets the indexes of the items that were added to the selection.
+ ///
+ public override IReadOnlyList SelectedIndexes { get; }
+
+ ///
+ /// Gets the items that were removed from the selection.
+ ///
+ public new IReadOnlyList DeselectedItems { get; }
+
+ ///
+ /// Gets the items that were added to the selection.
+ ///
+ public new IReadOnlyList SelectedItems { get; }
+
+ protected override IReadOnlyList GetUntypedDeselectedItems()
+ {
+ return _deselectedItems ??= (DeselectedItems as IReadOnlyList) ??
+ new SelectedItems.Untyped(DeselectedItems);
+ }
+
+ protected override IReadOnlyList GetUntypedSelectedItems()
+ {
+ return _selectedItems ??= (SelectedItems as IReadOnlyList) ??
+ new SelectedItems.Untyped(SelectedItems);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs
new file mode 100644
index 0000000000..ff3b8f43a8
--- /dev/null
+++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs
@@ -0,0 +1,286 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Controls.Utils;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+ public abstract class SelectionNodeBase : ICollectionChangedListener
+ {
+ private IEnumerable? _source;
+ private bool _rangesEnabled;
+ private List? _ranges;
+ private int _collectionChanging;
+
+ protected IEnumerable? Source
+ {
+ get => _source;
+ set
+ {
+ if (_source != value)
+ {
+ ItemsView?.RemoveListener(this);
+ _source = value;
+ ItemsView = value is object ? ItemsSourceView.GetOrCreate(value) : null;
+ ItemsView?.AddListener(this);
+ }
+ }
+ }
+
+ protected bool IsSourceCollectionChanging => _collectionChanging > 0;
+
+ protected bool RangesEnabled
+ {
+ get => _rangesEnabled;
+ set
+ {
+ if (_rangesEnabled != value)
+ {
+ _rangesEnabled = value;
+
+ if (!_rangesEnabled)
+ {
+ _ranges = null;
+ }
+ }
+ }
+ }
+
+ internal ItemsSourceView? ItemsView { get; set; }
+
+ internal IReadOnlyList Ranges
+ {
+ get
+ {
+ if (!RangesEnabled)
+ {
+ throw new InvalidOperationException("Ranges not enabled.");
+ }
+
+ return _ranges ??= new List();
+ }
+ }
+
+ void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+ {
+ ++_collectionChanging;
+ }
+
+ void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+ {
+ OnSourceCollectionChanged(e);
+ }
+
+ void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (--_collectionChanging == 0)
+ {
+ OnSourceCollectionChangeFinished();
+ }
+ }
+
+ protected abstract void OnSourceCollectionChangeFinished();
+
+ private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta);
+
+ private protected abstract void OnSourceReset();
+
+ private protected abstract void OnSelectionChanged(IReadOnlyList deselectedItems);
+
+ private protected int CommitSelect(IndexRange range)
+ {
+ if (RangesEnabled)
+ {
+ _ranges ??= new List();
+ return IndexRange.Add(_ranges, range);
+ }
+
+ return 0;
+ }
+
+ private protected int CommitSelect(IReadOnlyList ranges)
+ {
+ if (RangesEnabled)
+ {
+ _ranges ??= new List();
+ return IndexRange.Add(_ranges, ranges);
+ }
+
+ return 0;
+ }
+
+ private protected int CommitDeselect(IndexRange range)
+ {
+ if (RangesEnabled)
+ {
+ _ranges ??= new List();
+ return IndexRange.Remove(_ranges, range);
+ }
+
+ return 0;
+ }
+
+ private protected int CommitDeselect(IReadOnlyList ranges)
+ {
+ if (RangesEnabled && _ranges is object)
+ {
+ return IndexRange.Remove(_ranges, ranges);
+ }
+
+ return 0;
+ }
+
+ private protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
+ {
+ var count = items.Count;
+ var shifted = false;
+
+ if (_ranges is object)
+ {
+ List? toAdd = null;
+
+ for (var i = 0; i < Ranges!.Count; ++i)
+ {
+ var range = Ranges[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 ??= new List()).Add(before);
+ begin = index;
+ }
+
+ // Shift the range to the right
+ _ranges[i] = new IndexRange(begin + count, range.End + count);
+ shifted = true;
+ }
+ }
+
+ if (toAdd is object)
+ {
+ foreach (var range in toAdd)
+ {
+ IndexRange.Add(_ranges, range);
+ }
+ }
+ }
+
+ return new CollectionChangeState
+ {
+ ShiftIndex = index,
+ ShiftDelta = shifted ? count : 0,
+ };
+ }
+
+ private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items)
+ {
+ var count = items.Count;
+ var removedRange = new IndexRange(index, index + count - 1);
+ bool shifted = false;
+ List? removed = null;
+
+ if (_ranges is object)
+ {
+ var deselected = new List();
+
+ if (IndexRange.Remove(_ranges, removedRange, deselected) > 0)
+ {
+ removed = new List();
+
+ foreach (var range in deselected)
+ {
+ for (var i = range.Begin; i <= range.End; ++i)
+ {
+#pragma warning disable CS8604
+ removed.Add((T)items[i - index]);
+#pragma warning restore CS8604
+ }
+ }
+ }
+
+ for (var i = 0; i < Ranges!.Count; ++i)
+ {
+ var existing = Ranges[i];
+
+ if (existing.End > removedRange.Begin)
+ {
+ _ranges[i] = new IndexRange(existing.Begin - count, existing.End - count);
+ shifted = true;
+ }
+ }
+ }
+
+ return new CollectionChangeState
+ {
+ ShiftIndex = index,
+ ShiftDelta = shifted ? -count : 0,
+ RemovedItems = removed,
+ };
+ }
+
+ private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
+ {
+ var shiftDelta = 0;
+ var shiftIndex = -1;
+ List? removed = null;
+
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ {
+ var change = OnItemsAdded(e.NewStartingIndex, e.NewItems);
+ shiftIndex = change.ShiftIndex;
+ shiftDelta = change.ShiftDelta;
+ break;
+ }
+ case NotifyCollectionChangedAction.Remove:
+ {
+ var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems);
+ shiftIndex = change.ShiftIndex;
+ shiftDelta = change.ShiftDelta;
+ removed = change.RemovedItems;
+ break;
+ }
+ case NotifyCollectionChangedAction.Replace:
+ {
+ var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems);
+ var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems);
+ shiftIndex = removeChange.ShiftIndex;
+ shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
+ removed = removeChange.RemovedItems;
+ }
+ break;
+ case NotifyCollectionChangedAction.Reset:
+ OnSourceReset();
+ break;
+ }
+
+ if (shiftDelta != 0)
+ {
+ OnIndexesChanged(shiftIndex, shiftDelta);
+ }
+
+ if (removed is object)
+ {
+ OnSelectionChanged(removed);
+ }
+ }
+
+ private protected struct CollectionChangeState
+ {
+ public int ShiftIndex;
+ public int ShiftDelta;
+ public List? RemovedItems;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs
deleted file mode 100644
index aa6552579f..0000000000
--- a/src/Avalonia.Controls/SelectionModel.cs
+++ /dev/null
@@ -1,894 +0,0 @@
-// 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 System.Linq;
-using System.Reactive.Linq;
-using Avalonia.Controls.Utils;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
- public class SelectionModel : ISelectionModel, IDisposable
- {
- private readonly SelectionNode _rootNode;
- private bool _singleSelect;
- private bool _autoSelect;
- private int _operationCount;
- private IndexPath _oldAnchorIndex;
- private IReadOnlyList? _selectedIndicesCached;
- private IReadOnlyList? _selectedItemsCached;
- private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
-
- public event EventHandler? ChildrenRequested;
- public event PropertyChangedEventHandler? PropertyChanged;
- public event EventHandler? SelectionChanged;
-
- public SelectionModel()
- {
- _rootNode = new SelectionNode(this, null);
- SharedLeafNode = new SelectionNode(this, null);
- }
-
- public object? Source
- {
- get => _rootNode.Source;
- set
- {
- if (_rootNode.Source != value)
- {
- var raiseChanged = _rootNode.Source == null && SelectedIndices.Count > 0;
-
- if (_rootNode.Source != null)
- {
- // Temporarily prevent auto-select when switching source.
- var restoreAutoSelect = _autoSelect;
- _autoSelect = false;
-
- try
- {
- using (var operation = new Operation(this))
- {
- ClearSelection(resetAnchor: true);
- }
- }
- finally
- {
- _autoSelect = restoreAutoSelect;
- }
- }
-
- _rootNode.Source = value;
- ApplyAutoSelect(true);
-
- RaisePropertyChanged("Source");
-
- if (raiseChanged)
- {
- var e = new SelectionModelSelectionChangedEventArgs(
- null,
- SelectedIndices,
- null,
- SelectedItems);
- OnSelectionChanged(e);
- }
- }
- }
- }
-
- public bool SingleSelect
- {
- get => _singleSelect;
- set
- {
- if (_singleSelect != value)
- {
- _singleSelect = value;
- var selectedIndices = SelectedIndices;
-
- if (value && selectedIndices != null && selectedIndices.Count > 0)
- {
- using var operation = new Operation(this);
-
- // We want to be single select, so make sure there is only
- // one selected item.
- var firstSelectionIndexPath = selectedIndices[0];
- ClearSelection(resetAnchor: true);
- SelectWithPathImpl(firstSelectionIndexPath, select: true);
- SelectedIndex = firstSelectionIndexPath;
- }
-
- RaisePropertyChanged("SingleSelect");
- }
- }
- }
-
- public bool RetainSelectionOnReset
- {
- get => _rootNode.RetainSelectionOnReset;
- set => _rootNode.RetainSelectionOnReset = value;
- }
-
- public bool AutoSelect
- {
- get => _autoSelect;
- set
- {
- if (_autoSelect != value)
- {
- _autoSelect = value;
- ApplyAutoSelect(true);
- }
- }
- }
-
- public IndexPath AnchorIndex
- {
- get
- {
- IndexPath anchor = default;
-
- if (_rootNode.AnchorIndex >= 0)
- {
- var path = new List();
- SelectionNode? current = _rootNode;
-
- while (current?.AnchorIndex >= 0)
- {
- path.Add(current.AnchorIndex);
- current = current.GetAt(current.AnchorIndex, false, default);
- }
-
- anchor = new IndexPath(path);
- }
-
- return anchor;
- }
- set
- {
- var oldValue = AnchorIndex;
-
- if (value != null)
- {
- SelectionTreeHelper.TraverseIndexPath(
- _rootNode,
- value,
- realizeChildren: true,
- (currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth));
- }
- else
- {
- _rootNode.AnchorIndex = -1;
- }
-
- if (_operationCount == 0 && oldValue != AnchorIndex)
- {
- RaisePropertyChanged("AnchorIndex");
- }
- }
- }
-
- public IndexPath SelectedIndex
- {
- get
- {
- IndexPath selectedIndex = default;
- var selectedIndices = SelectedIndices;
-
- if (selectedIndices?.Count > 0)
- {
- selectedIndex = selectedIndices[0];
- }
-
- return selectedIndex;
- }
- set
- {
- if (!IsSelectedAt(value) || SelectedItems.Count > 1)
- {
- using var operation = new Operation(this);
- ClearSelection(resetAnchor: true);
- SelectWithPathImpl(value, select: true);
- }
- }
- }
-
- public object? SelectedItem
- {
- get
- {
- object? item = null;
- var selectedItems = SelectedItems;
-
- if (selectedItems?.Count > 0)
- {
- item = selectedItems[0];
- }
-
- return item;
- }
- }
-
- public IReadOnlyList SelectedItems
- {
- get
- {
- if (_selectedItemsCached == null)
- {
- var selectedInfos = new List();
- var count = 0;
-
- if (_rootNode.Source != null)
- {
- SelectionTreeHelper.Traverse(
- _rootNode,
- realizeChildren: false,
- currentInfo =>
- {
- if (currentInfo.Node.SelectedCount > 0)
- {
- selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
- count += currentInfo.Node.SelectedCount;
- }
- });
- }
-
- // Instead of creating a dumb vector that takes up the space for all the selected items,
- // we create a custom IReadOnlyList implementation 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 (
- selectedInfos,
- count,
- (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 SelectedIndices
- {
- get
- {
- if (_selectedIndicesCached == null)
- {
- var selectedInfos = new List();
- var count = 0;
-
- SelectionTreeHelper.Traverse(
- _rootNode,
- false,
- currentInfo =>
- {
- if (currentInfo.Node.SelectedCount > 0)
- {
- selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
- count += currentInfo.Node.SelectedCount;
- }
- });
-
- // 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(
- selectedInfos,
- count,
- (infos, index) => // callback for GetAt(index)
- {
- var currentIndex = 0;
- IndexPath path = default;
-
- 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);
- _rootNode.Cleanup();
- _rootNode.Dispose();
- _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)
- {
- using var operation = new Operation(this);
- SelectImpl(index, select: true);
- }
-
- public void Select(int groupIndex, int itemIndex)
- {
- using var operation = new Operation(this);
- SelectWithGroupImpl(groupIndex, itemIndex, select: true);
- }
-
- public void SelectAt(IndexPath index)
- {
- using var operation = new Operation(this);
- SelectWithPathImpl(index, select: true);
- }
-
- public void Deselect(int index)
- {
- using var operation = new Operation(this);
- SelectImpl(index, select: false);
- }
-
- public void Deselect(int groupIndex, int itemIndex)
- {
- using var operation = new Operation(this);
- SelectWithGroupImpl(groupIndex, itemIndex, select: false);
- }
-
- public void DeselectAt(IndexPath index)
- {
- using var operation = new Operation(this);
- SelectWithPathImpl(index, select: false);
- }
-
- public bool IsSelected(int index) => _rootNode.IsSelected(index);
-
- public bool IsSelected(int grouIndex, int itemIndex)
- {
- return IsSelectedAt(new IndexPath(grouIndex, itemIndex));
- }
-
- public bool IsSelectedAt(IndexPath index)
- {
- var path = index;
- SelectionNode? node = _rootNode;
-
- for (int i = 0; i < path.GetSize() - 1; i++)
- {
- var childIndex = path.GetAt(i);
- node = node.GetAt(childIndex, false, default);
-
- if (node == null)
- {
- return false;
- }
- }
-
- return node.IsSelected(index.GetAt(index.GetSize() - 1));
- }
-
- public bool? IsSelectedWithPartial(int index)
- {
- if (index < 0)
- {
- throw new ArgumentException("Index must be >= 0", nameof(index));
- }
-
- var isSelected = _rootNode.IsSelectedWithPartial(index);
- return isSelected;
- }
-
- public bool? IsSelectedWithPartial(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, false, default);
-
- if (childNode != null)
- {
- isSelected = childNode.IsSelectedWithPartial(itemIndex);
- }
-
- return isSelected;
- }
-
- public bool? IsSelectedWithPartialAt(IndexPath index)
- {
- var path = index;
- var isRealized = true;
- SelectionNode? node = _rootNode;
-
- for (int i = 0; i < path.GetSize() - 1; i++)
- {
- var childIndex = path.GetAt(i);
- node = node.GetAt(childIndex, false, default);
-
- 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)
- {
- using var operation = new Operation(this);
- SelectRangeFromAnchorImpl(index, select: true);
- }
-
- public void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex)
- {
- using var operation = new Operation(this);
- SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, select: true);
- }
-
- public void SelectRangeFromAnchorTo(IndexPath index)
- {
- using var operation = new Operation(this);
- SelectRangeImpl(AnchorIndex, index, select: true);
- }
-
- public void DeselectRangeFromAnchor(int index)
- {
- using var operation = new Operation(this);
- SelectRangeFromAnchorImpl(index, select: false);
- }
-
- public void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex)
- {
- using var operation = new Operation(this);
- SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, false /* select */);
- }
-
- public void DeselectRangeFromAnchorTo(IndexPath index)
- {
- using var operation = new Operation(this);
- SelectRangeImpl(AnchorIndex, index, select: false);
- }
-
- public void SelectRange(IndexPath start, IndexPath end)
- {
- using var operation = new Operation(this);
- SelectRangeImpl(start, end, select: true);
- }
-
- public void DeselectRange(IndexPath start, IndexPath end)
- {
- using var operation = new Operation(this);
- SelectRangeImpl(start, end, select: false);
- }
-
- public void SelectAll()
- {
- using var operation = new Operation(this);
-
- SelectionTreeHelper.Traverse(
- _rootNode,
- realizeChildren: true,
- info =>
- {
- if (info.Node.DataCount > 0)
- {
- info.Node.SelectAll();
- }
- });
- }
-
- public void ClearSelection()
- {
- using var operation = new Operation(this);
- ClearSelection(resetAnchor: true);
- }
-
- public IDisposable Update() => new Operation(this);
-
- protected void OnPropertyChanged(string propertyName)
- {
- RaisePropertyChanged(propertyName);
- }
-
- private void RaisePropertyChanged(string propertyName)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
-
- public void OnSelectionInvalidatedDueToCollectionChange(
- bool selectionInvalidated,
- IReadOnlyList? removedItems)
- {
- SelectionModelSelectionChangedEventArgs? e = null;
-
- if (selectionInvalidated)
- {
- e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null);
- }
-
- OnSelectionChanged(e);
- ApplyAutoSelect(true);
- }
-
- internal IObservable? ResolvePath(
- object data,
- IndexPath dataIndexPath,
- IndexPath finalIndexPath)
- {
- IObservable? resolved = null;
-
- // Raise ChildrenRequested event if there is a handler
- if (ChildrenRequested != null)
- {
- if (_childrenRequestedEventArgs == null)
- {
- _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(
- data,
- dataIndexPath,
- finalIndexPath,
- false);
- }
- else
- {
- _childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false);
- }
-
- 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, default, default, true);
- }
-
- return resolved;
- }
-
- private void ClearSelection(bool resetAnchor)
- {
- SelectionTreeHelper.Traverse(
- _rootNode,
- realizeChildren: false,
- info => info.Node.Clear());
-
- if (resetAnchor)
- {
- AnchorIndex = default;
- }
-
- OnSelectionChanged();
- }
-
- private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null)
- {
- _selectedIndicesCached = null;
- _selectedItemsCached = null;
-
- if (e != null)
- {
- SelectionChanged?.Invoke(this, e);
-
- 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);
- }
-
- 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);
- }
-
- var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex));
- var selected = childNode!.Select(itemIndex, select);
-
- if (selected)
- {
- AnchorIndex = new IndexPath(groupIndex, itemIndex);
- }
-
- OnSelectionChanged();
- }
-
- private void SelectWithPathImpl(IndexPath index, bool select)
- {
- bool selected = false;
-
- if (_singleSelect)
- {
- ClearSelection(resetAnchor: true);
- }
-
- SelectionTreeHelper.TraverseIndexPath(
- _rootNode,
- index,
- true,
- (currentNode, path, depth, childIndex) =>
- {
- if (depth == path.GetSize() - 1)
- {
- selected = currentNode.Select(childIndex, select);
- }
- }
- );
-
- if (selected)
- {
- AnchorIndex = index;
- }
-
- OnSelectionChanged();
- }
-
- private void SelectRangeFromAnchorImpl(int index, bool select)
- {
- int anchorIndex = 0;
- var anchor = AnchorIndex;
-
- if (anchor != null)
- {
- anchorIndex = anchor.GetAt(0);
- }
-
- _rootNode.SelectRange(new IndexRange(anchorIndex, index), select);
- 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;
- }
-
- for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++)
- {
- var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!;
- int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0;
- int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1;
- groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);
- }
-
- 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.Path >= winrtStart && info.Path <= winrtEnd)
- {
- info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
- }
- });
-
- OnSelectionChanged();
- }
-
- private void BeginOperation()
- {
- if (_operationCount++ == 0)
- {
- _oldAnchorIndex = AnchorIndex;
- _rootNode.BeginOperation();
- }
- }
-
- private void EndOperation()
- {
- if (_operationCount == 0)
- {
- throw new AvaloniaInternalException("No selection operation in progress.");
- }
-
- SelectionModelSelectionChangedEventArgs? e = null;
-
- if (--_operationCount == 0)
- {
- ApplyAutoSelect(false);
-
- var changes = new List();
- _rootNode.EndOperation(changes);
-
- if (changes.Count > 0)
- {
- var changeSet = new SelectionModelChangeSet(changes);
- e = changeSet.CreateEventArgs();
- }
-
- OnSelectionChanged(e);
-
- if (_oldAnchorIndex != AnchorIndex)
- {
- RaisePropertyChanged(nameof(AnchorIndex));
- }
-
- _rootNode.Cleanup();
- _oldAnchorIndex = default;
- }
- }
-
- private void ApplyAutoSelect(bool createOperation)
- {
- if (AutoSelect)
- {
- _selectedIndicesCached = null;
-
- if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0)
- {
- if (createOperation)
- {
- using var operation = new Operation(this);
- SelectImpl(0, true);
- }
- else
- {
- SelectImpl(0, true);
- }
- }
- }
- }
-
- internal class SelectedItemInfo : ISelectedItemInfo
- {
- public SelectedItemInfo(SelectionNode node, IndexPath path)
- {
- Node = node;
- Path = path;
- }
-
- public SelectionNode Node { get; }
- public IndexPath Path { get; }
- public int Count => Node.SelectedCount;
- }
-
- private struct Operation : IDisposable
- {
- private readonly SelectionModel _manager;
- public Operation(SelectionModel manager) => (_manager = manager).BeginOperation();
- public void Dispose() => _manager.EndOperation();
- }
- }
-}
diff --git a/src/Avalonia.Controls/SelectionModelChangeSet.cs b/src/Avalonia.Controls/SelectionModelChangeSet.cs
deleted file mode 100644
index d1df38656a..0000000000
--- a/src/Avalonia.Controls/SelectionModelChangeSet.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
- internal class SelectionModelChangeSet
- {
- private readonly List _changes;
-
- public SelectionModelChangeSet(List changes)
- {
- _changes = changes;
- }
-
- public SelectionModelSelectionChangedEventArgs CreateEventArgs()
- {
- var deselectedIndexCount = 0;
- var selectedIndexCount = 0;
- var deselectedItemCount = 0;
- var selectedItemCount = 0;
-
- foreach (var change in _changes)
- {
- deselectedIndexCount += change.DeselectedCount;
- selectedIndexCount += change.SelectedCount;
-
- if (change.Items != null)
- {
- deselectedItemCount += change.DeselectedCount;
- selectedItemCount += change.SelectedCount;
- }
- }
-
- var deselectedIndices = new SelectedItems(
- _changes,
- deselectedIndexCount,
- GetDeselectedIndexAt);
- var selectedIndices = new SelectedItems(
- _changes,
- selectedIndexCount,
- GetSelectedIndexAt);
- var deselectedItems = new SelectedItems(
- _changes,
- deselectedItemCount,
- GetDeselectedItemAt);
- var selectedItems = new SelectedItems(
- _changes,
- selectedItemCount,
- GetSelectedItemAt);
-
- return new SelectionModelSelectionChangedEventArgs(
- deselectedIndices,
- selectedIndices,
- deselectedItems,
- selectedItems);
- }
-
- private IndexPath GetDeselectedIndexAt(
- List infos,
- int index)
- {
- static int GetCount(SelectionNodeOperation info) => info.DeselectedCount;
- static List? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
- return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
- }
-
- private IndexPath GetSelectedIndexAt(
- List infos,
- int index)
- {
- static int GetCount(SelectionNodeOperation info) => info.SelectedCount;
- static List? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
- return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
- }
-
- private object? GetDeselectedItemAt(
- List infos,
- int index)
- {
- static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.DeselectedCount : 0;
- static List? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
- return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
- }
-
- private object? GetSelectedItemAt(
- List infos,
- int index)
- {
- static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.SelectedCount : 0;
- static List? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
- return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
- }
-
- private IndexPath GetIndexAt(
- List infos,
- int index,
- Func getCount,
- Func?> getRanges)
- {
- var currentIndex = 0;
- IndexPath path = default;
-
- foreach (var info in infos)
- {
- var currentCount = getCount(info);
-
- if (index >= currentIndex && index < currentIndex + currentCount)
- {
- int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
- path = info.Path.CloneWithChildIndex(targetIndex);
- break;
- }
-
- currentIndex += currentCount;
- }
-
- return path;
- }
-
- private object? GetItemAt(
- List infos,
- int index,
- Func getCount,
- Func?> getRanges)
- {
- var currentIndex = 0;
- object? item = null;
-
- foreach (var info in infos)
- {
- var currentCount = getCount(info);
-
- if (index >= currentIndex && index < currentIndex + currentCount)
- {
- int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
- item = info.Items?.Count > targetIndex ? info.Items?.GetAt(targetIndex) : null;
- break;
- }
-
- currentIndex += currentCount;
- }
-
- return item;
- }
-
- private int GetIndexAt(List? ranges, int index)
- {
- var currentIndex = 0;
-
- if (ranges != null)
- {
- foreach (var range in ranges)
- {
- var currentCount = (range.End - range.Begin) + 1;
-
- if (index >= currentIndex && index < currentIndex + currentCount)
- {
- return range.Begin + (index - currentIndex);
- }
-
- currentIndex += currentCount;
- }
- }
-
- throw new IndexOutOfRangeException();
- }
- }
-}
diff --git a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
deleted file mode 100644
index b1f3e0b2c4..0000000000
--- a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-// 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;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
- ///
- /// Provides data for the event.
- ///
- public class SelectionModelChildrenRequestedEventArgs : EventArgs
- {
- private object? _source;
- private IndexPath _sourceIndexPath;
- private IndexPath _finalIndexPath;
- private bool _throwOnAccess;
-
- internal SelectionModelChildrenRequestedEventArgs(
- object source,
- IndexPath sourceIndexPath,
- IndexPath finalIndexPath,
- bool throwOnAccess)
- {
- source = source ?? throw new ArgumentNullException(nameof(source));
- Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess);
- }
-
- ///
- /// Gets or sets an observable which produces the children of the
- /// object.
- ///
- public IObservable? Children { get; set; }
-
- ///
- /// Gets the object whose children are being requested.
- ///
- public object Source
- {
- get
- {
- if (_throwOnAccess)
- {
- throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
- }
-
- return _source!;
- }
- }
-
- ///
- /// Gets the index of the object whose children are being requested.
- ///
- public IndexPath SourceIndex
- {
- get
- {
- if (_throwOnAccess)
- {
- throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
- }
-
- return _sourceIndexPath;
- }
- }
-
- ///
- /// Gets the index of the final object which is being attempted to be retrieved.
- ///
- public IndexPath FinalIndex
- {
- get
- {
- if (_throwOnAccess)
- {
- throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
- }
-
- return _finalIndexPath;
- }
- }
-
- internal void Initialize(
- object? source,
- IndexPath sourceIndexPath,
- IndexPath finalIndexPath,
- bool throwOnAccess)
- {
- if (!throwOnAccess && source == null)
- {
- throw new ArgumentNullException(nameof(source));
- }
-
- _source = source;
- _sourceIndexPath = sourceIndexPath;
- _finalIndexPath = finalIndexPath;
- _throwOnAccess = throwOnAccess;
- }
- }
-}
diff --git a/src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs b/src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs
deleted file mode 100644
index 5e2efdf331..0000000000
--- a/src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// 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;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
- public class SelectionModelSelectionChangedEventArgs : EventArgs
- {
- public SelectionModelSelectionChangedEventArgs(
- IReadOnlyList? deselectedIndices,
- IReadOnlyList