From af90219ff437b2cd78e0bfe1975df45dc9e97f69 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 19 Aug 2020 15:11:00 +0200 Subject: [PATCH] Reverted SelectionModel. --- samples/BindingDemo/MainWindow.xaml | 4 +- .../ViewModels/MainWindowViewModel.cs | 4 +- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- .../ControlCatalog/Pages/TreeViewPage.xaml | 2 +- .../ViewModels/ListBoxPageViewModel.cs | 18 +- .../ViewModels/TreeViewPageViewModel.cs | 34 +- samples/VirtualizationDemo/MainWindow.xaml | 4 +- .../ViewModels/MainWindowViewModel.cs | 14 +- src/Avalonia.Controls/ComboBox.cs | 2 +- src/Avalonia.Controls/ISelectionModel.cs | 249 -- src/Avalonia.Controls/IndexPath.cs | 200 -- src/Avalonia.Controls/IndexRange.cs | 279 -- src/Avalonia.Controls/ListBox.cs | 19 +- .../Primitives/SelectingItemsControl.cs | 821 ++++-- src/Avalonia.Controls/SelectedItems.cs | 49 - src/Avalonia.Controls/SelectionModel.cs | 894 ------- .../SelectionModelChangeSet.cs | 170 -- ...electionModelChildrenRequestedEventArgs.cs | 103 - ...SelectionModelSelectionChangedEventArgs.cs | 47 - src/Avalonia.Controls/SelectionNode.cs | 971 ------- .../SelectionNodeOperation.cs | 110 - src/Avalonia.Controls/TreeView.cs | 669 ++--- .../Utils/SelectedItemsSync.cs | 258 -- .../Utils/SelectionTreeHelper.cs | 189 -- .../Diagnostics/ViewModels/TreeNode.cs | 21 - .../ViewModels/TreePageViewModel.cs | 14 +- .../Diagnostics/Views/TreePageView.xaml | 2 +- .../CarouselTests.cs | 6 +- .../IndexPathTests.cs | 95 - .../IndexRangeTests.cs | 389 --- .../ListBoxTests.cs | 1 - .../Primitives/SelectingItemsControlTests.cs | 4 +- .../SelectingItemsControlTests_AutoSelect.cs | 4 +- .../SelectingItemsControlTests_Multiple.cs | 186 +- .../Primitives/TabStripTests.cs | 9 +- .../SelectionModelTests.cs | 2322 ----------------- .../TabControlTests.cs | 5 +- .../TreeViewTests.cs | 16 +- .../Utils/SelectedItemsSyncTests.cs | 237 -- 39 files changed, 1005 insertions(+), 7418 deletions(-) delete mode 100644 src/Avalonia.Controls/ISelectionModel.cs delete mode 100644 src/Avalonia.Controls/IndexPath.cs delete mode 100644 src/Avalonia.Controls/IndexRange.cs delete mode 100644 src/Avalonia.Controls/SelectedItems.cs delete mode 100644 src/Avalonia.Controls/SelectionModel.cs delete mode 100644 src/Avalonia.Controls/SelectionModelChangeSet.cs delete mode 100644 src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs delete mode 100644 src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs delete mode 100644 src/Avalonia.Controls/SelectionNode.cs delete mode 100644 src/Avalonia.Controls/SelectionNodeOperation.cs delete mode 100644 src/Avalonia.Controls/Utils/SelectedItemsSync.cs delete mode 100644 src/Avalonia.Controls/Utils/SelectionTreeHelper.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/IndexPathTests.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index 14c371efef..735e2d7102 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -75,11 +75,11 @@ - + - + diff --git a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs index 66800a606c..600e03ea47 100644 --- a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs @@ -28,7 +28,7 @@ namespace BindingDemo.ViewModels Detail = "Item " + x + " details", })); - Selection = new SelectionModel(); + SelectedItems = new ObservableCollection(); ShuffleItems = ReactiveCommand.Create(() => { @@ -57,7 +57,7 @@ namespace BindingDemo.ViewModels } public ObservableCollection Items { get; } - public SelectionModel Selection { get; } + public ObservableCollection SelectedItems { get; } public ReactiveCommand ShuffleItems { get; } public string BooleanString diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index edf3d41bf5..c92ea8f25b 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -11,7 +11,7 @@ Spacing="16"> - + diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index 6bdb5c0103..7e8f4dbcee 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -15,16 +15,16 @@ namespace ControlCatalog.ViewModels public ListBoxPageViewModel() { Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); - Selection = new SelectionModel(); - Selection.Select(1); + SelectedItems = new ObservableCollection(); + SelectedItems.Add(Items[1]); AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); RemoveItemCommand = ReactiveCommand.Create(() => { - while (Selection.SelectedItems.Count > 0) + while (SelectedItems.Count > 0) { - Items.Remove((string)Selection.SelectedItems.First()); + Items.Remove(SelectedItems.First()); } }); @@ -32,17 +32,14 @@ namespace ControlCatalog.ViewModels { var random = new Random(); - using (Selection.Update()) - { - Selection.ClearSelection(); - Selection.Select(random.Next(Items.Count - 1)); - } + SelectedItems.Clear(); + SelectedItems.Add(Items[random.Next(Items.Count - 1)]); }); } public ObservableCollection Items { get; } - public SelectionModel Selection { get; } + public ObservableCollection SelectedItems { get; } public ReactiveCommand AddItemCommand { get; } @@ -55,7 +52,6 @@ namespace ControlCatalog.ViewModels get => _selectionMode; set { - Selection.ClearSelection(); 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/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml index 4bd657bf93..dfe7524997 100644 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ b/samples/VirtualizationDemo/MainWindow.xaml @@ -44,8 +44,8 @@ SelectedItems { get; } + = new AvaloniaList(); public AvaloniaList Items { @@ -137,9 +138,9 @@ namespace VirtualizationDemo.ViewModels { var index = Items.Count; - if (Selection.SelectedIndices.Count > 0) + if (SelectedItems.Count > 0) { - index = Selection.SelectedIndex.GetAt(0); + index = Items.IndexOf(SelectedItems[0]); } Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); @@ -147,9 +148,9 @@ namespace VirtualizationDemo.ViewModels private void Remove() { - if (Selection.SelectedItems.Count > 0) + if (SelectedItems.Count > 0) { - Items.RemoveAll(Selection.SelectedItems.Cast().ToList()); + Items.RemoveAll(SelectedItems); } } @@ -163,7 +164,8 @@ namespace VirtualizationDemo.ViewModels private void SelectItem(int index) { - Selection.SelectedIndex = new IndexPath(index); + SelectedItems.Clear(); + SelectedItems.Add(Items[index]); } } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 27313b0b4c..bd9d7e0c97 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -347,7 +347,7 @@ namespace Avalonia.Controls if (container == null && SelectedIndex != -1) { - ScrollIntoView(Selection.SelectedIndex); + ScrollIntoView(SelectedItems[0]); container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); } 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/IndexRange.cs b/src/Avalonia.Controls/IndexRange.cs deleted file mode 100644 index e45d013af4..0000000000 --- a/src/Avalonia.Controls/IndexRange.cs +++ /dev/null @@ -1,279 +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 -{ - internal readonly struct IndexRange : IEquatable - { - private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue); - - public IndexRange(int begin, int end) - { - // Accept out of order begin/end pairs, just swap them. - if (begin > end) - { - int temp = begin; - begin = end; - end = temp; - } - - Begin = begin; - End = end; - } - - public int Begin { get; } - public int End { get; } - public int Count => (End - Begin) + 1; - - public bool Contains(int index) => index >= Begin && index <= End; - - public bool Split(int splitIndex, out IndexRange before, out IndexRange after) - { - bool afterIsValid; - - before = new IndexRange(Begin, splitIndex); - - if (splitIndex < End) - { - after = new IndexRange(splitIndex + 1, End); - afterIsValid = true; - } - else - { - after = new IndexRange(); - afterIsValid = false; - } - - return afterIsValid; - } - - public bool Intersects(IndexRange other) - { - return (Begin <= other.End) && (End >= other.Begin); - } - - public bool Adjacent(IndexRange other) - { - return Begin == other.End + 1 || End == other.Begin - 1; - } - - public override bool Equals(object? obj) - { - return obj is IndexRange range && Equals(range); - } - - public bool Equals(IndexRange other) - { - return Begin == other.Begin && End == other.End; - } - - public override int GetHashCode() - { - var hashCode = 1903003160; - hashCode = hashCode * -1521134295 + Begin.GetHashCode(); - hashCode = hashCode * -1521134295 + End.GetHashCode(); - return hashCode; - } - - public override string ToString() => $"[{Begin}..{End}]"; - - public static bool operator ==(IndexRange left, IndexRange right) => left.Equals(right); - public static bool operator !=(IndexRange left, IndexRange right) => !(left == right); - - public static int Add( - IList ranges, - IndexRange range, - IList? added = null) - { - var result = 0; - - for (var i = 0; i < ranges.Count && range != s_invalid; ++i) - { - var existing = ranges[i]; - - if (range.Intersects(existing) || range.Adjacent(existing)) - { - if (range.Begin < existing.Begin) - { - var add = new IndexRange(range.Begin, existing.Begin - 1); - ranges[i] = new IndexRange(range.Begin, existing.End); - added?.Add(add); - result += add.Count; - } - - range = range.End <= existing.End ? - s_invalid : - new IndexRange(existing.End + 1, range.End); - } - else if (range.End < existing.Begin) - { - ranges.Insert(i, range); - added?.Add(range); - result += range.Count; - range = s_invalid; - } - } - - if (range != s_invalid) - { - ranges.Add(range); - added?.Add(range); - result += range.Count; - } - - MergeRanges(ranges); - return result; - } - - public static int Intersect( - IList ranges, - IndexRange range, - IList? removed = null) - { - var result = 0; - - for (var i = 0; i < ranges.Count && range != s_invalid; ++i) - { - var existing = ranges[i]; - - if (existing.End < range.Begin || existing.Begin > range.End) - { - removed?.Add(existing); - ranges.RemoveAt(i--); - result += existing.Count; - } - else - { - if (existing.Begin < range.Begin) - { - var except = new IndexRange(existing.Begin, range.Begin - 1); - removed?.Add(except); - ranges[i] = existing = new IndexRange(range.Begin, existing.End); - result += except.Count; - } - - if (existing.End > range.End) - { - var except = new IndexRange(range.End + 1, existing.End); - removed?.Add(except); - ranges[i] = new IndexRange(existing.Begin, range.End); - result += except.Count; - } - } - } - - MergeRanges(ranges); - - if (removed is object) - { - MergeRanges(removed); - } - - return result; - } - - public static int Remove( - IList ranges, - IndexRange range, - IList? removed = null) - { - var result = 0; - - for (var i = 0; i < ranges.Count; ++i) - { - var existing = ranges[i]; - - if (range.Intersects(existing)) - { - if (range.Begin <= existing.Begin && range.End >= existing.End) - { - ranges.RemoveAt(i--); - removed?.Add(existing); - result += existing.Count; - } - else if (range.Begin > existing.Begin && range.End >= existing.End) - { - ranges[i] = new IndexRange(existing.Begin, range.Begin - 1); - removed?.Add(new IndexRange(range.Begin, existing.End)); - result += existing.End - (range.Begin - 1); - } - else if (range.Begin > existing.Begin && range.End < existing.End) - { - ranges[i] = new IndexRange(existing.Begin, range.Begin - 1); - ranges.Insert(++i, new IndexRange(range.End + 1, existing.End)); - removed?.Add(range); - result += range.Count; - } - else if (range.End <= existing.End) - { - var remove = new IndexRange(existing.Begin, range.End); - ranges[i] = new IndexRange(range.End + 1, existing.End); - removed?.Add(remove); - result += remove.Count; - } - } - } - - return result; - } - - public static IEnumerable Subtract( - IndexRange lhs, - IEnumerable rhs) - { - var result = new List { lhs }; - - foreach (var range in rhs) - { - Remove(result, range); - } - - return result; - } - - public static IEnumerable EnumerateIndices(IEnumerable ranges) - { - foreach (var range in ranges) - { - for (var i = range.Begin; i <= range.End; ++i) - { - yield return i; - } - } - } - - public static int GetCount(IEnumerable ranges) - { - var result = 0; - - foreach (var range in ranges) - { - result += (range.End - range.Begin) + 1; - } - - return result; - } - - private static void MergeRanges(IList ranges) - { - for (var i = ranges.Count - 2; i >= 0; --i) - { - var r = ranges[i]; - var r1 = ranges[i + 1]; - - if (r.Intersects(r1) || r.End == r1.Begin - 1) - { - ranges[i] = new IndexRange(r.Begin, r1.End); - ranges.RemoveAt(i + 1); - } - } - } - } -} diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index a085bfb6bc..2162019343 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -31,12 +31,6 @@ namespace Avalonia.Controls public static readonly new DirectProperty SelectedItemsProperty = SelectingItemsControl.SelectedItemsProperty; - /// - /// Defines the property. - /// - public static readonly new DirectProperty SelectionProperty = - SelectingItemsControl.SelectionProperty; - /// /// Defines the property. /// @@ -76,15 +70,6 @@ namespace Avalonia.Controls set => base.SelectedItems = value; } - /// - /// Gets or sets a model holding the current selection. - /// - public new ISelectionModel Selection - { - get => base.Selection; - set => base.Selection = value; - } - /// /// Gets or sets the selection mode. /// @@ -110,12 +95,12 @@ namespace Avalonia.Controls /// /// Selects all items in the . /// - public void SelectAll() => Selection.SelectAll(); + public void SelectAll() => base.SelectAll(); /// /// Deselects all items in the . /// - public void UnselectAll() => Selection.ClearSelection(); + public void UnselectAll() => base.UnselectAll(); /// protected override IItemContainerGenerator CreateItemContainerGenerator() diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 59b7777b1b..1dcbeaf23e 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -5,12 +5,14 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Logging; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -23,9 +25,9 @@ namespace Avalonia.Controls.Primitives /// provides a base class for s /// 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. + /// current multiple together with the + /// properties are protected, however a derived class can expose these if it wishes to support + /// multiple selection. /// /// /// maintains a selection respecting the current @@ -74,15 +76,6 @@ namespace Avalonia.Controls.Primitives o => o.SelectedItems, (o, v) => o.SelectedItems = v); - /// - /// Defines the property. - /// - public static readonly DirectProperty SelectionProperty = - AvaloniaProperty.RegisterDirect( - nameof(Selection), - o => o.Selection, - (o, v) => o.Selection = v); - /// /// Defines the property. /// @@ -109,22 +102,16 @@ namespace Avalonia.Controls.Primitives RoutingStrategies.Bubble); private static readonly IList Empty = Array.Empty(); - private readonly SelectedItemsSync _selectedItems; - private ISelectionModel _selection; + private readonly Selection _selection = new Selection(); private int _selectedIndex = -1; private object _selectedItem; + private IList _selectedItems; private bool _ignoreContainerSelectionChanged; + private bool _syncingSelectedItems; 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,15 +143,13 @@ namespace Avalonia.Controls.Primitives /// public int SelectedIndex { - get => Selection.SelectedIndex != default ? Selection.SelectedIndex.GetAt(0) : -1; + get => _selectedIndex; set { if (_updateCount == 0) { - if (value != SelectedIndex) - { - Selection.SelectedIndex = new IndexPath(value); - } + var effective = (value >= 0 && value < ItemCount) ? value : -1; + UpdateSelectedItem(effective); } else { @@ -179,12 +164,12 @@ namespace Avalonia.Controls.Primitives /// public object SelectedItem { - get => Selection.SelectedItem; + get => _selectedItem; set { if (_updateCount == 0) { - SelectedIndex = IndexOf(Items, value); + UpdateSelectedItem(IndexOf(Items, value)); } else { @@ -199,106 +184,28 @@ namespace Avalonia.Controls.Primitives /// protected IList SelectedItems { - get => _selectedItems.GetOrCreateItems(); - set => _selectedItems.SetItems(value); - } - - /// - /// Gets or sets a model holding the current selection. - /// - protected ISelectionModel Selection - { - get => _selection; - set + get { - value ??= new SelectionModel - { - SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), - AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected), - RetainSelectionOnReset = true, - }; - - if (_selection != value) + if (_selectedItems == null) { - 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) - { - oldSelection = Selection.SelectedItems.ToList(); - _selection.PropertyChanged -= OnSelectionModelPropertyChanged; - _selection.SelectionChanged -= OnSelectionModelSelectionChanged; - MarkContainersUnselected(); - } - - _selection = value; - - if (oldSelection?.Count > 0) - { - RaiseEvent(new SelectionChangedEventArgs( - SelectionChangedEvent, - oldSelection, - 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; + _selectedItems = new AvaloniaList(); + SubscribeToSelectedItems(); + } - if (_selectedIndex != selectedIndex) - { - RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, selectedIndex); - _selectedIndex = selectedIndex; - } + return _selectedItems; + } - if (_selectedItem != selectedItem) - { - RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem); - _selectedItem = selectedItem; - } - - if (selectedIndex != -1) - { - RaiseEvent(new SelectionChangedEventArgs( - SelectionChangedEvent, - Array.Empty(), - Selection.SelectedItems.ToList())); - } - } + set + { + if (value?.IsFixedSize == true || value?.IsReadOnly == true) + { + throw new NotSupportedException( + "Cannot use a fixed size or read-only collection as SelectedItems."); } + + UnsubscribeFromSelectedItems(); + _selectedItems = value ?? new AvaloniaList(); + SubscribeToSelectedItems(); } } @@ -374,18 +281,81 @@ namespace Avalonia.Controls.Primitives /// protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e) { + base.ItemsChanged(e); + if (_updateCount == 0) { - Selection.Source = e.NewValue; - } + var newIndex = -1; - base.ItemsChanged(e); + if (SelectedIndex != -1) + { + newIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); + } + + if (AlwaysSelected && Items != null && Items.Cast().Any()) + { + newIndex = 0; + } + + SelectedIndex = newIndex; + } } /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + if (_updateCount > 0) + { + base.ItemsCollectionChanged(sender, e); + return; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); + break; + case NotifyCollectionChangedAction.Remove: + _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); + break; + } + base.ItemsCollectionChanged(sender, e); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (AlwaysSelected && SelectedIndex == -1) + { + SelectedIndex = 0; + } + else + { + UpdateSelectedItem(_selection.First(), false); + } + + break; + + case NotifyCollectionChangedAction.Remove: + UpdateSelectedItem(_selection.First(), false); + ResetSelectedItems(); + break; + + case NotifyCollectionChangedAction.Replace: + UpdateSelectedItem(SelectedIndex, false); + ResetSelectedItems(); + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Reset: + SelectedIndex = IndexOf(Items, SelectedItem); + + if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) + { + SelectedIndex = 0; + } + break; + } } /// @@ -393,18 +363,36 @@ namespace Avalonia.Controls.Primitives { base.OnContainersMaterialized(e); + var resetSelectedItems = false; + foreach (var container in e.Containers) { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - Selection.Select(container.Index); + if (SelectionMode.HasFlag(SelectionMode.Multiple)) + { + if (_selection.Add(container.Index)) + { + resetSelectedItems = true; + } + } + else + { + SelectedIndex = container.Index; + } + MarkContainerSelected(container.ContainerControl, true); } - else if (Selection.IsSelected(container.Index) == true) + else if (_selection.Contains(container.Index)) { MarkContainerSelected(container.ContainerControl, true); } } + + if (resetSelectedItems) + { + ResetSelectedItems(); + } } /// @@ -433,7 +421,7 @@ namespace Avalonia.Controls.Primitives { if (i.ContainerControl != null && i.Item != null) { - bool selected = Selection.IsSelected(i.Index) == true; + bool selected = _selection.Contains(i.Index); MarkContainerSelected(i.ContainerControl, selected); } } @@ -455,18 +443,6 @@ namespace Avalonia.Controls.Primitives InternalEndInit(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == SelectionModeProperty) - { - var mode = change.NewValue.GetValueOrDefault(); - Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple); - Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected); - } - } - protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); @@ -481,7 +457,7 @@ namespace Avalonia.Controls.Primitives (((SelectionMode & SelectionMode.Multiple) != 0) || (SelectionMode & SelectionMode.Toggle) != 0)) { - Selection.SelectAll(); + SelectAll(); e.Handled = true; } } @@ -523,6 +499,36 @@ namespace Avalonia.Controls.Primitives return false; } + /// + /// Selects all items in the control. + /// + protected void SelectAll() + { + UpdateSelectedItems(() => + { + _selection.Clear(); + + for (var i = 0; i < ItemCount; ++i) + { + _selection.Add(i); + } + + UpdateSelectedItem(0, false); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected(container.Index, true); + } + + ResetSelectedItems(); + }); + } + + /// + /// Deselects all items in the control. + /// + protected void UnselectAll() => UpdateSelectedItem(-1); + /// /// Updates the selection for an item based on user interaction. /// @@ -549,35 +555,63 @@ namespace Avalonia.Controls.Primitives if (rightButton) { - if (Selection.IsSelected(index) == false) + if (!_selection.Contains(index)) { - SelectedIndex = index; + UpdateSelectedItem(index); } } else if (range) { - using var operation = Selection.Update(); - var anchor = Selection.AnchorIndex; - - if (anchor.GetSize() == 0) + UpdateSelectedItems(() => { - anchor = new IndexPath(0); - } + var start = SelectedIndex != -1 ? SelectedIndex : 0; + var step = start < index ? 1 : -1; + + _selection.Clear(); + + for (var i = start; i != index; i += step) + { + _selection.Add(i); + } + + _selection.Add(index); - Selection.ClearSelection(); - Selection.AnchorIndex = anchor; - Selection.SelectRangeFromAnchor(index); + var first = Math.Min(start, index); + var last = Math.Max(start, index); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected( + container.Index, + container.Index >= first && container.Index <= last); + } + + ResetSelectedItems(); + }); } else if (multi && toggle) { - if (Selection.IsSelected(index) == true) - { - Selection.Deselect(index); - } - else + UpdateSelectedItems(() => { - Selection.Select(index); - } + if (!_selection.Contains(index)) + { + _selection.Add(index); + MarkItemSelected(index, true); + SelectedItems.Add(ElementAt(Items, index)); + } + else + { + _selection.Remove(index); + MarkItemSelected(index, false); + + if (index == _selectedIndex) + { + UpdateSelectedItem(_selection.First(), false); + } + + SelectedItems.Remove(ElementAt(Items, index)); + } + }); } else if (toggle) { @@ -585,9 +619,7 @@ namespace Avalonia.Controls.Primitives } else { - using var operation = Selection.Update(); - Selection.ClearSelection(); - Selection.Select(index); + UpdateSelectedItem(index); } if (Presenter?.Panel != null) @@ -659,81 +691,6 @@ namespace Avalonia.Controls.Primitives return false; } - /// - /// Called when is raised. - /// - /// The sender. - /// The event args. - private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem) - { - if (Selection.AnchorIndex.GetSize() > 0) - { - ScrollIntoView(Selection.AnchorIndex.GetAt(0)); - } - } - } - - /// - /// Called when is raised. - /// - /// The sender. - /// The event args. - private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) - { - void Mark(int index, bool selected) - { - var container = ItemContainerGenerator.ContainerFromIndex(index); - - if (container != null) - { - MarkContainerSelected(container, selected); - } - } - - if (e.SelectedIndices.Count > 0 || e.DeselectedIndices.Count > 0) - { - foreach (var i in e.SelectedIndices) - { - Mark(i.GetAt(0), true); - } - - foreach (var i in e.DeselectedIndices) - { - Mark(i.GetAt(0), false); - } - } - else if (e.DeselectedItems.Count > 0) - { - // (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(); - } - - var newSelectedIndex = SelectedIndex; - var newSelectedItem = SelectedItem; - - if (newSelectedIndex != _selectedIndex) - { - RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, newSelectedIndex); - _selectedIndex = newSelectedIndex; - } - - if (newSelectedItem != _selectedItem) - { - RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem); - _selectedItem = newSelectedItem; - } - - var ev = new SelectionChangedEventArgs( - SelectionChangedEvent, - e.DeselectedItems.ToList(), - e.SelectedItems.ToList()); - RaiseEvent(ev); - } - /// /// Called when a container raises the . /// @@ -819,16 +776,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,10 +791,285 @@ 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) + { + var index = IndexOf(Items, item); + + if (index != -1) + { + MarkItemSelected(index, selected); + } + + return index; + } + + private void ResetSelectedItems() + { + UpdateSelectedItems(() => + { + SelectedItems.Clear(); + + foreach (var i in _selection) + { + SelectedItems.Add(ElementAt(Items, i)); + } + }); + } + + /// + /// Called when the CollectionChanged event is raised. + /// + /// The event sender. + /// The event args. + private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (_syncingSelectedItems) + { + return; + } + + void Add(IList newItems, IList addedItems = null) + { + foreach (var item in newItems) + { + var index = MarkItemSelected(item, true); + + if (index != -1 && _selection.Add(index) && addedItems != null) + { + addedItems.Add(item); + } + } + } + + void UpdateSelection() + { + if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) || + (SelectedIndex == -1 && _selection.HasItems)) + { + _selectedIndex = _selection.First(); + _selectedItem = ElementAt(Items, _selectedIndex); + RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue); + RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue); + } + } + + IList added = null; + IList removed = null; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + Add(e.NewItems); + UpdateSelection(); + added = e.NewItems; + } + + break; + + case NotifyCollectionChangedAction.Remove: + if (SelectedItems.Count == 0) + { + SelectedIndex = -1; + } + + foreach (var item in e.OldItems) + { + var index = MarkItemSelected(item, false); + _selection.Remove(index); + } + + removed = e.OldItems; + break; + + case NotifyCollectionChangedAction.Replace: + throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported."); + + case NotifyCollectionChangedAction.Move: + throw new NotSupportedException("Moving items in a SelectedItems collection is not supported."); + + case NotifyCollectionChangedAction.Reset: + { + removed = new List(); + added = new List(); + + foreach (var index in _selection.ToList()) + { + var item = ElementAt(Items, index); + + if (!SelectedItems.Contains(item)) + { + MarkItemSelected(index, false); + removed.Add(item); + _selection.Remove(index); + } + } + + Add(SelectedItems, added); + UpdateSelection(); + } + + break; + } + + if (added?.Count > 0 || removed?.Count > 0) + { + var changed = new SelectionChangedEventArgs( + SelectionChangedEvent, + removed ?? Empty, + added ?? Empty); + RaiseEvent(changed); + } + } + + /// + /// Subscribes to the CollectionChanged event, if any. + /// + private void SubscribeToSelectedItems() + { + var incc = _selectedItems as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged += SelectedItemsCollectionChanged; + } + + SelectedItemsCollectionChanged( + _selectedItems, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + /// Unsubscribes from the CollectionChanged event, if any. + /// + private void UnsubscribeFromSelectedItems() + { + var incc = _selectedItems as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged -= SelectedItemsCollectionChanged; + } + } + + /// + /// Updates the selection due to a change to or + /// . + /// + /// The new selected index. + /// Whether to clear existing selection. + private void UpdateSelectedItem(int index, bool clear = true) + { + var oldIndex = _selectedIndex; + var oldItem = _selectedItem; + + if (index == -1 && AlwaysSelected) + { + index = Math.Min(SelectedIndex, ItemCount - 1); + } + + var item = ElementAt(Items, index); + var itemChanged = !Equals(item, oldItem); + var added = -1; + HashSet removed = null; + + _selectedIndex = index; + _selectedItem = item; + + if (oldIndex != index || itemChanged || _selection.HasMultiple) + { + if (clear) + { + removed = _selection.Clear(); + } + + if (index != -1) + { + if (_selection.Add(index)) + { + added = index; + } + + if (removed?.Contains(index) == true) + { + removed.Remove(index); + added = -1; + } + } + + if (removed != null) + { + foreach (var i in removed) + { + MarkItemSelected(i, false); + } + } + + MarkItemSelected(index, true); + + RaisePropertyChanged( + SelectedIndexProperty, + oldIndex, + index); + } + + if (itemChanged) + { + RaisePropertyChanged( + SelectedItemProperty, + oldItem, + item); + } + + if (removed != null && index != -1) + { + removed.Remove(index); + } + + if (added != -1 || removed?.Count > 0) + { + ResetSelectedItems(); + + var e = new SelectionChangedEventArgs( + SelectionChangedEvent, + removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty(), + added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty()); + RaiseEvent(e); + } + + if (AutoScrollToSelectedItem && _selectedIndex != -1) + { + ScrollIntoView(_selectedItem); + } + } + + private void UpdateSelectedItems(Action action) { - Selection.Source = Items; + try + { + _syncingSelectedItems = true; + action(); + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Property)?.Log( + this, + "Error thrown updating SelectedItems: {Error}", + ex); + } + finally + { + _syncingSelectedItems = false; + } + } + private void UpdateFinished() + { if (_updateSelectedItem != null) { SelectedItem = _updateSelectedItem; @@ -892,5 +1114,104 @@ namespace Avalonia.Controls.Primitives UpdateFinished(); } } + + private class Selection : IEnumerable + { + private readonly List _list = new List(); + private HashSet _set = new HashSet(); + + public bool HasItems => _set.Count > 0; + public bool HasMultiple => _set.Count > 1; + + public bool Add(int index) + { + if (index == -1) + { + throw new ArgumentException("Invalid index", "index"); + } + + if (_set.Add(index)) + { + _list.Add(index); + return true; + } + + return false; + } + + public bool Remove(int index) + { + if (_set.Remove(index)) + { + _list.RemoveAll(x => x == index); + return true; + } + + return false; + } + + public HashSet Clear() + { + var result = _set; + _list.Clear(); + _set = new HashSet(); + return result; + } + + public void ItemsInserted(int index, int count) + { + _set = new HashSet(); + + for (var i = 0; i < _list.Count; ++i) + { + var ix = _list[i]; + + if (ix >= index) + { + var newIndex = ix + count; + _list[i] = newIndex; + _set.Add(newIndex); + } + else + { + _set.Add(ix); + } + } + } + + public void ItemsRemoved(int index, int count) + { + var last = (index + count) - 1; + + _set = new HashSet(); + + for (var i = 0; i < _list.Count; ++i) + { + var ix = _list[i]; + + if (ix >= index && ix <= last) + { + _list.RemoveAt(i--); + } + else if (ix > last) + { + var newIndex = ix - count; + _list[i] = newIndex; + _set.Add(newIndex); + } + else + { + _set.Add(ix); + } + } + } + + public bool Contains(int index) => _set.Contains(index); + + public int First() => HasItems ? _list[0] : -1; + + public IEnumerator GetEnumerator() => _set.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } } 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/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? selectedIndices, - IReadOnlyList? deselectedItems, - IReadOnlyList? selectedItems) - { - DeselectedIndices = deselectedIndices ?? Array.Empty(); - SelectedIndices = selectedIndices ?? Array.Empty(); - DeselectedItems = deselectedItems ?? Array.Empty(); - SelectedItems= selectedItems ?? Array.Empty(); - } - - /// - /// Gets the indices of the items that were removed from the selection. - /// - public IReadOnlyList DeselectedIndices { get; } - - /// - /// Gets the indices of the items that were added to the selection. - /// - public IReadOnlyList SelectedIndices { get; } - - /// - /// Gets the items that were removed from the selection. - /// - public IReadOnlyList DeselectedItems { get; } - - /// - /// Gets the items that were added to the selection. - /// - public IReadOnlyList SelectedItems { get; } - } -} diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs deleted file mode 100644 index d99606673e..0000000000 --- a/src/Avalonia.Controls/SelectionNode.cs +++ /dev/null @@ -1,971 +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; -using System.Collections.Specialized; -using System.Linq; -using Avalonia.Controls.Utils; - -#nullable enable - -namespace Avalonia.Controls -{ - /// - /// Tracks nested selection. - /// - /// - /// SelectionNode is the internal tree data structure that we keep track of for selection in - /// a nested scenario. This would map to one ItemsSourceView/Collection. This node reacts to - /// collection changes and keeps the selected indices up to date. This can either be a leaf - /// node or a non leaf node. - /// - internal class SelectionNode : IDisposable - { - private readonly SelectionModel _manager; - private readonly List _childrenNodes = new List(); - private readonly SelectionNode? _parent; - private readonly List _selected = new List(); - private readonly List _selectedIndicesCached = new List(); - private IDisposable? _childrenSubscription; - private SelectionNodeOperation? _operation; - private object? _source; - private bool _selectedIndicesCacheIsValid; - private bool _retainSelectionOnReset; - private List? _selectedItems; - - public SelectionNode(SelectionModel manager, SelectionNode? parent) - { - _manager = manager; - _parent = parent; - } - - public int AnchorIndex { get; set; } = -1; - - public bool RetainSelectionOnReset - { - get => _retainSelectionOnReset; - set - { - if (_retainSelectionOnReset != value) - { - _retainSelectionOnReset = value; - - if (_retainSelectionOnReset) - { - _selectedItems = new List(); - PopulateSelectedItemsFromSelectedIndices(); - } - else - { - _selectedItems = null; - } - - foreach (var child in _childrenNodes) - { - if (child != null) - { - child.RetainSelectionOnReset = value; - } - } - } - } - } - - public object? Source - { - get => _source; - set - { - if (_source != value) - { - if (_source != null) - { - ClearSelection(); - ClearChildNodes(); - UnhookCollectionChangedHandler(); - } - - _source = value; - - // Setup ItemsSourceView - var newDataSource = value as ItemsSourceView; - - if (value != null && newDataSource == null) - { - newDataSource = new ItemsSourceView((IEnumerable)value); - } - - ItemsSourceView = newDataSource; - - TrimInvalidSelections(); - PopulateSelectedItemsFromSelectedIndices(); - HookupCollectionChangedHandler(); - OnSelectionChanged(); - } - } - } - - private void TrimInvalidSelections() - { - if (_selected == null || ItemsSourceView == null) - { - return; - } - - var validRange = ItemsSourceView.Count > 0 ? new IndexRange(0, ItemsSourceView.Count - 1) : new IndexRange(-1, -1); - var removed = new List(); - var removedCount = IndexRange.Intersect(_selected, validRange, removed); - - if (removedCount > 0) - { - using var operation = _manager.Update(); - SelectedCount -= removedCount; - OnSelectionChanged(); - _operation!.Deselected(removed); - } - } - - public ItemsSourceView? ItemsSourceView { get; private set; } - public int DataCount => ItemsSourceView?.Count ?? 0; - public int ChildrenNodeCount => _childrenNodes.Count; - public int RealizedChildrenNodeCount { get; private set; } - - public IndexPath IndexPath - { - get - { - var path = new List(); ; - var parent = _parent; - var child = this; - - while (parent != null) - { - var childNodes = parent._childrenNodes; - var index = childNodes.IndexOf(child); - - // We are walking up to the parent, so the path will be backwards - path.Insert(0, index); - child = parent; - parent = parent._parent; - } - - return new IndexPath(path); - } - } - - // For a genuine tree view, we dont know which node is leaf until we - // actually walk to it, so currently the tree builds up to the leaf. I don't - // create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid - // an explosion of node objects. However, I'm still creating the m_childrenNodes - // collection unfortunately. - public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath) - { - SelectionNode? child = null; - - if (realizeChild) - { - if (ItemsSourceView == null || index < 0 || index >= ItemsSourceView.Count) - { - throw new IndexOutOfRangeException(); - } - - if (_childrenNodes.Count == 0) - { - if (ItemsSourceView != null) - { - for (int i = 0; i < ItemsSourceView.Count; i++) - { - _childrenNodes.Add(null); - } - } - } - - if (_childrenNodes[index] == null) - { - var childData = ItemsSourceView!.GetAt(index); - IObservable? resolver = null; - - if (childData != null) - { - var childDataIndexPath = IndexPath.CloneWithChildIndex(index); - resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath); - } - - if (resolver != null) - { - child = new SelectionNode(_manager, parent: this); - child.SetChildrenObservable(resolver); - } - else if (childData is IEnumerable || childData is IList) - { - child = new SelectionNode(_manager, parent: this); - child.Source = childData; - } - else - { - child = _manager.SharedLeafNode; - } - - if (_operation != null && child != _manager.SharedLeafNode) - { - child.BeginOperation(); - } - - _childrenNodes[index] = child; - RealizedChildrenNodeCount++; - } - else - { - child = _childrenNodes[index]; - } - } - else - { - if (_childrenNodes.Count > 0) - { - child = _childrenNodes[index]; - } - } - - return child; - } - - public void SetChildrenObservable(IObservable resolver) - { - _childrenSubscription = resolver.Subscribe(x => - { - if (Source != null) - { - using (_manager.Update()) - { - SelectionTreeHelper.Traverse( - this, - realizeChildren: false, - info => info.Node.Clear()); - } - } - - Source = x; - }); - } - - public int SelectedCount { get; private set; } - - public bool IsSelected(int index) - { - var isSelected = false; - - foreach (var range in _selected) - { - if (range.Contains(index)) - { - isSelected = true; - break; - } - } - - return isSelected; - } - - // True -> Selected - // False -> Not Selected - // Null -> Some descendents are selected and some are not - public bool? IsSelectedWithPartial() - { - var isSelected = (bool?)false; - - if (_parent != null) - { - var parentsChildren = _parent._childrenNodes; - - var myIndexInParent = parentsChildren.IndexOf(this); - - if (myIndexInParent != -1) - { - isSelected = _parent.IsSelectedWithPartial(myIndexInParent); - } - } - - return isSelected; - } - - // True -> Selected - // False -> Not Selected - // Null -> Some descendents are selected and some are not - public bool? IsSelectedWithPartial(int index) - { - SelectionState selectionState; - - if (_childrenNodes.Count == 0 || // no nodes realized - _childrenNodes.Count <= index || // target node is not realized - _childrenNodes[index] == null || // target node is not realized - _childrenNodes[index] == _manager.SharedLeafNode) // target node is a leaf node. - { - // Ask parent if the target node is selected. - selectionState = IsSelected(index) ? SelectionState.Selected : SelectionState.NotSelected; - } - else - { - // targetNode is the node representing the index. This node is the parent. - // targetNode is a non-leaf node, containing one or many children nodes. Evaluate - // based on children of targetNode. - var targetNode = _childrenNodes[index]; - selectionState = targetNode!.EvaluateIsSelectedBasedOnChildrenNodes(); - } - - return ConvertToNullableBool(selectionState); - } - - public int SelectedIndex - { - get => SelectedCount > 0 ? SelectedIndices[0] : -1; - set - { - if (IsValidIndex(value) && (SelectedCount != 1 || !IsSelected(value))) - { - ClearSelection(); - - if (value != -1) - { - Select(value, true); - } - } - } - } - - public List SelectedIndices - { - get - { - if (!_selectedIndicesCacheIsValid) - { - _selectedIndicesCacheIsValid = true; - - foreach (var range in _selected) - { - for (int index = range.Begin; index <= range.End; index++) - { - // Avoid duplicates - if (!_selectedIndicesCached.Contains(index)) - { - _selectedIndicesCached.Add(index); - } - } - } - - // Sort the list for easy consumption - _selectedIndicesCached.Sort(); - } - - return _selectedIndicesCached; - } - } - - public IEnumerable SelectedItems - { - get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x)); - } - - public void Dispose() - { - _childrenSubscription?.Dispose(); - ItemsSourceView?.Dispose(); - ClearChildNodes(); - UnhookCollectionChangedHandler(); - } - - public void BeginOperation() - { - if (_operation != null) - { - throw new AvaloniaInternalException("Selection operation already in progress."); - } - - _operation = new SelectionNodeOperation(this); - - for (var i = 0; i < _childrenNodes.Count; ++i) - { - var child = _childrenNodes[i]; - - if (child != null && child != _manager.SharedLeafNode) - { - child.BeginOperation(); - } - } - } - - public void EndOperation(List changes) - { - if (_operation == null) - { - throw new AvaloniaInternalException("No selection operation in progress."); - } - - if (_operation.HasChanges) - { - changes.Add(_operation); - } - - _operation = null; - - for (var i = 0; i < _childrenNodes.Count; ++i) - { - var child = _childrenNodes[i]; - - if (child != null && child != _manager.SharedLeafNode) - { - child.EndOperation(changes); - } - } - } - - public bool Cleanup() - { - var result = SelectedCount == 0; - - for (var i = 0; i < _childrenNodes.Count; ++i) - { - var child = _childrenNodes[i]; - - if (child != null) - { - if (child.Cleanup()) - { - child.Dispose(); - _childrenNodes[i] = null; - } - else - { - result = false; - } - } - } - - return result; - } - - public bool Select(int index, bool select) - { - return Select(index, select, raiseOnSelectionChanged: true); - } - - public bool ToggleSelect(int index) - { - return Select(index, !IsSelected(index)); - } - - public void SelectAll() - { - if (ItemsSourceView != null) - { - var size = ItemsSourceView.Count; - - if (size > 0) - { - SelectRange(new IndexRange(0, size - 1), select: true); - } - } - } - - public void Clear() => ClearSelection(); - - public bool SelectRange(IndexRange range, bool select) - { - if (IsValidIndex(range.Begin) && IsValidIndex(range.End)) - { - if (select) - { - AddRange(range, raiseOnSelectionChanged: true); - } - else - { - RemoveRange(range, raiseOnSelectionChanged: true); - } - - return true; - } - - return false; - } - - private void HookupCollectionChangedHandler() - { - if (ItemsSourceView != null) - { - ItemsSourceView.CollectionChanged += OnSourceListChanged; - } - } - - private void UnhookCollectionChangedHandler() - { - if (ItemsSourceView != null) - { - ItemsSourceView.CollectionChanged -= OnSourceListChanged; - } - } - - private bool IsValidIndex(int index) - { - return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count); - } - - private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged) - { - var selected = new List(); - - SelectedCount += IndexRange.Add(_selected, addRange, selected); - - if (selected.Count > 0) - { - _operation?.Selected(selected); - - if (_selectedItems != null && ItemsSourceView != null) - { - for (var i = addRange.Begin; i <= addRange.End; ++i) - { - _selectedItems.Add(ItemsSourceView!.GetAt(i)); - } - } - - if (raiseOnSelectionChanged) - { - OnSelectionChanged(); - } - } - } - - private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged) - { - var removed = new List(); - - SelectedCount -= IndexRange.Remove(_selected, removeRange, removed); - - if (removed.Count > 0) - { - _operation?.Deselected(removed); - - if (_selectedItems != null) - { - for (var i = removeRange.Begin; i <= removeRange.End; ++i) - { - _selectedItems.Remove(ItemsSourceView!.GetAt(i)); - } - } - - if (raiseOnSelectionChanged) - { - OnSelectionChanged(); - } - } - } - - private void ClearSelection() - { - // Deselect all items - if (_selected.Count > 0) - { - _operation?.Deselected(_selected); - _selected.Clear(); - OnSelectionChanged(); - } - - _selectedItems?.Clear(); - SelectedCount = 0; - AnchorIndex = -1; - } - - private void ClearChildNodes() - { - for (int i = 0; i < _childrenNodes.Count; i++) - { - var child = _childrenNodes[i]; - - if (child != null && child != _manager.SharedLeafNode) - { - child.Dispose(); - _childrenNodes[i] = null; - } - } - - RealizedChildrenNodeCount = 0; - } - - private bool Select(int index, bool select, bool raiseOnSelectionChanged) - { - if (IsValidIndex(index)) - { - // Ignore duplicate selection calls - if (IsSelected(index) == select) - { - return true; - } - - var range = new IndexRange(index, index); - - if (select) - { - AddRange(range, raiseOnSelectionChanged); - } - else - { - RemoveRange(range, raiseOnSelectionChanged); - } - - return true; - } - - return false; - } - - private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args) - { - bool selectionInvalidated = false; - List? removed = null; - - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - { - selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); - break; - } - - case NotifyCollectionChangedAction.Remove: - { - (selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems); - break; - } - - case NotifyCollectionChangedAction.Reset: - { - if (_selectedItems == null) - { - ClearSelection(); - } - else - { - removed = RecreateSelectionFromSelectedItems(); - } - - selectionInvalidated = true; - break; - } - - case NotifyCollectionChangedAction.Replace: - { - (selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems); - selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); - break; - } - } - - if (selectionInvalidated) - { - OnSelectionChanged(); - } - - _manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed); - } - - private bool OnItemsAdded(int index, int count) - { - var selectionInvalidated = false; - - // Update ranges for leaf items - var toAdd = new List(); - - for (int i = 0; i < _selected.Count; i++) - { - var range = _selected[i]; - - // The range is after the inserted items, need to shift the range right - if (range.End >= index) - { - int begin = range.Begin; - - // If the index left of newIndex is inside the range, - // Split the range and remember the left piece to add later - if (range.Contains(index - 1)) - { - range.Split(index - 1, out var before, out _); - toAdd.Add(before); - begin = index; - } - - // Shift the range to the right - _selected[i] = new IndexRange(begin + count, range.End + count); - selectionInvalidated = true; - } - } - - // Add the left sides of the split ranges - _selected.AddRange(toAdd); - - // Update for non-leaf if we are tracking non-leaf nodes - if (_childrenNodes.Count > 0) - { - selectionInvalidated = true; - for (int i = 0; i < count; i++) - { - _childrenNodes.Insert(index, null); - } - } - - // Adjust the anchor - if (AnchorIndex >= index) - { - AnchorIndex += count; - } - - // Check if adding a node invalidated an ancestors - // selection state. For example if parent was selected before - // adding a new item makes the parent partially selected now. - if (!selectionInvalidated) - { - var parent = _parent; - - while (parent != null) - { - var isSelected = parent.IsSelectedWithPartial(); - - // If a parent is selected, then it will become partially selected. - // If it is not selected or partially selected - there is no change. - if (isSelected == true) - { - selectionInvalidated = true; - break; - } - - parent = parent._parent; - } - } - - return selectionInvalidated; - } - - private (bool, List) OnItemsRemoved(int index, IList items) - { - var selectionInvalidated = false; - var removed = new List(); - var count = items.Count; - var isSelected = false; - - for (int i = 0; i <= count - 1; i++) - { - if (IsSelected(index + i)) - { - isSelected = true; - removed.Add(items[i]); - } - } - - if (isSelected) - { - var removeRange = new IndexRange(index, index + count - 1); - SelectedCount -= IndexRange.Remove(_selected, removeRange); - selectionInvalidated = true; - - if (_selectedItems != null) - { - foreach (var i in items) - { - _selectedItems.Remove(i); - } - } - } - - for (int i = 0; i < _selected.Count; i++) - { - var range = _selected[i]; - - // The range is after the removed items, need to shift the range left - if (range.End > index) - { - // Shift the range to the left - _selected[i] = new IndexRange(range.Begin - count, range.End - count); - selectionInvalidated = true; - } - } - - // Update for non-leaf if we are tracking non-leaf nodes - if (_childrenNodes.Count > 0) - { - selectionInvalidated = true; - for (int i = 0; i < count; i++) - { - if (_childrenNodes[index] != null) - { - removed.AddRange(_childrenNodes[index]!.SelectedItems); - RealizedChildrenNodeCount--; - _childrenNodes[index]!.Dispose(); - } - _childrenNodes.RemoveAt(index); - } - } - - //Adjust the anchor - if (AnchorIndex >= index) - { - AnchorIndex -= count; - } - - return (selectionInvalidated, removed); - } - - private void OnSelectionChanged() - { - _selectedIndicesCacheIsValid = false; - _selectedIndicesCached.Clear(); - } - - public static bool? ConvertToNullableBool(SelectionState isSelected) - { - bool? result = null; // PartialySelected - - if (isSelected == SelectionState.Selected) - { - result = true; - } - else if (isSelected == SelectionState.NotSelected) - { - result = false; - } - - return result; - } - - public SelectionState EvaluateIsSelectedBasedOnChildrenNodes() - { - var selectionState = SelectionState.NotSelected; - int realizedChildrenNodeCount = RealizedChildrenNodeCount; - int selectedCount = SelectedCount; - - if (realizedChildrenNodeCount != 0 || selectedCount != 0) - { - // There are realized children or some selected leaves. - int dataCount = DataCount; - if (realizedChildrenNodeCount == 0 && selectedCount > 0) - { - // All nodes are leaves under it - we didn't create children nodes as an optimization. - // See if all/some or none of the leaves are selected. - selectionState = dataCount != selectedCount ? - SelectionState.PartiallySelected : - dataCount == selectedCount ? SelectionState.Selected : SelectionState.NotSelected; - } - else - { - // There are child nodes, walk them individually and evaluate based on each child - // being selected/not selected or partially selected. - selectedCount = 0; - int notSelectedCount = 0; - for (int i = 0; i < ChildrenNodeCount; i++) - { - var child = GetAt(i, false, default); - - if (child != null) - { - // child is realized, ask it. - var isChildSelected = IsSelectedWithPartial(i); - if (isChildSelected == null) - { - selectionState = SelectionState.PartiallySelected; - break; - } - else if (isChildSelected == true) - { - selectedCount++; - } - else - { - notSelectedCount++; - } - } - else - { - // not realized. - if (IsSelected(i)) - { - selectedCount++; - } - else - { - notSelectedCount++; - } - } - - if (selectedCount > 0 && notSelectedCount > 0) - { - selectionState = SelectionState.PartiallySelected; - break; - } - } - - if (selectionState != SelectionState.PartiallySelected) - { - if (selectedCount != 0 && selectedCount != dataCount) - { - selectionState = SelectionState.PartiallySelected; - } - else - { - selectionState = selectedCount == dataCount ? SelectionState.Selected : SelectionState.NotSelected; - } - } - } - } - - return selectionState; - } - - private void PopulateSelectedItemsFromSelectedIndices() - { - if (_selectedItems != null) - { - _selectedItems.Clear(); - - foreach (var i in SelectedIndices) - { - _selectedItems.Add(ItemsSourceView!.GetAt(i)); - } - } - } - - private List RecreateSelectionFromSelectedItems() - { - var removed = new List(); - - _selected.Clear(); - SelectedCount = 0; - - for (var i = 0; i < _selectedItems!.Count; ++i) - { - var item = _selectedItems[i]; - var index = ItemsSourceView!.IndexOf(item); - - if (index != -1) - { - IndexRange.Add(_selected, new IndexRange(index, index)); - ++SelectedCount; - } - else - { - removed.Add(item); - _selectedItems.RemoveAt(i--); - } - } - - return removed; - } - - public enum SelectionState - { - Selected, - NotSelected, - PartiallySelected - } - } -} diff --git a/src/Avalonia.Controls/SelectionNodeOperation.cs b/src/Avalonia.Controls/SelectionNodeOperation.cs deleted file mode 100644 index 9622a52f00..0000000000 --- a/src/Avalonia.Controls/SelectionNodeOperation.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -#nullable enable - -namespace Avalonia.Controls -{ - internal class SelectionNodeOperation : ISelectedItemInfo - { - private readonly SelectionNode _owner; - private List? _selected; - private List? _deselected; - private int _selectedCount = -1; - private int _deselectedCount = -1; - - public SelectionNodeOperation(SelectionNode owner) - { - _owner = owner; - } - - public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0; - public List? SelectedRanges => _selected; - public List? DeselectedRanges => _deselected; - public IndexPath Path => _owner.IndexPath; - public ItemsSourceView? Items => _owner.ItemsSourceView; - - public int SelectedCount - { - get - { - if (_selectedCount == -1) - { - _selectedCount = (_selected != null) ? IndexRange.GetCount(_selected) : 0; - } - - return _selectedCount; - } - } - - public int DeselectedCount - { - get - { - if (_deselectedCount == -1) - { - _deselectedCount = (_deselected != null) ? IndexRange.GetCount(_deselected) : 0; - } - - return _deselectedCount; - } - } - - public void Selected(IndexRange range) - { - Add(range, ref _selected, _deselected); - _selectedCount = -1; - } - - public void Selected(IEnumerable ranges) - { - foreach (var range in ranges) - { - Selected(range); - } - } - - public void Deselected(IndexRange range) - { - Add(range, ref _deselected, _selected); - _deselectedCount = -1; - } - - public void Deselected(IEnumerable ranges) - { - foreach (var range in ranges) - { - Deselected(range); - } - } - - private static void Add( - IndexRange range, - ref List? add, - List? remove) - { - if (remove != null) - { - var removed = new List(); - IndexRange.Remove(remove, range, removed); - var selected = IndexRange.Subtract(range, removed); - - if (selected.Any()) - { - add ??= new List(); - - foreach (var r in selected) - { - IndexRange.Add(add, r); - } - } - } - else - { - add ??= new List(); - IndexRange.Add(add, range); - } - } - } -} diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index a91655855c..b4c30e0149 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -2,9 +2,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Reactive.Linq; +using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; @@ -44,29 +46,16 @@ namespace Avalonia.Controls o => o.SelectedItems, (o, v) => o.SelectedItems = v); - /// - /// Defines the property. - /// - public static readonly DirectProperty SelectionProperty = - SelectingItemsControl.SelectionProperty.AddOwner( - o => o.Selection, - (o, v) => o.Selection = v); - /// /// Defines the property. /// public static readonly StyledProperty SelectionModeProperty = ListBox.SelectionModeProperty.AddOwner(); - /// - /// Defines the property. - /// - public static RoutedEvent SelectionChangedEvent = - SelectingItemsControl.SelectionChangedEvent; - + private static readonly IList Empty = Array.Empty(); private object _selectedItem; - private ISelectionModel _selection; - private readonly SelectedItemsSync _selectedItems; + private IList _selectedItems; + private bool _syncingSelectedItems; /// /// Initializes static members of the class. @@ -76,13 +65,6 @@ namespace Avalonia.Controls // HACK: Needed or SelectedItem property will not be found in Release build. } - public TreeView() - { - // Setting Selection to null causes a default SelectionModel to be created. - Selection = null; - _selectedItems = new SelectedItemsSync(Selection); - } - /// /// Occurs when the control's selection changes. /// @@ -125,94 +107,56 @@ namespace Avalonia.Controls /// public object SelectedItem { - get => Selection.SelectedItem; - set => Selection.SelectedIndex = IndexFromItem(value); - } + get => _selectedItem; + set + { + var selectedItems = SelectedItems; - /// - /// Gets or sets the selected items. - /// - protected IList SelectedItems - { - get => _selectedItems.GetOrCreateItems(); - set => _selectedItems.SetItems(value); + SetAndRaise(SelectedItemProperty, ref _selectedItem, value); + + if (value != null) + { + if (selectedItems.Count != 1 || selectedItems[0] != value) + { + _syncingSelectedItems = true; + SelectSingleItem(value); + _syncingSelectedItems = false; + } + } + else if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); + } + } } /// - /// Gets or sets a model holding the current selection. + /// Gets or sets the selected items. /// - public ISelectionModel Selection + public IList SelectedItems { - get => _selection; - set + get { - value ??= new SelectionModel + if (_selectedItems == null) { - SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), - AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected), - RetainSelectionOnReset = true, - }; - - 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) - { - oldSelection = Selection.SelectedItems.ToList(); - _selection.PropertyChanged -= OnSelectionModelPropertyChanged; - _selection.SelectionChanged -= OnSelectionModelSelectionChanged; - _selection.ChildrenRequested -= OnSelectionModelChildrenRequested; - MarkContainersUnselected(); - } - - _selection = value; - - if (_selection != null) - { - _selection.Source = Items; - _selection.PropertyChanged += OnSelectionModelPropertyChanged; - _selection.SelectionChanged += OnSelectionModelSelectionChanged; - _selection.ChildrenRequested += OnSelectionModelChildrenRequested; - - if (_selection.SingleSelect) - { - SelectionMode &= ~SelectionMode.Multiple; - } - else - { - SelectionMode |= SelectionMode.Multiple; - } - - if (_selection.AutoSelect) - { - SelectionMode |= SelectionMode.AlwaysSelected; - } - else - { - SelectionMode &= ~SelectionMode.AlwaysSelected; - } - - UpdateContainerSelection(); + _selectedItems = new AvaloniaList(); + SubscribeToSelectedItems(); + } - var selectedItem = SelectedItem; + return _selectedItems; + } - if (_selectedItem != selectedItem) - { - RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem); - _selectedItem = selectedItem; - } - } + set + { + if (value?.IsFixedSize == true || value?.IsReadOnly == true) + { + throw new NotSupportedException( + "Cannot use a fixed size or read-only collection as SelectedItems."); } + + UnsubscribeFromSelectedItems(); + _selectedItems = value ?? new AvaloniaList(); + SubscribeToSelectedItems(); } } @@ -245,13 +189,186 @@ namespace Avalonia.Controls /// Note that this method only selects nodes currently visible due to their parent nodes /// being expanded: it does not expand nodes. /// - public void SelectAll() => Selection.SelectAll(); + public void SelectAll() + { + SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + } /// /// Deselects all items in the . /// - public void UnselectAll() => Selection.ClearSelection(); + public void UnselectAll() + { + SelectedItems.Clear(); + } + + /// + /// Subscribes to the CollectionChanged event, if any. + /// + private void SubscribeToSelectedItems() + { + if (_selectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged += SelectedItemsCollectionChanged; + } + + SelectedItemsCollectionChanged( + _selectedItems, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + private void SelectSingleItem(object item) + { + SelectedItems.Clear(); + SelectedItems.Add(item); + } + + /// + /// Called when the CollectionChanged event is raised. + /// + /// The event sender. + /// The event args. + private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + IList added = null; + IList removed = null; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + + SelectedItemsAdded(e.NewItems.Cast().ToArray()); + + if (AutoScrollToSelectedItem) + { + var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]); + + container?.BringIntoView(); + } + + added = e.NewItems; + + break; + case NotifyCollectionChangedAction.Remove: + + if (!_syncingSelectedItems) + { + if (SelectedItems.Count == 0) + { + SelectedItem = null; + } + else + { + var selectedIndex = SelectedItems.IndexOf(_selectedItem); + + if (selectedIndex == -1) + { + var old = _selectedItem; + _selectedItem = SelectedItems[0]; + + RaisePropertyChanged(SelectedItemProperty, old, _selectedItem); + } + } + } + + foreach (var item in e.OldItems) + { + MarkItemSelected(item, false); + } + + removed = e.OldItems; + + break; + case NotifyCollectionChangedAction.Reset: + + foreach (IControl container in ItemContainerGenerator.Index.Containers) + { + MarkContainerSelected(container, false); + } + + if (SelectedItems.Count > 0) + { + SelectedItemsAdded(SelectedItems); + + added = SelectedItems; + } + else if (!_syncingSelectedItems) + { + SelectedItem = null; + } + + break; + case NotifyCollectionChangedAction.Replace: + + foreach (var item in e.OldItems) + { + MarkItemSelected(item, false); + } + + foreach (var item in e.NewItems) + { + MarkItemSelected(item, true); + } + + if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems) + { + var oldItem = SelectedItem; + var item = SelectedItems[0]; + _selectedItem = item; + RaisePropertyChanged(SelectedItemProperty, oldItem, item); + } + + added = e.NewItems; + removed = e.OldItems; + + break; + } + + if (added?.Count > 0 || removed?.Count > 0) + { + var changed = new SelectionChangedEventArgs( + SelectingItemsControl.SelectionChangedEvent, + removed ?? Empty, + added ?? Empty); + RaiseEvent(changed); + } + } + + private void MarkItemSelected(object item, bool selected) + { + var container = ItemContainerGenerator.Index.ContainerFromItem(item); + + MarkContainerSelected(container, selected); + } + + private void SelectedItemsAdded(IList items) + { + if (items.Count == 0) + { + return; + } + + foreach (object item in items) + { + MarkItemSelected(item, true); + } + + if (SelectedItem == null && !_syncingSelectedItems) + { + SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]); + } + } + + /// + /// Unsubscribes from the CollectionChanged event, if any. + /// + private void UnsubscribeFromSelectedItems() + { + if (_selectedItems is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= SelectedItemsCollectionChanged; + } + } (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction) { @@ -334,86 +451,6 @@ namespace Avalonia.Controls } } - /// - /// Called when is raised. - /// - /// The sender. - /// The event args. - private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem) - { - var container = ContainerFromIndex(Selection.AnchorIndex); - - if (container != null) - { - DispatcherTimer.RunOnce(container.BringIntoView, TimeSpan.Zero); - } - } - } - - /// - /// Called when is raised. - /// - /// The sender. - /// The event args. - private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) - { - void Mark(IndexPath index, bool selected) - { - var container = ContainerFromIndex(index); - - if (container != null) - { - MarkContainerSelected(container, selected); - } - } - - foreach (var i in e.SelectedIndices) - { - Mark(i, true); - } - - foreach (var i in e.DeselectedIndices) - { - Mark(i, false); - } - - var newSelectedItem = SelectedItem; - - if (newSelectedItem != _selectedItem) - { - RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem); - _selectedItem = newSelectedItem; - } - - var ev = new SelectionChangedEventArgs( - SelectionChangedEvent, - e.DeselectedItems.ToList(), - e.SelectedItems.ToList()); - RaiseEvent(ev); - } - - private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e) - { - var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as TreeViewItem; - - if (container is object) - { - if (e.SourceIndex.IsAncestorOf(e.FinalIndex)) - { - container.IsExpanded = true; - container.ApplyTemplate(); - container.Presenter?.ApplyTemplate(); - } - - e.Children = Observable.CombineLatest( - container.GetObservable(TreeViewItem.IsExpandedProperty), - container.GetObservable(ItemsProperty), - (expanded, items) => expanded ? items : null); - } - } - private TreeViewItem GetContainerInDirection( TreeViewItem from, NavigationDirection direction, @@ -467,12 +504,6 @@ namespace Avalonia.Controls return result; } - protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e) - { - Selection.Source = Items; - base.ItemsChanged(e); - } - /// protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -494,18 +525,6 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == SelectionModeProperty) - { - var mode = change.NewValue.GetValueOrDefault(); - Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple); - Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected); - } - } - /// /// Updates the selection for an item based on user interaction. /// @@ -521,9 +540,9 @@ namespace Avalonia.Controls bool toggleModifier = false, bool rightButton = false) { - var index = IndexFromContainer((TreeViewItem)container); + var item = ItemContainerGenerator.Index.ItemFromContainer(container); - if (index.GetSize() == 0) + if (item == null) { return; } @@ -540,48 +559,41 @@ namespace Avalonia.Controls var multi = (mode & SelectionMode.Multiple) != 0; var range = multi && selectedContainer != null && rangeModifier; - if (!select) + if (rightButton) { - Selection.DeselectAt(index); - } - else if (rightButton) - { - if (!Selection.IsSelectedAt(index)) + if (!SelectedItems.Contains(item)) { - Selection.SelectedIndex = index; + SelectSingleItem(item); } } else if (!toggle && !range) { - Selection.SelectedIndex = index; + SelectSingleItem(item); } else if (multi && range) { - using var operation = Selection.Update(); - var anchor = Selection.AnchorIndex; - - if (anchor.GetSize() == 0) - { - anchor = new IndexPath(0); - } - - Selection.ClearSelection(); - Selection.AnchorIndex = anchor; - Selection.SelectRangeFromAnchorTo(index); + SynchronizeItems( + SelectedItems, + GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem)); } else { - if (Selection.IsSelectedAt(index)) - { - Selection.DeselectAt(index); - } - else if (multi) + var i = SelectedItems.IndexOf(item); + + if (i != -1) { - Selection.SelectAt(index); + SelectedItems.Remove(item); } else { - Selection.SelectedIndex = index; + if (multi) + { + SelectedItems.Add(item); + } + else + { + SelectedItem = item; + } } } } @@ -604,6 +616,117 @@ namespace Avalonia.Controls } } + /// + /// Find which node is first in hierarchy. + /// + /// Search root. + /// Nodes to find. + /// Node to find. + /// Found first node. + private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB) + { + return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB); + } + + private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator, + TreeViewItem nodeA, + TreeViewItem nodeB) + { + IEnumerable containers = containerGenerator.Containers; + + foreach (ItemContainerInfo container in containers) + { + TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB); + + if (node != null) + { + return node; + } + } + + return null; + } + + private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB) + { + if (node == null) + { + return null; + } + + TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null; + + if (match != null) + { + return match; + } + + return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB); + } + + /// + /// Returns all items that belong to containers between and . + /// The range is inclusive. + /// + /// From container. + /// To container. + private List GetItemsInRange(TreeViewItem from, TreeViewItem to) + { + var items = new List(); + + if (from == null || to == null) + { + return items; + } + + TreeViewItem firstItem = FindFirstNode(this, from, to); + + if (firstItem == null) + { + return items; + } + + bool wasReversed = false; + + if (firstItem == to) + { + var temp = from; + + from = to; + to = temp; + + wasReversed = true; + } + + TreeViewItem node = from; + + while (node != to) + { + var item = ItemContainerGenerator.Index.ItemFromContainer(node); + + if (item != null) + { + items.Add(item); + } + + node = GetContainerInDirection(node, NavigationDirection.Down, true); + } + + var toItem = ItemContainerGenerator.Index.ItemFromContainer(to); + + if (toItem != null) + { + items.Add(toItem); + } + + if (wasReversed) + { + items.Reverse(); + } + + return items; + } + /// /// Updates the selection based on an event that may have originated in a container that /// belongs to the control. @@ -709,90 +832,26 @@ namespace Avalonia.Controls } } - private void MarkContainersUnselected() - { - foreach (var container in ItemContainerGenerator.Index.Containers) - { - MarkContainerSelected(container, false); - } - } - - private void UpdateContainerSelection() - { - var index = ItemContainerGenerator.Index; - - foreach (var container in index.Containers) - { - var i = IndexFromContainer((TreeViewItem)container); - - MarkContainerSelected( - container, - Selection.IsSelectedAt(i) != false); - } - } - - private static IndexPath IndexFromContainer(TreeViewItem container) - { - var result = new List(); - - while (true) - { - if (container.Level == 0) - { - var treeView = container.FindAncestorOfType(); - - if (treeView == null) - { - return default; - } - - result.Add(treeView.ItemContainerGenerator.IndexFromContainer(container)); - result.Reverse(); - return new IndexPath(result); - } - else - { - var parent = container.FindAncestorOfType(); - - if (parent == null) - { - return default; - } - - result.Add(parent.ItemContainerGenerator.IndexFromContainer(container)); - container = parent; - } - } - } - - private IndexPath IndexFromItem(object item) + /// + /// Makes a list of objects equal another (though doesn't preserve order). + /// + /// The items collection. + /// The desired items. + private static void SynchronizeItems(IList items, IEnumerable desired) { - var container = ItemContainerGenerator.Index.ContainerFromItem(item) as TreeViewItem; + var list = items.Cast().ToList(); + var toRemove = list.Except(desired).ToList(); + var toAdd = desired.Except(list).ToList(); - if (container != null) + foreach (var i in toRemove) { - return IndexFromContainer(container); + items.Remove(i); } - return default; - } - - private TreeViewItem ContainerFromIndex(IndexPath index) - { - TreeViewItem treeViewItem = null; - - for (var i = 0; i < index.GetSize(); ++i) + foreach (var i in toAdd) { - var generator = treeViewItem?.ItemContainerGenerator ?? ItemContainerGenerator; - treeViewItem = generator.ContainerFromIndex(index.GetAt(i)) as TreeViewItem; - - if (treeViewItem == null) - { - return null; - } + items.Add(i); } - - return treeViewItem; } } } diff --git a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs b/src/Avalonia.Controls/Utils/SelectedItemsSync.cs deleted file mode 100644 index 91cef9fe64..0000000000 --- a/src/Avalonia.Controls/Utils/SelectedItemsSync.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using Avalonia.Collections; - -#nullable enable - -namespace Avalonia.Controls.Utils -{ - /// - /// Synchronizes an with a list of SelectedItems. - /// - internal class SelectedItemsSync - { - private IList? _items; - private bool _updatingItems; - private bool _updatingModel; - private bool _initializeOnSourceAssignment; - - public SelectedItemsSync(ISelectionModel model) - { - model = model ?? throw new ArgumentNullException(nameof(model)); - Model = model; - } - - public ISelectionModel Model { get; private set; } - - public IList GetOrCreateItems() - { - if (_items == null) - { - var items = new AvaloniaList(Model.SelectedItems); - items.CollectionChanged += ItemsCollectionChanged; - Model.SelectionChanged += SelectionModelSelectionChanged; - _items = items; - } - - return _items; - } - - public void SetItems(IList? items) - { - items ??= new AvaloniaList(); - - if (items.IsFixedSize) - { - throw new NotSupportedException( - "Cannot assign fixed size selection to SelectedItems."); - } - - if (_items is INotifyCollectionChanged incc) - { - incc.CollectionChanged -= ItemsCollectionChanged; - } - - if (_items == null) - { - Model.SelectionChanged += SelectionModelSelectionChanged; - } - - try - { - _updatingModel = true; - _items = items; - - if (Model.Source is object) - { - using (Model.Update()) - { - Model.ClearSelection(); - Add(items); - } - } - else if (!_initializeOnSourceAssignment) - { - Model.PropertyChanged += SelectionModelPropertyChanged; - _initializeOnSourceAssignment = true; - } - - if (_items is INotifyCollectionChanged incc2) - { - incc2.CollectionChanged += ItemsCollectionChanged; - } - } - finally - { - _updatingModel = false; - } - } - - public void SetModel(ISelectionModel model) - { - model = model ?? throw new ArgumentNullException(nameof(model)); - - if (_items != null) - { - Model.PropertyChanged -= SelectionModelPropertyChanged; - Model.SelectionChanged -= SelectionModelSelectionChanged; - Model = model; - Model.SelectionChanged += SelectionModelSelectionChanged; - _initializeOnSourceAssignment = false; - - try - { - _updatingItems = true; - _items.Clear(); - - foreach (var i in model.SelectedItems) - { - _items.Add(i); - } - } - finally - { - _updatingItems = false; - } - } - } - - private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (_updatingItems) - { - return; - } - - if (_items == null) - { - throw new AvaloniaInternalException("CollectionChanged raised but we don't have items."); - } - - void Remove() - { - foreach (var i in e.OldItems) - { - var index = IndexOf(Model.Source, i); - - if (index != -1) - { - Model.Deselect(index); - } - } - } - - try - { - using var operation = Model.Update(); - - _updatingModel = true; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Add(e.NewItems); - break; - case NotifyCollectionChangedAction.Remove: - Remove(); - break; - case NotifyCollectionChangedAction.Replace: - Remove(); - Add(e.NewItems); - break; - case NotifyCollectionChangedAction.Reset: - Model.ClearSelection(); - Add(_items); - break; - } - } - finally - { - _updatingModel = false; - } - } - - private void Add(IList newItems) - { - foreach (var i in newItems) - { - var index = IndexOf(Model.Source, i); - - if (index != -1) - { - Model.Select(index); - } - } - } - - private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (_initializeOnSourceAssignment && - _items != null && - e.PropertyName == nameof(SelectionModel.Source)) - { - try - { - _updatingModel = true; - Add(_items); - _initializeOnSourceAssignment = false; - } - finally - { - _updatingModel = false; - } - } - } - - private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) - { - if (_updatingModel) - { - return; - } - - if (_items == null) - { - throw new AvaloniaInternalException("SelectionModelChanged raised but we don't have items."); - } - - try - { - var deselected = e.DeselectedItems.ToList(); - var selected = e.SelectedItems.ToList(); - - _updatingItems = true; - - foreach (var i in deselected) - { - _items.Remove(i); - } - - foreach (var i in selected) - { - _items.Add(i); - } - } - finally - { - _updatingItems = false; - } - } - - private static int IndexOf(object source, object item) - { - if (source is IList l) - { - return l.IndexOf(item); - } - else if (source is ItemsSourceView v) - { - return v.IndexOf(item); - } - - return -1; - } - } -} diff --git a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs b/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs deleted file mode 100644 index 5adf5bdeea..0000000000 --- a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs +++ /dev/null @@ -1,189 +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.Utils -{ - internal static class SelectionTreeHelper - { - public static void TraverseIndexPath( - SelectionNode root, - IndexPath path, - bool realizeChildren, - Action nodeAction) - { - var node = root; - - for (int depth = 0; depth < path.GetSize(); depth++) - { - int childIndex = path.GetAt(depth); - nodeAction(node, path, depth, childIndex); - - if (depth < path.GetSize() - 1) - { - node = node.GetAt(childIndex, realizeChildren, path)!; - } - } - } - - public static void Traverse( - SelectionNode root, - bool realizeChildren, - Action nodeAction) - { - var pendingNodes = new List(); - var current = new IndexPath(null); - - pendingNodes.Add(new TreeWalkNodeInfo(root, current)); - - while (pendingNodes.Count > 0) - { - var nextNode = pendingNodes.Last(); - pendingNodes.RemoveAt(pendingNodes.Count - 1); - int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount; - for (int i = count - 1; i >= 0; i--) - { - var child = nextNode.Node.GetAt(i, realizeChildren, nextNode.Path); - var childPath = nextNode.Path.CloneWithChildIndex(i); - if (child != null) - { - pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, nextNode.Node)); - } - } - - // Queue the children first and then perform the action. This way - // the action can remove the children in the action if necessary - nodeAction(nextNode); - } - } - - public static void TraverseRangeRealizeChildren( - SelectionNode root, - IndexPath start, - IndexPath end, - Action nodeAction) - { - var pendingNodes = new List(); - var current = start; - - // Build up the stack to account for the depth first walk up to the - // start index path. - TraverseIndexPath( - root, - start, - true, - (node, path, depth, childIndex) => - { - var currentPath = StartPath(path, depth); - bool isStartPath = IsSubSet(start, currentPath); - bool isEndPath = IsSubSet(end, currentPath); - - int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0; - int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : node.DataCount - 1; - - for (int i = endIndex; i >= startIndex; i--) - { - var child = node.GetAt(i, true, end); - if (child != null) - { - var childPath = currentPath.CloneWithChildIndex(i); - pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, node)); - } - } - }); - - // From the start index path, do a depth first walk as long as the - // current path is less than the end path. - while (pendingNodes.Count > 0) - { - var info = pendingNodes.Last(); - pendingNodes.RemoveAt(pendingNodes.Count - 1); - int depth = info.Path.GetSize(); - bool isStartPath = IsSubSet(start, info.Path); - bool isEndPath = IsSubSet(end, info.Path); - int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0; - int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1; - for (int i = endIndex; i >= startIndex; i--) - { - var child = info.Node.GetAt(i, true, end); - if (child != null) - { - var childPath = info.Path.CloneWithChildIndex(i); - pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, info.Node)); - } - } - - nodeAction(info); - - if (info.Path.CompareTo(end) == 0) - { - // We reached the end index path. stop iterating. - break; - } - } - } - - private static bool IsSubSet(IndexPath path, IndexPath subset) - { - var subsetSize = subset.GetSize(); - if (path.GetSize() < subsetSize) - { - return false; - } - - for (int i = 0; i < subsetSize; i++) - { - if (path.GetAt(i) != subset.GetAt(i)) - { - return false; - } - } - - return true; - } - - private static IndexPath StartPath(IndexPath path, int length) - { - var subPath = new List(); - for (int i = 0; i < length; i++) - { - subPath.Add(path.GetAt(i)); - } - - return new IndexPath(subPath); - } - - public struct TreeWalkNodeInfo - { - public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode? parent) - { - node = node ?? throw new ArgumentNullException(nameof(node)); - - Node = node; - Path = indexPath; - ParentNode = parent; - } - - public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath) - { - node = node ?? throw new ArgumentNullException(nameof(node)); - - Node = node; - Path = indexPath; - ParentNode = null; - } - - public SelectionNode Node { get; } - public IndexPath Path { get; } - public SelectionNode? ParentNode { get; } - }; - - } -} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index cb5f5b1fda..d9a0d17518 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -83,27 +83,6 @@ namespace Avalonia.Diagnostics.ViewModels private set; } - public IndexPath Index - { - get - { - var indices = new List(); - var child = this; - var parent = Parent; - - while (parent is object) - { - indices.Add(IndexOf(parent.Children, child)); - child = child.Parent; - parent = parent.Parent; - } - - indices.Add(0); - indices.Reverse(); - return new IndexPath(indices); - } - } - public void Dispose() { _classesSubscription.Dispose(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index ec48cff399..19d2de442c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -13,22 +13,10 @@ namespace Avalonia.Diagnostics.ViewModels public TreePageViewModel(TreeNode[] nodes) { Nodes = nodes; - Selection = new SelectionModel - { - SingleSelect = true, - Source = Nodes - }; - - Selection.SelectionChanged += (s, e) => - { - SelectedNode = (TreeNode)Selection.SelectedItem; - }; } public TreeNode[] Nodes { get; protected set; } - public SelectionModel Selection { get; } - public TreeNode SelectedNode { get => _selectedNode; @@ -103,8 +91,8 @@ namespace Avalonia.Diagnostics.ViewModels if (node != null) { + SelectedNode = node; ExpandNode(node.Parent); - Selection.SelectedIndex = node.Index; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index 4ddb320175..a1e6ca7d37 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -6,7 +6,7 @@ + SelectedItem="{Binding SelectedNode, Mode=TwoWay}"> diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index a292910fae..32f3b06791 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -265,7 +265,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() + public void Selected_Item_Changes_To_NextAvailable_Item_If_SelectedItem_Is_Removed_From_Middle() { var items = new ObservableCollection { @@ -288,8 +288,8 @@ namespace Avalonia.Controls.UnitTests items.RemoveAt(1); - Assert.Equal(0, target.SelectedIndex); - Assert.Equal("Foo", target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("FooBar", target.SelectedItem); } private Control CreateTemplate(Carousel control, INameScope scope) diff --git a/tests/Avalonia.Controls.UnitTests/IndexPathTests.cs b/tests/Avalonia.Controls.UnitTests/IndexPathTests.cs deleted file mode 100644 index 1e4aa0a2b8..0000000000 --- a/tests/Avalonia.Controls.UnitTests/IndexPathTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class IndexPathTests - { - [Fact] - public void Simple_Index() - { - var a = new IndexPath(1); - - Assert.Equal(1, a.GetSize()); - Assert.Equal(1, a.GetAt(0)); - } - - [Fact] - public void Equal_Paths() - { - var a = new IndexPath(1); - var b = new IndexPath(1); - - Assert.True(a == b); - Assert.False(a != b); - Assert.True(a.Equals(b)); - Assert.Equal(0, a.CompareTo(b)); - Assert.Equal(a.GetHashCode(), b.GetHashCode()); - } - - [Fact] - public void Unequal_Paths() - { - var a = new IndexPath(1); - var b = new IndexPath(2); - - Assert.False(a == b); - Assert.True(a != b); - Assert.False(a.Equals(b)); - Assert.Equal(-1, a.CompareTo(b)); - Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); - } - - [Fact] - public void Equal_Null_Path() - { - var a = new IndexPath(null); - var b = new IndexPath(null); - - Assert.True(a == b); - Assert.False(a != b); - Assert.True(a.Equals(b)); - Assert.Equal(0, a.CompareTo(b)); - Assert.Equal(a.GetHashCode(), b.GetHashCode()); - } - - [Fact] - public void Unequal_Null_Path() - { - var a = new IndexPath(null); - var b = new IndexPath(2); - - Assert.False(a == b); - Assert.True(a != b); - Assert.False(a.Equals(b)); - Assert.Equal(-1, a.CompareTo(b)); - Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); - } - - [Fact] - public void Default_Is_Null_Path() - { - var a = new IndexPath(null); - var b = default(IndexPath); - - Assert.True(a == b); - Assert.False(a != b); - Assert.True(a.Equals(b)); - Assert.Equal(0, a.CompareTo(b)); - Assert.Equal(a.GetHashCode(), b.GetHashCode()); - } - - [Fact] - public void Null_Equality() - { - var a = new IndexPath(null); - var b = new IndexPath(1); - - // Implementing operator == on a struct automatically implements an operator which - // accepts null, so make sure this does something useful. - Assert.True(a == null); - Assert.False(a != null); - Assert.False(b == null); - Assert.True(b != null); - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs deleted file mode 100644 index e01c752658..0000000000 --- a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs +++ /dev/null @@ -1,389 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class IndexRangeTests - { - [Fact] - public void Add_Should_Add_Range_To_Empty_List() - { - var ranges = new List(); - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected); - - Assert.Equal(5, result); - Assert.Equal(new[] { new IndexRange(0, 4) }, ranges); - Assert.Equal(new[] { new IndexRange(0, 4) }, selected); - } - - [Fact] - public void Add_Should_Add_Non_Intersecting_Range_At_End() - { - var ranges = new List { new IndexRange(0, 4) }; - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected); - - Assert.Equal(3, result); - Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges); - Assert.Equal(new[] { new IndexRange(8, 10) }, selected); - } - - [Fact] - public void Add_Should_Add_Non_Intersecting_Range_At_Beginning() - { - var ranges = new List { new IndexRange(8, 10) }; - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected); - - Assert.Equal(5, result); - Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges); - Assert.Equal(new[] { new IndexRange(0, 4) }, selected); - } - - [Fact] - public void Add_Should_Add_Non_Intersecting_Range_In_Middle() - { - var ranges = new List { new IndexRange(0, 4), new IndexRange(14, 16) }; - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected); - - Assert.Equal(3, result); - Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10), new IndexRange(14, 16) }, ranges); - Assert.Equal(new[] { new IndexRange(8, 10) }, selected); - } - - [Fact] - public void Add_Should_Add_Intersecting_Range_Start() - { - var ranges = new List { new IndexRange(8, 10) }; - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(6, 9), selected); - - Assert.Equal(2, result); - Assert.Equal(new[] { new IndexRange(6, 10) }, ranges); - Assert.Equal(new[] { new IndexRange(6, 7) }, selected); - } - - [Fact] - public void Add_Should_Add_Intersecting_Range_End() - { - var ranges = new List { new IndexRange(8, 10) }; - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(9, 12), selected); - - Assert.Equal(2, result); - Assert.Equal(new[] { new IndexRange(8, 12) }, ranges); - Assert.Equal(new[] { new IndexRange(11, 12) }, selected); - } - - [Fact] - public void Add_Should_Add_Intersecting_Range_Both() - { - var ranges = new List { new IndexRange(8, 10) }; - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(6, 12), selected); - - Assert.Equal(4, result); - Assert.Equal(new[] { new IndexRange(6, 12) }, ranges); - Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 12) }, selected); - } - - [Fact] - public void Add_Should_Join_Two_Intersecting_Ranges() - { - var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14) }; - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(8, 14), selected); - - Assert.Equal(1, result); - Assert.Equal(new[] { new IndexRange(8, 14) }, ranges); - Assert.Equal(new[] { new IndexRange(11, 11) }, selected); - } - - [Fact] - public void Add_Should_Join_Two_Intersecting_Ranges_And_Add_Ranges() - { - var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14) }; - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(6, 18), selected); - - Assert.Equal(7, result); - Assert.Equal(new[] { new IndexRange(6, 18) }, ranges); - Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 11), new IndexRange(15, 18) }, selected); - } - - [Fact] - public void Add_Should_Not_Add_Already_Selected_Range() - { - var ranges = new List { new IndexRange(8, 10) }; - var selected = new List(); - var result = IndexRange.Add(ranges, new IndexRange(9, 10), selected); - - Assert.Equal(0, result); - Assert.Equal(new[] { new IndexRange(8, 10) }, ranges); - Assert.Empty(selected); - } - - [Fact] - public void Intersect_Should_Remove_Items_From_Beginning() - { - var ranges = new List { new IndexRange(0, 10) }; - var removed = new List(); - var result = IndexRange.Intersect(ranges, new IndexRange(2, 12), removed); - - Assert.Equal(2, result); - Assert.Equal(new[] { new IndexRange(2, 10) }, ranges); - Assert.Equal(new[] { new IndexRange(0, 1) }, removed); - } - - [Fact] - public void Intersect_Should_Remove_Items_From_End() - { - var ranges = new List { new IndexRange(0, 10) }; - var removed = new List(); - var result = IndexRange.Intersect(ranges, new IndexRange(0, 8), removed); - - Assert.Equal(2, result); - Assert.Equal(new[] { new IndexRange(0, 8) }, ranges); - Assert.Equal(new[] { new IndexRange(9, 10) }, removed); - } - - [Fact] - public void Intersect_Should_Remove_Entire_Range_Start() - { - var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; - var removed = new List(); - var result = IndexRange.Intersect(ranges, new IndexRange(6, 10), removed); - - Assert.Equal(6, result); - Assert.Equal(new[] { new IndexRange(6, 10) }, ranges); - Assert.Equal(new[] { new IndexRange(0, 5) }, removed); - } - - [Fact] - public void Intersect_Should_Remove_Entire_Range_End() - { - var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; - var removed = new List(); - var result = IndexRange.Intersect(ranges, new IndexRange(0, 4), removed); - - Assert.Equal(6, result); - Assert.Equal(new[] { new IndexRange(0, 4) }, ranges); - Assert.Equal(new[] { new IndexRange(5, 10) }, removed); - } - - [Fact] - public void Intersect_Should_Remove_Entire_Range_Start_End() - { - var ranges = new List - { - new IndexRange(0, 2), - new IndexRange(3, 7), - new IndexRange(8, 10) - }; - var removed = new List(); - var result = IndexRange.Intersect(ranges, new IndexRange(3, 7), removed); - - Assert.Equal(6, result); - Assert.Equal(new[] { new IndexRange(3, 7) }, ranges); - Assert.Equal(new[] { new IndexRange(0, 2), new IndexRange(8, 10) }, removed); - } - - [Fact] - public void Intersect_Should_Remove_Entire_And_Partial_Range_Start_End() - { - var ranges = new List - { - new IndexRange(0, 2), - new IndexRange(3, 7), - new IndexRange(8, 10) - }; - var removed = new List(); - var result = IndexRange.Intersect(ranges, new IndexRange(4, 6), removed); - - Assert.Equal(8, result); - Assert.Equal(new[] { new IndexRange(4, 6) }, ranges); - Assert.Equal(new[] { new IndexRange(0, 3), new IndexRange(7, 10) }, removed); - } - - [Fact] - public void Remove_Should_Remove_Entire_Range() - { - var ranges = new List { new IndexRange(8, 10) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected); - - Assert.Equal(3, result); - Assert.Empty(ranges); - Assert.Equal(new[] { new IndexRange(8, 10) }, deselected); - } - - [Fact] - public void Remove_Should_Remove_Start_Of_Range() - { - var ranges = new List { new IndexRange(8, 12) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected); - - Assert.Equal(3, result); - Assert.Equal(new[] { new IndexRange(11, 12) }, ranges); - Assert.Equal(new[] { new IndexRange(8, 10) }, deselected); - } - - [Fact] - public void Remove_Should_Remove_End_Of_Range() - { - var ranges = new List { new IndexRange(8, 12) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(10, 12), deselected); - - Assert.Equal(3, result); - Assert.Equal(new[] { new IndexRange(8, 9) }, ranges); - Assert.Equal(new[] { new IndexRange(10, 12) }, deselected); - } - - [Fact] - public void Remove_Should_Remove_Overlapping_End_Of_Range() - { - var ranges = new List { new IndexRange(8, 12) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(10, 14), deselected); - - Assert.Equal(3, result); - Assert.Equal(new[] { new IndexRange(8, 9) }, ranges); - Assert.Equal(new[] { new IndexRange(10, 12) }, deselected); - } - - [Fact] - public void Remove_Should_Remove_Middle_Of_Range() - { - var ranges = new List { new IndexRange(10, 20) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(12, 16), deselected); - - Assert.Equal(5, result); - Assert.Equal(new[] { new IndexRange(10, 11), new IndexRange(17, 20) }, ranges); - Assert.Equal(new[] { new IndexRange(12, 16) }, deselected); - } - - [Fact] - public void Remove_Should_Remove_Multiple_Ranges() - { - var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(6, 15), deselected); - - Assert.Equal(6, result); - Assert.Equal(new[] { new IndexRange(16, 18) }, ranges); - Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 14) }, deselected); - } - - [Fact] - public void Remove_Should_Remove_Multiple_And_Partial_Ranges_1() - { - var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(9, 15), deselected); - - Assert.Equal(5, result); - Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(16, 18) }, ranges); - Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 14) }, deselected); - } - - [Fact] - public void Remove_Should_Remove_Multiple_And_Partial_Ranges_2() - { - var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(8, 13), deselected); - - Assert.Equal(5, result); - Assert.Equal(new[] { new IndexRange(14, 14), new IndexRange(16, 18) }, ranges); - Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 13) }, deselected); - } - - [Fact] - public void Remove_Should_Remove_Multiple_And_Partial_Ranges_3() - { - var ranges = new List { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(9, 13), deselected); - - Assert.Equal(4, result); - Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(14, 14), new IndexRange(16, 18) }, ranges); - Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 13) }, deselected); - } - - [Fact] - public void Remove_Should_Do_Nothing_For_Unselected_Range() - { - var ranges = new List { new IndexRange(8, 10) }; - var deselected = new List(); - var result = IndexRange.Remove(ranges, new IndexRange(2, 4), deselected); - - Assert.Equal(0, result); - Assert.Equal(new[] { new IndexRange(8, 10) }, ranges); - Assert.Empty(deselected); - } - - [Fact] - public void Stress_Test() - { - const int iterations = 100; - var random = new Random(0); - var selection = new List(); - var expected = new List(); - - IndexRange Generate() - { - var start = random.Next(100); - return new IndexRange(start, start + random.Next(20)); - } - - for (var i = 0; i < iterations; ++i) - { - var toAdd = random.Next(5); - - for (var j = 0; j < toAdd; ++j) - { - var range = Generate(); - IndexRange.Add(selection, range); - - for (var k = range.Begin; k <= range.End; ++k) - { - if (!expected.Contains(k)) - { - expected.Add(k); - } - } - - var actual = IndexRange.EnumerateIndices(selection).ToList(); - expected.Sort(); - Assert.Equal(expected, actual); - } - - var toRemove = random.Next(5); - - for (var j = 0; j < toRemove; ++j) - { - var range = Generate(); - IndexRange.Remove(selection, range); - - for (var k = range.Begin; k <= range.End; ++k) - { - expected.Remove(k); - } - - var actual = IndexRange.EnumerateIndices(selection).ToList(); - Assert.Equal(expected, actual); - } - - selection.Clear(); - expected.Clear(); - } - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index c4346e571b..ba9e68e6c4 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -385,7 +385,6 @@ namespace Avalonia.Controls.UnitTests // First an item that is not index 0 must be selected. _mouse.Click(target.Presenter.Panel.Children[1]); - Assert.Equal(new IndexPath(1), target.Selection.AnchorIndex); // We're going to be clicking on item 9. var item = (ListBoxItem)target.Presenter.Panel.Children[9]; diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 9ef2750ff3..320f660a8f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1111,8 +1111,8 @@ namespace Avalonia.Controls.UnitTests.Primitives items[1] = "Qux"; - Assert.Equal(-1, target.SelectedIndex); - Assert.Null(target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Qux", target.SelectedItem); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 7b7e651cc9..a1a143750e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -75,8 +75,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectedIndex = 2; items.RemoveAt(2); - Assert.Equal(0, target.SelectedIndex); - Assert.Equal("foo", target.SelectedItem); + Assert.Equal(2, target.SelectedIndex); + Assert.Equal("qux", target.SelectedItem); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index dcf25beb50..6b56242ace 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1259,183 +1259,9 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - target.Selection.Select(1); - - Assert.Equal(1, target.SelectedIndex); - } - - [Fact] - public void Assigning_Null_To_Selection_Should_Create_New_SelectionModel() - { - var target = new TestSelector - { - Items = new[] { "foo", "bar" }, - Template = Template(), - }; - - var oldSelection = target.Selection; - - target.Selection = null; - - Assert.NotNull(target.Selection); - Assert.NotSame(oldSelection, target.Selection); - } - - [Fact] - public void Assigning_SelectionModel_With_Different_Source_To_Selection_Should_Fail() - { - var target = new TestSelector - { - Items = new[] { "foo", "bar" }, - Template = Template(), - }; - - var selection = new SelectionModel { Source = new[] { "baz" } }; - Assert.Throws(() => target.Selection = selection); - } - - [Fact] - public void Assigning_SelectionModel_With_Null_Source_To_Selection_Should_Set_Source() - { - var target = new TestSelector - { - Items = new[] { "foo", "bar" }, - Template = Template(), - }; - - var selection = new SelectionModel(); - target.Selection = selection; - - Assert.Same(target.Items, selection.Source); - } - - [Fact] - public void Assigning_Single_Selected_Item_To_Selection_Should_Set_SelectedIndex() - { - var target = new TestSelector - { - Items = new[] { "foo", "bar" }, - Template = Template(), - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var selection = new SelectionModel { Source = target.Items }; - selection.Select(1); - target.Selection = selection; + target.SelectedItems.Add("bar"); Assert.Equal(1, target.SelectedIndex); - Assert.Equal(new[] { "bar" }, target.Selection.SelectedItems); - Assert.Equal(new[] { 1 }, SelectedContainers(target)); - } - - [Fact] - public void Assigning_Multiple_Selected_Items_To_Selection_Should_Set_SelectedIndex() - { - var target = new TestSelector - { - Items = new[] { "foo", "bar", "baz" }, - Template = Template(), - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var selection = new SelectionModel { Source = target.Items }; - selection.SelectRange(new IndexPath(0), new IndexPath(2)); - target.Selection = selection; - - Assert.Equal(0, target.SelectedIndex); - Assert.Equal(new[] { "foo", "bar", "baz" }, target.Selection.SelectedItems); - Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); - } - - [Fact] - public void Reassigning_Selection_Should_Clear_Selection() - { - var target = new TestSelector - { - Items = new[] { "foo", "bar" }, - Template = Template(), - }; - - target.ApplyTemplate(); - target.Selection.Select(1); - target.Selection = new SelectionModel(); - - Assert.Equal(-1, target.SelectedIndex); - Assert.Null(target.SelectedItem); - } - - [Fact] - public void Assigning_Selection_Should_Set_Item_IsSelected() - { - var items = new[] - { - new ListBoxItem(), - new ListBoxItem(), - new ListBoxItem(), - }; - - var target = new TestSelector - { - Items = items, - Template = Template(), - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var selection = new SelectionModel { Source = items }; - selection.SelectRange(new IndexPath(0), new IndexPath(1)); - target.Selection = selection; - - Assert.True(items[0].IsSelected); - Assert.True(items[1].IsSelected); - Assert.False(items[2].IsSelected); - } - - [Fact] - public void Assigning_Selection_Should_Raise_SelectionChanged() - { - var items = new[] { "foo", "bar", "baz" }; - - var target = new TestSelector - { - Items = items, - Template = Template(), - SelectedItem = "bar", - }; - - var raised = 0; - - target.SelectionChanged += (s, e) => - { - if (raised == 0) - { - Assert.Empty(e.AddedItems.Cast()); - Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast()); - } - else - { - Assert.Equal(new[] { "foo", "baz" }, e.AddedItems.Cast()); - Assert.Empty(e.RemovedItems.Cast()); - } - - ++raised; - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - - var selection = new SelectionModel { Source = items }; - selection.Select(0); - selection.Select(2); - target.Selection = selection; - - Assert.Equal(2, raised); } private IEnumerable SelectedContainers(SelectingItemsControl target) @@ -1472,20 +1298,14 @@ namespace Avalonia.Controls.UnitTests.Primitives set { base.SelectedItems = value; } } - public new ISelectionModel Selection - { - get => base.Selection; - set => base.Selection = value; - } - public new SelectionMode SelectionMode { get { return base.SelectionMode; } set { base.SelectionMode = value; } } - public void SelectAll() => Selection.SelectAll(); - public void UnselectAll() => Selection.ClearSelection(); + public new void SelectAll() => base.SelectAll(); + public new void UnselectAll() => base.UnselectAll(); public void SelectRange(int index) => UpdateSelection(index, true, true); public void Toggle(int index) => UpdateSelection(index, true, false, true); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs index b1e18dc587..a8b16a7ebf 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Removing_Selected_Should_Select_First() + public void Removing_Selected_Should_Select_Next() { var items = new ObservableCollection() { @@ -96,9 +96,10 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Same(items[1], target.SelectedItem); items.RemoveAt(1); - Assert.Equal(0, target.SelectedIndex); - Assert.Same(items[0], target.SelectedItem); - Assert.Same("first", ((TabItem)target.SelectedItem).Name); + // Assert for former element [2] now [1] == "3rd" + Assert.Equal(1, target.SelectedIndex); + Assert.Same(items[1], target.SelectedItem); + Assert.Same("3rd", ((TabItem)target.SelectedItem).Name); } private Control CreateTabStripTemplate(TabStrip parent, INameScope scope) diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs deleted file mode 100644 index 24e82a69d0..0000000000 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ /dev/null @@ -1,2322 +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; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Collections; -using Avalonia.Diagnostics; -using ReactiveUI; -using Xunit; -using Xunit.Abstractions; - -namespace Avalonia.Controls.UnitTests -{ - public class SelectionModelTests - { - private readonly ITestOutputHelper _output; - - public SelectionModelTests(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public void ValidateOneLevelSingleSelectionNoSource() - { - SelectionModel selectionModel = new SelectionModel() { SingleSelect = true }; - _output.WriteLine("No source set."); - Select(selectionModel, 4, true); - ValidateSelection(selectionModel, Path(4)); - Select(selectionModel, 4, false); - ValidateSelection(selectionModel); - } - - [Fact] - public void ValidateOneLevelSingleSelection() - { - SelectionModel selectionModel = new SelectionModel() { SingleSelect = true }; - _output.WriteLine("Set the source to 10 items"); - selectionModel.Source = Enumerable.Range(0, 10).ToList(); - Select(selectionModel, 3, true); - ValidateSelection(selectionModel, Path(3)); - Select(selectionModel, 3, false); - ValidateSelection(selectionModel); - } - - [Fact] - public void ValidateSelectionChangedEvent() - { - SelectionModel selectionModel = new SelectionModel(); - selectionModel.Source = Enumerable.Range(0, 10).ToList(); - - int selectionChangedFiredCount = 0; - selectionModel.SelectionChanged += delegate (object sender, SelectionModelSelectionChangedEventArgs args) - { - selectionChangedFiredCount++; - ValidateSelection(selectionModel, Path(4)); - }; - - Select(selectionModel, 4, true); - ValidateSelection(selectionModel, Path(4)); - Assert.Equal(1, selectionChangedFiredCount); - } - - [Fact] - public void ValidateCanSetSelectedIndex() - { - var model = new SelectionModel(); - var ip = IndexPath.CreateFrom(34); - model.SelectedIndex = ip; - Assert.Equal(0, ip.CompareTo(model.SelectedIndex)); - } - - [Fact] - public void ValidateOneLevelMultipleSelection() - { - SelectionModel selectionModel = new SelectionModel(); - selectionModel.Source = Enumerable.Range(0, 10).ToList(); - - Select(selectionModel, 4, true); - ValidateSelection(selectionModel, Path(4)); - SelectRangeFromAnchor(selectionModel, 8, true /* select */); - ValidateSelection(selectionModel, - Path(4), - Path(5), - Path(6), - Path(7), - Path(8)); - - ClearSelection(selectionModel); - SetAnchorIndex(selectionModel, 6); - SelectRangeFromAnchor(selectionModel, 3, true /* select */); - ValidateSelection(selectionModel, - Path(3), - Path(4), - Path(5), - Path(6)); - - SetAnchorIndex(selectionModel, 4); - SelectRangeFromAnchor(selectionModel, 5, false /* select */); - ValidateSelection(selectionModel, - Path(3), - Path(6)); - } - - [Fact] - public void ValidateTwoLevelSingleSelection() - { - SelectionModel selectionModel = new SelectionModel(); - _output.WriteLine("Setting the source"); - selectionModel.Source = CreateNestedData(1 /* levels */ , 2 /* groupsAtLevel */, 2 /* countAtLeaf */); - Select(selectionModel, 1, 1, true); - ValidateSelection(selectionModel, Path(1, 1)); - Select(selectionModel, 1, 1, false); - ValidateSelection(selectionModel); - } - - [Fact] - public void ValidateTwoLevelMultipleSelection() - { - SelectionModel selectionModel = new SelectionModel(); - _output.WriteLine("Setting the source"); - selectionModel.Source = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); - - Select(selectionModel, 1, 2, true); - ValidateSelection(selectionModel, Path(1, 2)); - SelectRangeFromAnchor(selectionModel, 2, 2, true /* select */); - ValidateSelection(selectionModel, - Path(1, 2), - Path(2, 0), - Path(2, 1), - Path(2, 2)); - - ClearSelection(selectionModel); - SetAnchorIndex(selectionModel, 2, 1); - SelectRangeFromAnchor(selectionModel, 0, 1, true /* select */); - ValidateSelection(selectionModel, - Path(0, 1), - Path(0, 2), - Path(1, 0), - Path(1, 1), - Path(1, 2), - Path(2, 0), - Path(2, 1)); - - SetAnchorIndex(selectionModel, 1, 1); - SelectRangeFromAnchor(selectionModel, 2, 0, false /* select */); - ValidateSelection(selectionModel, - Path(0, 1), - Path(0, 2), - Path(1, 0), - Path(2, 1)); - - ClearSelection(selectionModel); - ValidateSelection(selectionModel); - } - - [Fact] - public void ValidateNestedSingleSelection() - { - SelectionModel selectionModel = new SelectionModel() { SingleSelect = true }; - _output.WriteLine("Setting the source"); - selectionModel.Source = CreateNestedData(3 /* levels */ , 2 /* groupsAtLevel */, 2 /* countAtLeaf */); - var path = Path(1, 0, 1, 1); - Select(selectionModel, path, true); - ValidateSelection(selectionModel, path); - Select(selectionModel, Path(0, 0, 1, 0), true); - ValidateSelection(selectionModel, Path(0, 0, 1, 0)); - Select(selectionModel, Path(0, 0, 1, 0), false); - ValidateSelection(selectionModel); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ValidateNestedMultipleSelection(bool handleChildrenRequested) - { - SelectionModel selectionModel = new SelectionModel(); - List sourcePaths = new List(); - - _output.WriteLine("Setting the source"); - selectionModel.Source = CreateNestedData(3 /* levels */ , 2 /* groupsAtLevel */, 4 /* countAtLeaf */); - if (handleChildrenRequested) - { - selectionModel.ChildrenRequested += (object sender, SelectionModelChildrenRequestedEventArgs args) => - { - _output.WriteLine("ChildrenRequestedIndexPath:" + args.SourceIndex); - sourcePaths.Add(args.SourceIndex); - args.Children = Observable.Return(args.Source as IEnumerable); - }; - } - - var startPath = Path(1, 0, 1, 0); - Select(selectionModel, startPath, true); - ValidateSelection(selectionModel, startPath); - - var endPath = Path(1, 1, 1, 0); - SelectRangeFromAnchor(selectionModel, endPath, true /* select */); - - if (handleChildrenRequested) - { - // Validate SourceIndices. - var expectedSourceIndices = new List() - { - Path(1), - Path(1, 0), - Path(1, 0, 1), - Path(1, 1), - Path(1, 0, 1, 3), - Path(1, 0, 1, 2), - Path(1, 0, 1, 1), - Path(1, 0, 1, 0), - Path(1, 1, 1), - Path(1, 1, 0), - Path(1, 1, 0, 3), - Path(1, 1, 0, 2), - Path(1, 1, 0, 1), - Path(1, 1, 0, 0), - Path(1, 1, 1, 0) - }; - - Assert.Equal(expectedSourceIndices.Count, sourcePaths.Count); - for (int i = 0; i < expectedSourceIndices.Count; i++) - { - Assert.True(AreEqual(expectedSourceIndices[i], sourcePaths[i])); - } - } - - ValidateSelection(selectionModel, - Path(1, 1), - Path(1, 0, 1, 0), - Path(1, 0, 1, 1), - Path(1, 0, 1, 2), - Path(1, 0, 1, 3), - Path(1, 1, 0), - Path(1, 1, 1), - Path(1, 1, 0, 0), - Path(1, 1, 0, 1), - Path(1, 1, 0, 2), - Path(1, 1, 0, 3), - Path(1, 1, 1, 0)); - - ClearSelection(selectionModel); - ValidateSelection(selectionModel); - - startPath = Path(0, 1, 0, 2); - SetAnchorIndex(selectionModel, startPath); - endPath = Path(0, 0, 0, 2); - SelectRangeFromAnchor(selectionModel, endPath, true /* select */); - ValidateSelection(selectionModel, - Path(0, 1), - Path(0, 0, 1), - Path(0, 0, 0, 2), - Path(0, 0, 0, 3), - Path(0, 0, 1, 0), - Path(0, 0, 1, 1), - Path(0, 0, 1, 2), - Path(0, 0, 1, 3), - Path(0, 1, 0), - Path(0, 1, 0, 0), - Path(0, 1, 0, 1), - Path(0, 1, 0, 2)); - - startPath = Path(0, 1, 0, 2); - SetAnchorIndex(selectionModel, startPath); - endPath = Path(0, 0, 0, 2); - SelectRangeFromAnchor(selectionModel, endPath, false /* select */); - ValidateSelection(selectionModel); - } - - [Fact] - public void ValidateInserts() - { - var data = new ObservableCollection(Enumerable.Range(0, 10)); - var selectionModel = new SelectionModel(); - selectionModel.Source = data; - - selectionModel.Select(3); - selectionModel.Select(4); - selectionModel.Select(5); - ValidateSelection(selectionModel, - Path(3), - Path(4), - Path(5)); - - _output.WriteLine("Insert in selected range: Inserting 3 items at index 4"); - data.Insert(4, 41); - data.Insert(4, 42); - data.Insert(4, 43); - ValidateSelection(selectionModel, - Path(3), - Path(7), - Path(8)); - - _output.WriteLine("Insert before selected range: Inserting 3 items at index 0"); - data.Insert(0, 100); - data.Insert(0, 101); - data.Insert(0, 102); - ValidateSelection(selectionModel, - Path(6), - Path(10), - Path(11)); - - _output.WriteLine("Insert after selected range: Inserting 3 items at index 12"); - data.Insert(12, 1000); - data.Insert(12, 1001); - data.Insert(12, 1002); - ValidateSelection(selectionModel, - Path(6), - Path(10), - Path(11)); - } - - [Fact] - public void ValidateGroupInserts() - { - var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); - var selectionModel = new SelectionModel(); - selectionModel.Source = data; - - selectionModel.Select(1, 1); - ValidateSelection(selectionModel, Path(1, 1)); - - _output.WriteLine("Insert before selected range: Inserting item at group index 0"); - data.Insert(0, 100); - ValidateSelection(selectionModel, Path(2, 1)); - - _output.WriteLine("Insert after selected range: Inserting item at group index 3"); - data.Insert(3, 1000); - ValidateSelection(selectionModel, Path(2, 1)); - } - - [Fact] - public void ValidateRemoves() - { - var data = new ObservableCollection(Enumerable.Range(0, 10)); - var selectionModel = new SelectionModel(); - selectionModel.Source = data; - - selectionModel.Select(6); - selectionModel.Select(7); - selectionModel.Select(8); - ValidateSelection(selectionModel, - Path(6), - Path(7), - Path(8)); - - _output.WriteLine("Remove before selected range: Removing item at index 0"); - data.RemoveAt(0); - ValidateSelection(selectionModel, - Path(5), - Path(6), - Path(7)); - - _output.WriteLine("Remove from before to middle of selected range: Removing items at index 3, 4, 5"); - data.RemoveAt(3); - data.RemoveAt(3); - data.RemoveAt(3); - ValidateSelection(selectionModel, Path(3), Path(4)); - - _output.WriteLine("Remove after selected range: Removing item at index 5"); - data.RemoveAt(5); - ValidateSelection(selectionModel, Path(3), Path(4)); - } - - [Fact] - public void ValidateGroupRemoves() - { - var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); - var selectionModel = new SelectionModel(); - selectionModel.Source = data; - - selectionModel.Select(1, 1); - selectionModel.Select(1, 2); - ValidateSelection(selectionModel, Path(1, 1), Path(1, 2)); - - _output.WriteLine("Remove before selected range: Removing item at group index 0"); - data.RemoveAt(0); - ValidateSelection(selectionModel, Path(0, 1), Path(0, 2)); - - _output.WriteLine("Remove after selected range: Removing item at group index 1"); - data.RemoveAt(1); - ValidateSelection(selectionModel, Path(0, 1), Path(0, 2)); - - _output.WriteLine("Remove group containing selected items"); - - var raised = 0; - - selectionModel.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Equal(new object[] { 4, 5, }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - data.RemoveAt(0); - ValidateSelection(selectionModel); - Assert.Equal(1, raised); - } - - [Fact] - public void CanReplaceItem() - { - var data = new ObservableCollection(Enumerable.Range(0, 10)); - var selectionModel = new SelectionModel(); - selectionModel.Source = data; - - selectionModel.Select(3); - selectionModel.Select(4); - selectionModel.Select(5); - ValidateSelection(selectionModel, Path(3), Path(4), Path(5)); - - data[3] = 300; - data[4] = 400; - ValidateSelection(selectionModel, Path(5)); - } - - [Fact] - public void ValidateGroupReplaceLosesSelection() - { - var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); - var selectionModel = new SelectionModel(); - selectionModel.Source = data; - - selectionModel.Select(1, 1); - ValidateSelection(selectionModel, Path(1, 1)); - - data[1] = new ObservableCollection(Enumerable.Range(0, 5)); - ValidateSelection(selectionModel); - } - - [Fact] - public void ValidateClear() - { - var data = new ObservableCollection(Enumerable.Range(0, 10)); - var selectionModel = new SelectionModel(); - selectionModel.Source = data; - - selectionModel.Select(3); - selectionModel.Select(4); - selectionModel.Select(5); - ValidateSelection(selectionModel, Path(3), Path(4), Path(5)); - - data.Clear(); - ValidateSelection(selectionModel); - } - - [Fact] - public void ValidateGroupClear() - { - var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); - var selectionModel = new SelectionModel(); - selectionModel.Source = data; - - selectionModel.Select(1, 1); - ValidateSelection(selectionModel, Path(1, 1)); - - (data[1] as IList).Clear(); - ValidateSelection(selectionModel); - } - - // In some cases the leaf node might get a collection change that affects an ancestors selection - // state. In this case we were not raising selection changed event. For example, if all elements - // in a group are selected and a new item gets inserted - the parent goes from selected to partially - // selected. In that case we need to raise the selection changed event so that the header containers - // can show the correct visual. - [Fact] - public void ValidateEventWhenInnerNodeChangesSelectionState() - { - bool selectionChangedRaised = false; - var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); - var selectionModel = new SelectionModel(); - selectionModel.Source = data; - selectionModel.SelectionChanged += (sender, args) => { selectionChangedRaised = true; }; - - selectionModel.Select(1, 0); - selectionModel.Select(1, 1); - selectionModel.Select(1, 2); - ValidateSelection(selectionModel, Path(1, 0), Path(1, 1), Path(1, 2)); - - _output.WriteLine("Inserting 1.0"); - selectionChangedRaised = false; - (data[1] as AvaloniaList).Insert(0, 100); - Assert.True(selectionChangedRaised, "SelectionChanged event was not raised"); - ValidateSelection(selectionModel, Path(1, 1), Path(1, 2), Path(1, 3)); - - _output.WriteLine("Removing 1.0"); - selectionChangedRaised = false; - (data[1] as AvaloniaList).RemoveAt(0); - Assert.True(selectionChangedRaised, "SelectionChanged event was not raised"); - ValidateSelection(selectionModel, - Path(1, 0), - Path(1, 1), - Path(1, 2)); - } - - [Fact] - public void ValidatePropertyChangedEventIsRaised() - { - var selectionModel = new SelectionModel(); - _output.WriteLine("Set the source to 10 items"); - selectionModel.Source = Enumerable.Range(0, 10).ToList(); - - bool selectedIndexChanged = false; - bool selectedIndicesChanged = false; - bool SelectedItemChanged = false; - bool SelectedItemsChanged = false; - bool AnchorIndexChanged = false; - selectionModel.PropertyChanged += (sender, args) => - { - switch (args.PropertyName) - { - case "SelectedIndex": - selectedIndexChanged = true; - break; - case "SelectedIndices": - selectedIndicesChanged = true; - break; - case "SelectedItem": - SelectedItemChanged = true; - break; - case "SelectedItems": - SelectedItemsChanged = true; - break; - case "AnchorIndex": - AnchorIndexChanged = true; - break; - - default: - throw new InvalidOperationException(); - } - }; - - Select(selectionModel, 3, true); - - Assert.True(selectedIndexChanged); - Assert.True(selectedIndicesChanged); - Assert.True(SelectedItemChanged); - Assert.True(SelectedItemsChanged); - Assert.True(AnchorIndexChanged); - } - - [Fact] - public void CanExtendSelectionModelINPC() - { - var selectionModel = new CustomSelectionModel(); - bool intPropertyChanged = false; - selectionModel.PropertyChanged += (sender, args) => - { - if (args.PropertyName == "IntProperty") - { - intPropertyChanged = true; - } - }; - - selectionModel.IntProperty = 5; - Assert.True(intPropertyChanged); - } - - [Fact] - public void SelectRangeRegressionTest() - { - var selectionModel = new SelectionModel() - { - Source = CreateNestedData(1, 2, 3) - }; - - // length of start smaller than end used to cause an out of range error. - selectionModel.SelectRange(IndexPath.CreateFrom(0), IndexPath.CreateFrom(1, 1)); - - ValidateSelection(selectionModel, - Path(0), - Path(1), - Path(0, 0), - Path(0, 1), - Path(0, 2), - Path(1, 0), - Path(1, 1)); - } - - [Fact] - public void SelectRange_Should_Select_Nested_Items_On_Different_Levels() - { - var target = new SelectionModel(); - var data = CreateNestedData(1, 2, 3); - - target.Source = data; - target.AnchorIndex = new IndexPath(0, 1); - target.SelectRange(Path(0, 1), Path(1)); - - Assert.Equal( - new[] - { - Path(1), - Path(0, 1), - Path(0, 2), - }, - target.SelectedIndices); - } - - [Fact] - public void Should_Listen_For_Changes_After_Deselect() - { - var target = new SelectionModel(); - var data = CreateNestedData(1, 2, 3); - - target.Source = data; - target.Select(1, 0); - target.Deselect(1, 0); - target.Select(1, 0); - ((AvaloniaList)data[1]).Insert(0, "foo"); - - Assert.Equal(new IndexPath(1, 1), target.SelectedIndex); - } - - [Fact] - public void Selecting_Item_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(4) }, e.SelectedIndices); - Assert.Equal(new object[] { 4 }, e.SelectedItems); - ++raised; - }; - - target.Select(4); - - Assert.Equal(1, raised); - } - - [Fact] - public void Selecting_Already_Selected_Item_Doesnt_Raise_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.Select(4); - target.SelectionChanged += (s, e) => ++raised; - target.Select(4); - - Assert.Equal(0, raised); - } - - [Fact] - public void SingleSelecting_Item_Raises_SelectionChanged() - { - var target = new SelectionModel { SingleSelect = true }; - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.Select(3); - - target.SelectionChanged += (s, e) => - { - Assert.Equal(new[] { new IndexPath(3) }, e.DeselectedIndices); - Assert.Equal(new object[] { 3 }, e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(4) }, e.SelectedIndices); - Assert.Equal(new object[] { 4 }, e.SelectedItems); - ++raised; - }; - - target.Select(4); - - Assert.Equal(1, raised); - } - - [Fact] - public void SingleSelecting_Already_Selected_Item_Doesnt_Raise_SelectionChanged() - { - var target = new SelectionModel { SingleSelect = true }; - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.Select(4); - target.SelectionChanged += (s, e) => ++raised; - target.Select(4); - - Assert.Equal(0, raised); - } - - [Fact] - public void Selecting_Item_With_Group_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = CreateNestedData(1, 2, 3); - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(1, 1) }, e.SelectedIndices); - Assert.Equal(new object[] { 4 }, e.SelectedItems); - ++raised; - }; - - target.Select(1, 1); - - Assert.Equal(1, raised); - } - - [Fact] - public void SelectAt_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = CreateNestedData(1, 2, 3); - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(1, 1) }, e.SelectedIndices); - Assert.Equal(new object[] { 4 }, e.SelectedItems); - ++raised; - }; - - target.SelectAt(new IndexPath(1, 1)); - - Assert.Equal(1, raised); - } - - [Fact] - public void SelectAll_Raises_SelectionChanged() - { - var target = new SelectionModel { SingleSelect = true }; - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.SelectionChanged += (s, e) => - { - var expected = Enumerable.Range(0, 10); - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(expected.Select(x => new IndexPath(x)), e.SelectedIndices); - Assert.Equal(expected, e.SelectedItems.Cast()); - ++raised; - }; - - target.SelectAll(); - - Assert.Equal(1, raised); - } - - [Fact] - public void SelectAll_With_Already_Selected_Items_Raises_SelectionChanged() - { - var target = new SelectionModel { SingleSelect = true }; - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.Select(4); - - target.SelectionChanged += (s, e) => - { - var expected = Enumerable.Range(0, 10).Except(new[] { 4 }); - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(expected.Select(x => new IndexPath(x)), e.SelectedIndices); - Assert.Equal(expected, e.SelectedItems.Cast()); - ++raised; - }; - - target.SelectAll(); - - Assert.Equal(1, raised); - } - - [Fact] - public void SelectRangeFromAnchor_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.SelectionChanged += (s, e) => - { - var expected = Enumerable.Range(4, 3); - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(expected.Select(x => new IndexPath(x)), e.SelectedIndices); - Assert.Equal(expected, e.SelectedItems.Cast()); - ++raised; - }; - - target.AnchorIndex = new IndexPath(4); - target.SelectRangeFromAnchor(6); - - Assert.Equal(1, raised); - } - - [Fact] - public void SelectRangeFromAnchor_With_Group_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = CreateNestedData(1, 2, 10); - target.SelectionChanged += (s, e) => - { - var expected = Enumerable.Range(11, 6); - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(expected.Select(x => new IndexPath(x / 10, x % 10)), e.SelectedIndices); - Assert.Equal(expected, e.SelectedItems.Cast()); - ++raised; - }; - - target.AnchorIndex = new IndexPath(1, 1); - target.SelectRangeFromAnchor(1, 6); - - Assert.Equal(1, raised); - } - - [Fact] - public void SelectRangeFromAnchorTo_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = CreateNestedData(1, 2, 10); - target.SelectionChanged += (s, e) => - { - var expected = Enumerable.Range(11, 6); - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(expected.Select(x => new IndexPath(x / 10, x % 10)), e.SelectedIndices); - Assert.Equal(expected, e.SelectedItems.Cast()); - ++raised; - }; - - target.AnchorIndex = new IndexPath(1, 1); - target.SelectRangeFromAnchorTo(new IndexPath(1, 6)); - - Assert.Equal(1, raised); - } - - [Fact] - public void ClearSelection_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.Select(4); - target.Select(5); - - target.SelectionChanged += (s, e) => - { - var expected = Enumerable.Range(4, 2); - Assert.Equal(expected.Select(x => new IndexPath(x)), e.DeselectedIndices); - Assert.Equal(expected, e.DeselectedItems.Cast()); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - target.ClearSelection(); - - Assert.Equal(1, raised); - } - - [Fact] - public void Clearing_Nested_Selection_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = CreateNestedData(1, 2, 3); - target.Select(1, 1); - - target.SelectionChanged += (s, e) => - { - Assert.Equal(new[] { new IndexPath(1, 1) }, e.DeselectedIndices); - Assert.Equal(new object[] { 4 }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - target.ClearSelection(); - - Assert.Equal(1, raised); - } - - [Fact] - public void Changing_Source_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.Select(4); - target.Select(5); - - target.SelectionChanged += (s, e) => - { - var expected = Enumerable.Range(4, 2); - Assert.Equal(expected.Select(x => new IndexPath(x)), e.DeselectedIndices); - Assert.Equal(expected, e.DeselectedItems.Cast()); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - target.Source = Enumerable.Range(20, 10).ToList(); - - Assert.Equal(1, raised); - } - - [Fact] - public void Setting_SelectedIndex_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.Select(4); - target.Select(5); - - target.SelectionChanged += (s, e) => - { - Assert.Equal(new[] { new IndexPath(4), new IndexPath(5) }, e.DeselectedIndices); - Assert.Equal(new object[] { 4, 5 }, e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(6) }, e.SelectedIndices); - Assert.Equal(new object[] { 6 }, e.SelectedItems); - ++raised; - }; - - target.SelectedIndex = new IndexPath(6); - - Assert.Equal(1, raised); - } - - [Fact] - public void Removing_Selected_Item_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var data = new ObservableCollection(Enumerable.Range(0, 10)); - var raised = 0; - - target.Source = data; - target.Select(4); - target.Select(5); - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Equal(new object[] { 4 }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - data.Remove(4); - - Assert.Equal(1, raised); - } - - [Fact] - public void Removing_Selected_Child_Item_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var data = CreateNestedData(1, 2, 3); - var raised = 0; - - target.Source = data; - target.SelectRange(new IndexPath(0), new IndexPath(1, 1)); - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Equal(new object[] { 1}, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - ((AvaloniaList)data[0]).RemoveAt(1); - - Assert.Equal(1, raised); - } - - [Fact] - public void Removing_Selected_Item_With_Children_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var data = CreateNestedData(1, 2, 3); - var raised = 0; - - target.Source = data; - target.SelectRange(new IndexPath(0), new IndexPath(1, 1)); - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Equal(new object[] { new AvaloniaList { 0, 1, 2 }, 0, 1, 2 }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - data.RemoveAt(0); - - Assert.Equal(1, raised); - } - - [Fact] - public void Removing_Unselected_Item_Before_Selected_Item_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var data = new ObservableCollection(Enumerable.Range(0, 10)); - var raised = 0; - - target.Source = data; - target.Select(8); - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - data.Remove(6); - - Assert.Equal(1, raised); - } - - [Fact] - public void Removing_Unselected_Item_After_Selected_Item_Doesnt_Raise_SelectionChanged() - { - var target = new SelectionModel(); - var data = new ObservableCollection(Enumerable.Range(0, 10)); - var raised = 0; - - target.Source = data; - target.Select(4); - - target.SelectionChanged += (s, e) => ++raised; - - data.Remove(6); - - Assert.Equal(0, raised); - } - - [Fact] - public void Disposing_Unhooks_CollectionChanged_Handlers() - { - var data = CreateNestedData(2, 2, 2); - var target = new SelectionModel { Source = data }; - - target.SelectAll(); - VerifyCollectionChangedHandlers(1, data); - - target.Dispose(); - - VerifyCollectionChangedHandlers(0, data); - } - - [Fact] - public void Clearing_Selection_Unhooks_CollectionChanged_Handlers() - { - var data = CreateNestedData(2, 2, 2); - var target = new SelectionModel { Source = data }; - - target.SelectAll(); - VerifyCollectionChangedHandlers(1, data); - - target.ClearSelection(); - - // Root subscription not unhooked until SelectionModel is disposed. - Assert.Equal(1, GetSubscriberCount(data)); - - foreach (AvaloniaList i in data) - { - VerifyCollectionChangedHandlers(0, i); - } - } - - [Fact] - public void Removing_Item_Unhooks_CollectionChanged_Handlers() - { - var data = CreateNestedData(2, 2, 2); - var target = new SelectionModel { Source = data }; - - target.SelectAll(); - - var toRemove = (AvaloniaList)data[1]; - data.Remove(toRemove); - - Assert.Equal(0, GetSubscriberCount(toRemove)); - } - - [Fact] - public void SelectRange_Behaves_The_Same_As_Multiple_Selects() - { - var data = new[] { 1, 2, 3 }; - var target = new SelectionModel { Source = data }; - - target.Select(1); - - Assert.Equal(new[] { IndexPath.CreateFrom(1) }, target.SelectedIndices); - - target.ClearSelection(); - target.SelectRange(new IndexPath(1), new IndexPath(1)); - - Assert.Equal(new[] { IndexPath.CreateFrom(1) }, target.SelectedIndices); - } - - [Fact] - public void SelectRange_Behaves_The_Same_As_Multiple_Selects_Nested() - { - var data = CreateNestedData(3, 2, 2); - var target = new SelectionModel { Source = data }; - - target.Select(1); - - Assert.Equal(new[] { IndexPath.CreateFrom(1) }, target.SelectedIndices); - - target.ClearSelection(); - target.SelectRange(new IndexPath(1), new IndexPath(1)); - - Assert.Equal(new[] { IndexPath.CreateFrom(1) }, target.SelectedIndices); - } - - [Fact] - public void Should_Not_Treat_Strings_As_Nested_Selections() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data }; - - target.SelectAll(); - - Assert.Equal(3, target.SelectedItems.Count); - } - - [Fact] - public void Not_Enumerating_Changes_Does_Not_Prevent_Further_Operations() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data }; - - target.SelectionChanged += (s, e) => { }; - - target.SelectAll(); - target.ClearSelection(); - } - - [Fact] - public void Can_Change_Selection_From_SelectionChanged() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data }; - var raised = 0; - - target.SelectionChanged += (s, e) => - { - if (raised++ == 0) - { - target.ClearSelection(); - } - }; - - target.SelectAll(); - - Assert.Equal(2, raised); - } - - [Fact] - public void Raises_SelectionChanged_With_No_Source() - { - var target = new SelectionModel(); - var raised = 0; - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - target.Select(1); - - Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); - Assert.Empty(target.SelectedItems); - } - - [Fact] - public void Raises_SelectionChanged_With_Items_After_Source_Is_Set() - { - var target = new SelectionModel(); - var raised = 0; - - target.Select(1); - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices); - Assert.Equal(new[] { "bar" }, e.SelectedItems); - ++raised; - }; - - target.Source = new[] { "foo", "bar", "baz" }; - - Assert.Equal(1, raised); - } - - [Fact] - public void RetainSelectionOnReset_Retains_Selection_On_Reset() - { - var data = new ResettingList { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; - - target.SelectRange(new IndexPath(1), new IndexPath(2)); - data.Reset(); - - Assert.Equal(new[] { new IndexPath(1), new IndexPath(2) }, target.SelectedIndices); - Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); - } - - [Fact] - public void RetainSelectionOnReset_Retains_Correct_Selection_After_Deselect() - { - var data = new ResettingList { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; - - target.SelectRange(new IndexPath(1), new IndexPath(2)); - target.Deselect(2); - data.Reset(); - - Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); - Assert.Equal(new[] { "bar" }, target.SelectedItems); - } - - [Fact] - public void RetainSelectionOnReset_Retains_Correct_Selection_After_Remove_1() - { - var data = new ResettingList { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; - - target.SelectRange(new IndexPath(1), new IndexPath(2)); - data.RemoveAt(2); - data.Reset(new[] { "foo", "bar", "baz" }); - - Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); - Assert.Equal(new[] { "bar" }, target.SelectedItems); - } - - [Fact] - public void RetainSelectionOnReset_Retains_Correct_Selection_After_Remove_2() - { - var data = new ResettingList { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; - - target.SelectRange(new IndexPath(1), new IndexPath(2)); - data.RemoveAt(0); - data.Reset(new[] { "foo", "bar", "baz" }); - - Assert.Equal(new[] { new IndexPath(1), new IndexPath(2) }, target.SelectedIndices); - Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); - } - - [Fact] - public void RetainSelectionOnReset_Retains_No_Selection_After_Clear() - { - var data = new ResettingList { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; - - target.SelectRange(new IndexPath(1), new IndexPath(2)); - target.ClearSelection(); - data.Reset(); - - Assert.Empty(target.SelectedIndices); - Assert.Empty(target.SelectedItems); - } - - [Fact] - public void RetainSelectionOnReset_Retains_Correct_Selection_After_Two_Resets() - { - var data = new ResettingList { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; - - target.SelectRange(new IndexPath(1), new IndexPath(2)); - data.Reset(new[] { "foo", "bar" }); - data.Reset(new[] { "foo", "bar", "baz" }); - - Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); - Assert.Equal(new[] { "bar", }, target.SelectedItems); - } - - [Fact] - public void RetainSelectionOnReset_Raises_Empty_SelectionChanged_On_Reset_With_No_Changes() - { - var data = new ResettingList { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; - var raised = 0; - - target.SelectRange(new IndexPath(1), new IndexPath(2)); - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - data.Reset(); - } - - [Fact] - public void RetainSelectionOnReset_Raises_SelectionChanged_On_Reset_With_Removed_Items() - { - var data = new ResettingList { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; - var raised = 0; - - target.SelectRange(new IndexPath(1), new IndexPath(2)); - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Equal(new[] { "bar" }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - data.Reset(new[] { "foo", "baz" }); - - Assert.Equal(1, raised); - } - - [Fact] - public void RetainSelectionOnReset_Handles_Null_Source() - { - var data = new ResettingList { "foo", "bar", "baz" }; - var target = new SelectionModel { RetainSelectionOnReset = true }; - var raised = 0; - - target.SelectionChanged += (s, e) => - { - if (raised == 0) - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices); - Assert.Empty(e.SelectedItems); - } - else if (raised == 1) - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices); - Assert.Equal(new[] { "bar" }, e.SelectedItems); - } - else if (raised == 3) - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - } - - ++raised; - }; - - target.Select(1); - Assert.Equal(1, raised); - - target.Source = data; - Assert.Equal(2, raised); - Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); - - data.Reset(new[] { "qux", "foo", "bar", "baz" }); - Assert.Equal(3, raised); - Assert.Equal(new[] { new IndexPath(2) }, target.SelectedIndices); - } - - [Fact] - public void Can_Batch_Update() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = Enumerable.Range(0, 10).ToList(); - target.Select(1); - - target.SelectionChanged += (s, e) => - { - Assert.Equal(new[] { new IndexPath(1) }, e.DeselectedIndices); - Assert.Equal(new object[] { 1 }, e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(4) }, e.SelectedIndices); - Assert.Equal(new object[] { 4 }, e.SelectedItems); - ++raised; - }; - - using (target.Update()) - { - target.Deselect(1); - target.Select(4); - } - - Assert.Equal(1, raised); - } - - [Fact] - public void Batch_Update_Clear_Nested_Data_Raises_SelectionChanged() - { - var target = new SelectionModel(); - var raised = 0; - - target.Source = CreateNestedData(3, 2, 2); - target.SelectRange(new IndexPath(0), new IndexPath(1, 1)); - - Assert.Equal(24, target.SelectedIndices.Count); - - var indices = target.SelectedIndices.ToList(); - var items = target.SelectedItems.ToList(); - - target.SelectionChanged += (s, e) => - { - Assert.Equal(indices, e.DeselectedIndices); - Assert.Equal(items, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - using (target.Update()) - { - target.ClearSelection(); - } - - Assert.Equal(1, raised); - } - - [Fact] - public void Batch_Update_Does_Not_Raise_PropertyChanged_Until_Operation_Finished() - { - var data = new[] { "foo", "bar", "baz", "qux" }; - var target = new SelectionModel { Source = data }; - var raised = 0; - - target.SelectedIndex = new IndexPath(1); - - Assert.Equal(new IndexPath(1), target.AnchorIndex); - - target.PropertyChanged += (s, e) => ++raised; - - using (target.Update()) - { - target.ClearSelection(); - - Assert.Equal(0, raised); - - target.AnchorIndex = new IndexPath(2); - - Assert.Equal(0, raised); - - target.SelectedIndex = new IndexPath(3); - - Assert.Equal(0, raised); - } - - Assert.Equal(new IndexPath(3), target.AnchorIndex); - Assert.Equal(5, raised); - } - - [Fact] - public void Batch_Update_Does_Not_Raise_PropertyChanged_If_Nothing_Changed() - { - var data = new[] { "foo", "bar", "baz", "qux" }; - var target = new SelectionModel { Source = data }; - var raised = 0; - - target.SelectedIndex = new IndexPath(1); - - Assert.Equal(new IndexPath(1), target.AnchorIndex); - - target.PropertyChanged += (s, e) => ++raised; - - using (target.Update()) - { - target.ClearSelection(); - target.SelectedIndex = new IndexPath(1); - } - - Assert.Equal(0, raised); - } - - [Fact] - public void Batch_Update_Selection_Is_Correct_Throughout() - { - var data = new[] { "foo", "bar", "baz", "qux" }; - var target = new SelectionModel { Source = data }; - var raised = 0; - - using (target.Update()) - { - target.Select(1); - - Assert.Equal(new IndexPath(1), target.SelectedIndex); - Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); - Assert.Equal("bar", target.SelectedItem); - Assert.Equal(new[] { "bar" }, target.SelectedItems); - - target.Deselect(1); - - Assert.Equal(new IndexPath(), target.SelectedIndex); - Assert.Empty(target.SelectedIndices); - Assert.Null(target.SelectedItem); - Assert.Empty(target.SelectedItems); - - target.SelectRange(new IndexPath(1), new IndexPath(1)); - - Assert.Equal(new IndexPath(1), target.SelectedIndex); - Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); - Assert.Equal("bar", target.SelectedItem); - Assert.Equal(new[] { "bar" }, target.SelectedItems); - - target.ClearSelection(); - - Assert.Equal(new IndexPath(), target.SelectedIndex); - Assert.Empty(target.SelectedIndices); - Assert.Null(target.SelectedItem); - Assert.Empty(target.SelectedItems); - } - - Assert.Equal(0, raised); - } - - [Fact] - public void AutoSelect_Selects_When_Enabled() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data }; - var raised = 0; - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); - Assert.Equal(new[] { "foo" }, e.SelectedItems); - ++raised; - }; - - target.AutoSelect = true; - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - Assert.Equal(1, raised); - } - - [Fact] - public void AutoSelect_Selects_When_Source_Assigned() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { AutoSelect = true }; - var raised = 0; - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); - Assert.Equal(new[] { "foo" }, e.SelectedItems); - ++raised; - }; - - target.Source = data; - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - Assert.Equal(1, raised); - } - - [Fact] - public void AutoSelect_Selects_When_New_Source_Assigned_And_Old_Source_Has_Selection() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { AutoSelect = true, Source = data }; - var raised = 0; - - target.SelectionChanged += (s, e) => - { - if (raised == 0) - { - Assert.Equal(new[] { new IndexPath(0) }, e.DeselectedIndices); - Assert.Equal(new[] { "foo" }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - } - else - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); - Assert.Equal(new[] { "newfoo" }, e.SelectedItems); - } - ++raised; - }; - - target.Source = new[] { "newfoo" }; - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - Assert.Equal(2, raised); - } - - [Fact] - public void AutoSelect_Selects_When_First_Item_Added() - { - var data = new ObservableCollection(); - var target = new SelectionModel { AutoSelect = true , Source = data }; - var raised = 0; - - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); - Assert.Equal(new[] { "foo" }, e.SelectedItems); - ++raised; - }; - - data.Add("foo"); - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - Assert.Equal(1, raised); - } - - [Fact] - public void AutoSelect_Selects_When_Selected_Item_Removed() - { - var data = new ObservableCollection { "foo", "bar", "baz" }; - var target = new SelectionModel { AutoSelect = true, Source = data }; - var raised = 0; - - target.SelectedIndex = new IndexPath(2); - - target.SelectionChanged += (s, e) => - { - if (raised == 0) - { - Assert.Empty(e.DeselectedIndices); - Assert.Equal(new[] { "baz" }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - } - else - { - Assert.Empty(e.DeselectedIndices); - Assert.Empty(e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); - Assert.Equal(new[] { "foo" }, e.SelectedItems); - } - - ++raised; - }; - - data.RemoveAt(2); - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - Assert.Equal(2, raised); - } - - [Fact] - public void AutoSelect_Selects_On_Deselection() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { AutoSelect = true, Source = data }; - var raised = 0; - - target.SelectedIndex = new IndexPath(2); - - target.SelectionChanged += (s, e) => - { - Assert.Equal(new[] { new IndexPath(2) }, e.DeselectedIndices); - Assert.Equal(new[] { "baz" }, e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); - Assert.Equal(new[] { "foo" }, e.SelectedItems); - ++raised; - }; - - target.Deselect(2); - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - Assert.Equal(1, raised); - } - - [Fact] - public void AutoSelect_Selects_On_ClearSelection() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { AutoSelect = true, Source = data }; - var raised = 0; - - target.SelectedIndex = new IndexPath(2); - - target.SelectionChanged += (s, e) => - { - Assert.Equal(new[] { new IndexPath(2) }, e.DeselectedIndices); - Assert.Equal(new[] { "baz" }, e.DeselectedItems); - Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); - Assert.Equal(new[] { "foo" }, e.SelectedItems); - ++raised; - }; - - target.ClearSelection(); - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - Assert.Equal(1, raised); - } - - [Fact] - public void AutoSelect_Overrides_Deselecting_First_Item() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { AutoSelect = true, Source = data }; - var raised = 0; - - target.Select(0); - - target.SelectionChanged += (s, e) => - { - ++raised; - }; - - target.Deselect(0); - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - Assert.Equal(0, raised); - } - - [Fact] - public void AutoSelect_Is_Applied_At_End_Of_Batch_Update() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { AutoSelect = true, Source = data }; - - using (target.Update()) - { - target.ClearSelection(); - - Assert.Equal(new IndexPath(), target.SelectedIndex); - Assert.Empty(target.SelectedIndices); - Assert.Null(target.SelectedItem); - Assert.Empty(target.SelectedItems); - } - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - Assert.Equal(new[] { new IndexPath(0) }, target.SelectedIndices); - Assert.Equal("foo", target.SelectedItem); - Assert.Equal(new[] { "foo" }, target.SelectedItems); - - Assert.Equal(new IndexPath(0), target.SelectedIndex); - } - - [Fact] - public void Can_Replace_Parent_Children_Collection() - { - var root = new Node("Root"); - var target = new SelectionModel { Source = new[] { root } }; - var raised = 0; - - target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); - - target.Select(0, 9); - - var selected = (Node)target.SelectedItem; - Assert.Equal("Child 9", selected.Header); - - target.SelectionChanged += (s, e) => - { - Assert.Equal(new[] { Path(0, 9) }, e.DeselectedIndices); - Assert.Equal(new[] { selected }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - root.ReplaceChildren(); - - Assert.Null(target.SelectedItem); - Assert.Equal(1, raised); - } - - [Fact] - public void Can_Replace_Grandparent_Children_Collection() - { - var root = new Node("Root"); - var target = new SelectionModel { Source = new[] { root } }; - var raised = 0; - - target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); - - target.SelectAt(Path(0, 9, 1)); - - var selected = (Node)target.SelectedItem; - Assert.Equal("Child 1", selected.Header); - - target.SelectionChanged += (s, e) => - { - Assert.Equal(new[] { Path(0, 9, 1) }, e.DeselectedIndices); - Assert.Equal(new[] { selected }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - ++raised; - }; - - root.ReplaceChildren(); - - Assert.Null(target.SelectedItem); - Assert.Equal(1, raised); - } - - [Fact] - public void Child_Resolver_Is_Unsubscribed_When_Source_Changed() - { - var root = new Node("Root"); - var target = new SelectionModel { Source = new[] { root } }; - target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); - - target.Select(0, 9); - - Assert.Equal(1, root.PropertyChangedSubscriptions); - - target.Source = null; - - Assert.Equal(0, root.PropertyChangedSubscriptions); - } - - [Fact] - public void Child_Resolver_Is_Unsubscribed_When_Parent_Removed() - { - var root = new Node("Root"); - var target = new SelectionModel { Source = new[] { root } }; - var node = root.Children[1]; - var path = new IndexPath(new[] { 0, 1, 1 }); - - target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); - - target.SelectAt(path); - - Assert.Equal(1, node.PropertyChangedSubscriptions); - - root.ReplaceChildren(); - - Assert.Equal(0, node.PropertyChangedSubscriptions); - } - - [Fact] - public void Setting_SelectedIndex_To_Minus_1_Clears_Selection() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { Source = data }; - target.SelectedIndex = new IndexPath(1); - target.SelectedIndex = new IndexPath(-1); - Assert.Empty(target.SelectedIndices); - } - - [Fact] - public void Assigning_Source_With_Less_Items_Than_Previous_Clears_Selection() - { - var data = new[] { "foo", "bar", "baz", "boo", "hoo" }; - var smallerData = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { RetainSelectionOnReset = true }; - target.Source = data; - target.SelectedIndex = new IndexPath(4); - target.Source = smallerData; - Assert.Empty(target.SelectedIndices); - } - - [Fact] - public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel(); - target.SelectedIndex = new IndexPath(4); - target.Source = data; - Assert.Empty(target.SelectedIndices); - } - - [Fact] - public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection_RetainSelection() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { RetainSelectionOnReset = true }; - target.SelectedIndex = new IndexPath(4); - target.Source = data; - Assert.Empty(target.SelectedIndices); - } - - [Fact] - public void Initializing_Source_With_Less_Items_Than_Multiple_Selection_Trims_Selection() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { RetainSelectionOnReset = true }; - target.Select(4); - target.Select(2); - target.Source = data; - Assert.Equal(1, target.SelectedIndices.Count); - Assert.Equal(new IndexPath(2), target.SelectedIndices.First()); - } - - [Fact] - public void Initializing_Source_With_Less_Items_Than_Selection_Raises_SelectionChanged() - { - var data = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel(); - var raised = 0; - - target.SelectedIndex = new IndexPath(4); - - target.SelectionChanged += (s, e) => - { - if (raised == 0) - { - Assert.Equal(new[] { Path(4) }, e.DeselectedIndices); - Assert.Equal(new object[] { null }, e.DeselectedItems); - Assert.Empty(e.SelectedIndices); - Assert.Empty(e.SelectedItems); - } - - ++raised; - }; - - target.Source = data; - - Assert.Equal(2, raised); - } - - private int GetSubscriberCount(AvaloniaList list) - { - return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; - } - - private void VerifyCollectionChangedHandlers(int expected, AvaloniaList list) - { - var count = GetSubscriberCount(list); - - Assert.Equal(expected, count); - - foreach (var i in list) - { - if (i is AvaloniaList l) - { - VerifyCollectionChangedHandlers(expected, l); - } - } - } - - private void Select(SelectionModel manager, int index, bool select) - { - _output.WriteLine((select ? "Selecting " : "DeSelecting ") + index); - if (select) - { - manager.Select(index); - } - else - { - manager.Deselect(index); - } - } - - private void Select(SelectionModel manager, int groupIndex, int itemIndex, bool select) - { - _output.WriteLine((select ? "Selecting " : "DeSelecting ") + groupIndex + "." + itemIndex); - if (select) - { - manager.Select(groupIndex, itemIndex); - } - else - { - manager.Deselect(groupIndex, itemIndex); - } - } - - private void Select(SelectionModel manager, IndexPath index, bool select) - { - _output.WriteLine((select ? "Selecting " : "DeSelecting ") + index); - if (select) - { - manager.SelectAt(index); - } - else - { - manager.DeselectAt(index); - } - } - - private void SelectRangeFromAnchor(SelectionModel manager, int index, bool select) - { - _output.WriteLine("SelectRangeFromAnchor " + index + " select: " + select.ToString()); - if (select) - { - manager.SelectRangeFromAnchor(index); - } - else - { - manager.DeselectRangeFromAnchor(index); - } - } - - private void SelectRangeFromAnchor(SelectionModel manager, int groupIndex, int itemIndex, bool select) - { - _output.WriteLine("SelectRangeFromAnchor " + groupIndex + "." + itemIndex + " select:" + select.ToString()); - if (select) - { - manager.SelectRangeFromAnchor(groupIndex, itemIndex); - } - else - { - manager.DeselectRangeFromAnchor(groupIndex, itemIndex); - } - } - - private void SelectRangeFromAnchor(SelectionModel manager, IndexPath index, bool select) - { - _output.WriteLine("SelectRangeFromAnchor " + index + " select: " + select.ToString()); - if (select) - { - manager.SelectRangeFromAnchorTo(index); - } - else - { - manager.DeselectRangeFromAnchorTo(index); - } - } - - private void ClearSelection(SelectionModel manager) - { - _output.WriteLine("ClearSelection"); - manager.ClearSelection(); - } - - private void SetAnchorIndex(SelectionModel manager, int index) - { - _output.WriteLine("SetAnchorIndex " + index); - manager.SetAnchorIndex(index); - } - - private void SetAnchorIndex(SelectionModel manager, int groupIndex, int itemIndex) - { - _output.WriteLine("SetAnchor " + groupIndex + "." + itemIndex); - manager.SetAnchorIndex(groupIndex, itemIndex); - } - - private void SetAnchorIndex(SelectionModel manager, IndexPath index) - { - _output.WriteLine("SetAnchor " + index); - manager.AnchorIndex = index; - } - - private void ValidateSelection( - SelectionModel selectionModel, - params IndexPath[] expectedSelected) - { - Assert.Equal(expectedSelected, selectionModel.SelectedIndices); - } - - private object GetData(SelectionModel selectionModel, IndexPath indexPath) - { - var data = selectionModel.Source; - for (int i = 0; i < indexPath.GetSize(); i++) - { - var listData = data as IList; - data = listData[indexPath.GetAt(i)]; - } - - return data; - } - - private bool AreEqual(IndexPath a, IndexPath b) - { - if (a.GetSize() != b.GetSize()) - { - return false; - } - - for (int i = 0; i < a.GetSize(); i++) - { - if (a.GetAt(i) != b.GetAt(i)) - { - return false; - } - } - - return true; - } - - private List GetIndexPathsInSource(object source) - { - List paths = new List(); - Traverse(source, (TreeWalkNodeInfo node) => - { - if (!paths.Contains(node.Path)) - { - paths.Add(node.Path); - } - }); - - _output.WriteLine("All Paths in source.."); - foreach (var path in paths) - { - _output.WriteLine(path.ToString()); - } - _output.WriteLine("done."); - - return paths; - } - - private static void Traverse(object root, Action nodeAction) - { - var pendingNodes = new Stack(); - IndexPath current = Path(null); - pendingNodes.Push(new TreeWalkNodeInfo() { Current = root, Path = current }); - - while (pendingNodes.Count > 0) - { - var currentNode = pendingNodes.Pop(); - var currentObject = currentNode.Current as IList; - - if (currentObject != null) - { - for (int i = currentObject.Count - 1; i >= 0; i--) - { - var child = currentObject[i]; - List path = new List(); - for (int idx = 0; idx < currentNode.Path.GetSize(); idx++) - { - path.Add(currentNode.Path.GetAt(idx)); - } - - path.Add(i); - var childPath = IndexPath.CreateFromIndices(path); - if (child != null) - { - pendingNodes.Push(new TreeWalkNodeInfo() { Current = child, Path = childPath }); - } - } - } - - nodeAction(currentNode); - } - } - - private bool Contains(List list, IndexPath index) - { - bool contains = false; - foreach (var item in list) - { - if (item.CompareTo(index) == 0) - { - contains = true; - break; - } - } - - return contains; - } - - public static AvaloniaList CreateNestedData(int levels = 3, int groupsAtLevel = 5, int countAtLeaf = 10) - { - var nextData = 0; - return CreateNestedData(levels, groupsAtLevel, countAtLeaf, ref nextData); - } - - public static AvaloniaList CreateNestedData( - int levels, - int groupsAtLevel, - int countAtLeaf, - ref int nextData) - { - var data = new AvaloniaList(); - if (levels != 0) - { - for (int i = 0; i < groupsAtLevel; i++) - { - data.Add(CreateNestedData(levels - 1, groupsAtLevel, countAtLeaf, ref nextData)); - } - } - else - { - for (int i = 0; i < countAtLeaf; i++) - { - data.Add(nextData++); - } - } - - return data; - } - - static IndexPath Path(params int[] path) - { - return IndexPath.CreateFromIndices(path); - } - - private static int _nextData = 0; - private struct TreeWalkNodeInfo - { - public object Current { get; set; } - - public IndexPath Path { get; set; } - } - - private class ResettingList : List, INotifyCollectionChanged - { - public event NotifyCollectionChangedEventHandler CollectionChanged; - - public new void RemoveAt(int index) - { - var item = this[index]; - base.RemoveAt(index); - CollectionChanged?.Invoke( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { item }, index)); - } - - public void Reset(IEnumerable items = null) - { - if (items != null) - { - Clear(); - AddRange(items); - } - - CollectionChanged?.Invoke( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } - - private class Node : INotifyPropertyChanged - { - private ObservableCollection _children; - private PropertyChangedEventHandler _propertyChanged; - - public Node(string header) - { - Header = header; - } - - public string Header { get; } - - public ObservableCollection Children - { - get => _children ??= CreateChildren(10); - private set - { - _children = value; - _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Children))); - } - } - - public event PropertyChangedEventHandler PropertyChanged - { - add - { - _propertyChanged += value; - ++PropertyChangedSubscriptions; - } - - remove - { - _propertyChanged -= value; - --PropertyChangedSubscriptions; - } - } - - public int PropertyChangedSubscriptions { get; private set; } - - public void ReplaceChildren() - { - Children = CreateChildren(5); - } - - private ObservableCollection CreateChildren(int count) - { - return new ObservableCollection( - Enumerable.Range(0, count).Select(x => new Node("Child " + x))); - } - } - } - - class CustomSelectionModel : SelectionModel - { - public int IntProperty - { - get { return _intProperty; } - set - { - _intProperty = value; - OnPropertyChanged("IntProperty"); - } - } - - private int _intProperty; - } -} diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index fd52aeb9af..5bca187f38 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Removal_Should_Set_First_Tab() + public void Removal_Should_Set_Next_Tab() { var collection = new ObservableCollection() { @@ -126,7 +126,8 @@ namespace Avalonia.Controls.UnitTests target.SelectedItem = collection[1]; collection.RemoveAt(1); - Assert.Same(collection[0], target.SelectedItem); + // compare with former [2] now [1] == "3rd" + Assert.Same(collection[1], target.SelectedItem); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index c25ad19027..7022fbf4c1 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -255,12 +255,12 @@ namespace Avalonia.Controls.UnitTests ClickContainer(item2Container, KeyModifiers.Control); Assert.True(item2Container.IsSelected); - Assert.Equal(new[] { item1, item2 }, target.Selection.SelectedItems.OfType()); + Assert.Equal(new[] { item1, item2 }, target.SelectedItems.OfType()); ClickContainer(item1Container, KeyModifiers.Control); Assert.False(item1Container.IsSelected); - Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType()); + Assert.DoesNotContain(item1, target.SelectedItems.OfType()); } } @@ -785,11 +785,11 @@ namespace Avalonia.Controls.UnitTests target.SelectAll(); AssertChildrenSelected(target, tree[0]); - Assert.Equal(5, target.Selection.SelectedItems.Count); + Assert.Equal(5, target.SelectedItems.Count); _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); - Assert.Equal(5, target.Selection.SelectedItems.Count); + Assert.Equal(5, target.SelectedItems.Count); } [Fact] @@ -823,11 +823,11 @@ namespace Avalonia.Controls.UnitTests ClickContainer(fromContainer, KeyModifiers.None); ClickContainer(toContainer, KeyModifiers.Shift); - Assert.Equal(2, target.Selection.SelectedItems.Count); + Assert.Equal(2, target.SelectedItems.Count); _mouse.Click(thenContainer, MouseButton.Right); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.SelectedItems.Count); } } @@ -860,7 +860,7 @@ namespace Avalonia.Controls.UnitTests _mouse.Click(fromContainer); _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.SelectedItems.Count); } } @@ -893,7 +893,7 @@ namespace Avalonia.Controls.UnitTests _mouse.Click(fromContainer); _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.SelectedItems.Count); } } diff --git a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs deleted file mode 100644 index 4aa7e24aa7..0000000000 --- a/tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Avalonia.Collections; -using Avalonia.Controls.Utils; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Utils -{ - public class SelectedItemsSyncTests - { - [Fact] - public void Initial_Items_Are_From_Model() - { - var target = CreateTarget(); - var items = target.GetOrCreateItems(); - - Assert.Equal(new[] { "bar", "baz" }, items); - } - - [Fact] - public void Selecting_On_Model_Adds_Item() - { - var target = CreateTarget(); - var items = target.GetOrCreateItems(); - - target.Model.Select(0); - - Assert.Equal(new[] { "bar", "baz", "foo" }, items); - } - - [Fact] - public void Selecting_Duplicate_On_Model_Adds_Item() - { - var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); - var items = target.GetOrCreateItems(); - - target.Model.Select(4); - - Assert.Equal(new[] { "bar", "baz", "bar" }, items); - } - - [Fact] - public void Deselecting_On_Model_Removes_Item() - { - var target = CreateTarget(); - var items = target.GetOrCreateItems(); - - target.Model.Deselect(1); - - Assert.Equal(new[] { "baz" }, items); - } - - [Fact] - public void Deselecting_Duplicate_On_Model_Removes_Item() - { - var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); - var items = target.GetOrCreateItems(); - - target.Model.Select(4); - target.Model.Deselect(4); - - Assert.Equal(new[] { "baz", "bar" }, items); - } - - [Fact] - public void Reassigning_Model_Resets_Items() - { - var target = CreateTarget(); - var items = target.GetOrCreateItems(); - - var newModel = new SelectionModel { Source = target.Model.Source }; - newModel.Select(0); - newModel.Select(1); - - target.SetModel(newModel); - - Assert.Equal(new[] { "foo", "bar" }, items); - } - - [Fact] - public void Reassigning_Model_Tracks_New_Model() - { - var target = CreateTarget(); - var items = target.GetOrCreateItems(); - - var newModel = new SelectionModel { Source = target.Model.Source }; - target.SetModel(newModel); - - newModel.Select(0); - newModel.Select(1); - - Assert.Equal(new[] { "foo", "bar" }, items); - } - - [Fact] - public void Adding_To_Items_Selects_On_Model() - { - var target = CreateTarget(); - var items = target.GetOrCreateItems(); - - items.Add("foo"); - - Assert.Equal( - new[] { new IndexPath(0), new IndexPath(1), new IndexPath(2) }, - target.Model.SelectedIndices); - Assert.Equal(new[] { "bar", "baz", "foo" }, items); - } - - [Fact] - public void Removing_From_Items_Deselects_On_Model() - { - var target = CreateTarget(); - var items = target.GetOrCreateItems(); - - items.Remove("baz"); - - Assert.Equal(new[] { new IndexPath(1) }, target.Model.SelectedIndices); - Assert.Equal(new[] { "bar" }, items); - } - - [Fact] - public void Replacing_Item_Updates_Model() - { - var target = CreateTarget(); - var items = target.GetOrCreateItems(); - - items[0] = "foo"; - - Assert.Equal(new[] { new IndexPath(0), new IndexPath(2) }, target.Model.SelectedIndices); - Assert.Equal(new[] { "foo", "baz" }, items); - } - - [Fact] - public void Clearing_Items_Updates_Model() - { - var target = CreateTarget(); - var items = target.GetOrCreateItems(); - - items.Clear(); - - Assert.Empty(target.Model.SelectedIndices); - } - - [Fact] - public void Setting_Items_Updates_Model() - { - var target = CreateTarget(); - var oldItems = target.GetOrCreateItems(); - - var newItems = new AvaloniaList { "foo", "baz" }; - target.SetItems(newItems); - - Assert.Equal(new[] { new IndexPath(0), new IndexPath(2) }, target.Model.SelectedIndices); - Assert.Same(newItems, target.GetOrCreateItems()); - Assert.NotSame(oldItems, target.GetOrCreateItems()); - Assert.Equal(new[] { "foo", "baz" }, newItems); - } - - [Fact] - public void Setting_Items_Subscribes_To_Model() - { - var target = CreateTarget(); - var items = new AvaloniaList { "foo", "baz" }; - - target.SetItems(items); - target.Model.Select(1); - - Assert.Equal(new[] { "foo", "baz", "bar" }, items); - } - - [Fact] - public void Setting_Items_To_Null_Creates_Empty_Items() - { - var target = CreateTarget(); - var oldItems = target.GetOrCreateItems(); - - target.SetItems(null); - - var newItems = Assert.IsType>(target.GetOrCreateItems()); - - Assert.NotSame(oldItems, newItems); - } - - [Fact] - public void Handles_Null_Model_Source() - { - var model = new SelectionModel(); - model.Select(1); - - var target = new SelectedItemsSync(model); - var items = target.GetOrCreateItems(); - - Assert.Empty(items); - - model.Select(2); - model.Source = new[] { "foo", "bar", "baz" }; - - Assert.Equal(new[] { "bar", "baz" }, items); - } - - [Fact] - public void Does_Not_Accept_Fixed_Size_Items() - { - var target = CreateTarget(); - - Assert.Throws(() => - target.SetItems(new[] { "foo", "bar", "baz" })); - } - - [Fact] - public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source() - { - var model = new SelectionModel(); - var target = new SelectedItemsSync(model); - var items = new AvaloniaList { "foo", "bar", "baz" }; - var selectedItems = new AvaloniaList { "bar" }; - - target.SetItems(selectedItems); - model.Source = items; - - Assert.Equal(new IndexPath(1), model.SelectedIndex); - } - - private static SelectedItemsSync CreateTarget( - IEnumerable items = null) - { - items ??= new[] { "foo", "bar", "baz" }; - - var model = new SelectionModel { Source = items }; - model.SelectRange(new IndexPath(1), new IndexPath(2)); - - var target = new SelectedItemsSync(model); - return target; - } - } -}