Browse Source

Merge branch 'master' into datagrid-details-height

pull/4530/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
c084e6cd6a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      build/SkiaSharp.props
  2. 5
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  3. 2
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  4. 15
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  5. 34
      samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
  6. 11
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  7. 18
      src/Avalonia.Controls/ApiCompatBaseline.txt
  8. 3
      src/Avalonia.Controls/Avalonia.Controls.csproj
  9. 5
      src/Avalonia.Controls/ContextMenu.cs
  10. 249
      src/Avalonia.Controls/ISelectionModel.cs
  11. 200
      src/Avalonia.Controls/IndexPath.cs
  12. 30
      src/Avalonia.Controls/ItemsControl.cs
  13. 120
      src/Avalonia.Controls/ItemsSourceView.cs
  14. 7
      src/Avalonia.Controls/ListBox.cs
  15. 553
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  16. 49
      src/Avalonia.Controls/SelectedItems.cs
  17. 66
      src/Avalonia.Controls/Selection/ISelectionModel.cs
  18. 82
      src/Avalonia.Controls/Selection/IndexRange.cs
  19. 82
      src/Avalonia.Controls/Selection/SelectedIndexes.cs
  20. 121
      src/Avalonia.Controls/Selection/SelectedItems.cs
  21. 726
      src/Avalonia.Controls/Selection/SelectionModel.cs
  22. 18
      src/Avalonia.Controls/Selection/SelectionModelIndexesChangedEventArgs.cs
  23. 85
      src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs
  24. 286
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  25. 894
      src/Avalonia.Controls/SelectionModel.cs
  26. 170
      src/Avalonia.Controls/SelectionModelChangeSet.cs
  27. 103
      src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
  28. 47
      src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs
  29. 971
      src/Avalonia.Controls/SelectionNode.cs
  30. 110
      src/Avalonia.Controls/SelectionNodeOperation.cs
  31. 669
      src/Avalonia.Controls/TreeView.cs
  32. 140
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs
  33. 211
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  34. 189
      src/Avalonia.Controls/Utils/SelectionTreeHelper.cs
  35. 21
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  36. 15
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  37. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  38. 101
      src/Avalonia.Themes.Default/TextBox.xaml
  39. 54
      src/Avalonia.Themes.Fluent/TextBox.xaml
  40. 6
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs
  41. 2
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  42. 2
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  43. 39
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  44. 95
      tests/Avalonia.Controls.UnitTests/IndexPathTests.cs
  45. 389
      tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs
  46. 2
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  47. 187
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  48. 73
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  49. 1584
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs
  50. 1210
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs
  51. 2322
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs
  52. 16
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  53. 131
      tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.0" />
<PackageReference Include="SkiaSharp" Version="2.80.2-preview.33" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2-preview.33" />
</ItemGroup>
</Project>

5
samples/BindingDemo/ViewModels/MainWindowViewModel.cs

@ -8,6 +8,7 @@ using System.Threading;
using ReactiveUI;
using Avalonia.Controls;
using Avalonia.Metadata;
using Avalonia.Controls.Selection;
namespace BindingDemo.ViewModels
{
@ -29,7 +30,7 @@ namespace BindingDemo.ViewModels
Detail = "Item " + x + " details",
}));
Selection = new SelectionModel();
Selection = new SelectionModel<TestItem> { SingleSelect = false };
ShuffleItems = ReactiveCommand.Create(() =>
{
@ -58,7 +59,7 @@ namespace BindingDemo.ViewModels
}
public ObservableCollection<TestItem> Items { get; }
public SelectionModel Selection { get; }
public SelectionModel<TestItem> Selection { get; }
public ReactiveCommand<Unit, Unit> ShuffleItems { get; }
public string BooleanString

2
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -10,7 +10,7 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<TreeView Items="{Binding Items}" Selection="{Binding Selection}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}"/>

15
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Controls.Selection;
using ReactiveUI;
namespace ControlCatalog.ViewModels
@ -15,16 +16,16 @@ namespace ControlCatalog.ViewModels
public ListBoxPageViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
Selection = new SelectionModel();
Selection = new SelectionModel<string>();
Selection.Select(1);
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (Selection.SelectedItems.Count > 0)
while (Selection.Count > 0)
{
Items.Remove((string)Selection.SelectedItems.First());
Items.Remove(Selection.SelectedItems.First());
}
});
@ -32,9 +33,9 @@ namespace ControlCatalog.ViewModels
{
var random = new Random();
using (Selection.Update())
using (Selection.BatchUpdate())
{
Selection.ClearSelection();
Selection.Clear();
Selection.Select(random.Next(Items.Count - 1));
}
});
@ -42,7 +43,7 @@ namespace ControlCatalog.ViewModels
public ObservableCollection<string> Items { get; }
public SelectionModel Selection { get; }
public SelectionModel<string> Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
@ -55,7 +56,7 @@ namespace ControlCatalog.ViewModels
get => _selectionMode;
set
{
Selection.ClearSelection();
Selection.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}

34
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<Node>();
AddItemCommand = ReactiveCommand.Create(AddItem);
RemoveItemCommand = ReactiveCommand.Create(RemoveItem);
@ -27,7 +25,7 @@ namespace ControlCatalog.ViewModels
}
public ObservableCollection<Node> Items { get; }
public SelectionModel Selection { get; }
public ObservableCollection<Node> SelectedItems { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> 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<Node> 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

11
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -7,6 +7,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ReactiveUI;
using Avalonia.Layout;
using Avalonia.Controls.Selection;
namespace VirtualizationDemo.ViewModels
{
@ -48,7 +49,7 @@ namespace VirtualizationDemo.ViewModels
set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
}
public SelectionModel Selection { get; } = new SelectionModel();
public SelectionModel<ItemViewModel> Selection { get; } = new SelectionModel<ItemViewModel>();
public AvaloniaList<ItemViewModel> Items
{
@ -137,9 +138,9 @@ namespace VirtualizationDemo.ViewModels
{
var index = Items.Count;
if (Selection.SelectedIndices.Count > 0)
if (Selection.SelectedItems.Count > 0)
{
index = Selection.SelectedIndex.GetAt(0);
index = Selection.SelectedIndex;
}
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
@ -149,7 +150,7 @@ namespace VirtualizationDemo.ViewModels
{
if (Selection.SelectedItems.Count > 0)
{
Items.RemoveAll(Selection.SelectedItems.Cast<ItemViewModel>().ToList());
Items.RemoveAll(Selection.SelectedItems.ToList());
}
}
@ -163,7 +164,7 @@ namespace VirtualizationDemo.ViewModels
private void SelectItem(int index)
{
Selection.SelectedIndex = new IndexPath(index);
Selection.SelectedIndex = index;
}
}
}

18
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -0,0 +1,18 @@
Compat issues with assembly Avalonia.Controls:
TypesMustExist : Type 'Avalonia.Controls.IndexPath' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.ISelectedItemInfo' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.ISelectionModel' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.ListBox.SelectionProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.ListBox.Selection.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.ListBox.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.SelectionModel' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.SelectionModelChildrenRequestedEventArgs' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.SelectionModelSelectionChangedEventArgs' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.TreeView, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.TreeView, Avalonia.Controls.ISelectionModel> Avalonia.Controls.TreeView.SelectionProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
Total Issues: 16

3
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -2,6 +2,9 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />

5
src/Avalonia.Controls/ContextMenu.cs

@ -279,6 +279,11 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
}
if (PlacementTarget is null && _popup.PlacementTarget != control)
{
_popup.PlacementTarget = control;
}
_popup.Child = this;
IsOpen = true;
_popup.IsOpen = true;

249
src/Avalonia.Controls/ISelectionModel.cs

@ -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
{
/// <summary>
/// Holds the selected items for a control.
/// </summary>
public interface ISelectionModel : INotifyPropertyChanged
{
/// <summary>
/// Gets or sets the anchor index.
/// </summary>
IndexPath AnchorIndex { get; set; }
/// <summary>
/// Gets or set the index of the first selected item.
/// </summary>
IndexPath SelectedIndex { get; set; }
/// <summary>
/// Gets or set the indexes of the selected items.
/// </summary>
IReadOnlyList<IndexPath> SelectedIndices { get; }
/// <summary>
/// Gets the first selected item.
/// </summary>
object SelectedItem { get; }
/// <summary>
/// Gets the selected items.
/// </summary>
IReadOnlyList<object> SelectedItems { get; }
/// <summary>
/// Gets a value indicating whether the model represents a single or multiple selection.
/// </summary>
bool SingleSelect { get; set; }
/// <summary>
/// Gets a value indicating whether to always keep an item selected where possible.
/// </summary>
bool AutoSelect { get; set; }
/// <summary>
/// Gets or sets the collection that contains the items that can be selected.
/// </summary>
object Source { get; set; }
/// <summary>
/// Raised when the children of a selection are required.
/// </summary>
event EventHandler<SelectionModelChildrenRequestedEventArgs> ChildrenRequested;
/// <summary>
/// Raised when the selection has changed.
/// </summary>
event EventHandler<SelectionModelSelectionChangedEventArgs> SelectionChanged;
/// <summary>
/// Clears the selection.
/// </summary>
void ClearSelection();
/// <summary>
/// Deselects an item.
/// </summary>
/// <param name="index">The index of the item.</param>
void Deselect(int index);
/// <summary>
/// Deselects an item.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
void Deselect(int groupIndex, int itemIndex);
/// <summary>
/// Deselects an item.
/// </summary>
/// <param name="index">The index of the item.</param>
void DeselectAt(IndexPath index);
/// <summary>
/// Deselects a range of items.
/// </summary>
/// <param name="start">The start index of the range.</param>
/// <param name="end">The end index of the range.</param>
void DeselectRange(IndexPath start, IndexPath end);
/// <summary>
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void DeselectRangeFromAnchor(int index);
/// <summary>
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="endGroupIndex">
/// The index of the item group that represents the end of the selection.
/// </param>
/// <param name="endItemIndex">
/// The index of the item in the group that represents the end of the selection.
/// </param>
void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex);
/// <summary>
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void DeselectRangeFromAnchorTo(IndexPath index);
/// <summary>
/// Disposes the object and clears the selection.
/// </summary>
void Dispose();
/// <summary>
/// Checks whether an item is selected.
/// </summary>
/// <param name="index">The index of the item</param>
bool IsSelected(int index);
/// <summary>
/// Checks whether an item is selected.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
bool IsSelected(int groupIndex, int itemIndex);
/// <summary>
/// Checks whether an item is selected.
/// </summary>
/// <param name="index">The index of the item</param>
public bool IsSelectedAt(IndexPath index);
/// <summary>
/// Checks whether an item or its descendents are selected.
/// </summary>
/// <param name="index">The index of the item</param>
/// <returns>
/// 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.
/// </returns>
bool? IsSelectedWithPartial(int index);
/// <summary>
/// Checks whether an item or its descendents are selected.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
/// <returns>
/// 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.
/// </returns>
bool? IsSelectedWithPartial(int groupIndex, int itemIndex);
/// <summary>
/// Checks whether an item or its descendents are selected.
/// </summary>
/// <param name="index">The index of the item</param>
/// <returns>
/// 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.
/// </returns>
bool? IsSelectedWithPartialAt(IndexPath index);
/// <summary>
/// Selects an item.
/// </summary>
/// <param name="index">The index of the item</param>
void Select(int index);
/// <summary>
/// Selects an item.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
void Select(int groupIndex, int itemIndex);
/// <summary>
/// Selects an item.
/// </summary>
/// <param name="index">The index of the item</param>
void SelectAt(IndexPath index);
/// <summary>
/// Selects all items.
/// </summary>
void SelectAll();
/// <summary>
/// Selects a range of items.
/// </summary>
/// <param name="start">The start index of the range.</param>
/// <param name="end">The end index of the range.</param>
void SelectRange(IndexPath start, IndexPath end);
/// <summary>
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void SelectRangeFromAnchor(int index);
/// <summary>
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="endGroupIndex">
/// The index of the item group that represents the end of the selection.
/// </param>
/// <param name="endItemIndex">
/// The index of the item in the group that represents the end of the selection.
/// </param>
void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex);
/// <summary>
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void SelectRangeFromAnchorTo(IndexPath index);
/// <summary>
/// Sets the <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The anchor index.</param>
void SetAnchorIndex(int index);
/// <summary>
/// Sets the <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="index">The index of the item in the group.</param>
void SetAnchorIndex(int groupIndex, int index);
/// <summary>
/// Begins a batch update of the selection.
/// </summary>
/// <returns>An <see cref="IDisposable"/> that finishes the batch update.</returns>
IDisposable Update();
}
}

200
src/Avalonia.Controls/IndexPath.cs

@ -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<IndexPath>, IEquatable<IndexPath>
{
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<int>? 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<int> 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; }
}
}

30
src/Avalonia.Controls/ItemsControl.cs

@ -18,7 +18,7 @@ namespace Avalonia.Controls
/// <summary>
/// Displays a collection of items.
/// </summary>
public class ItemsControl : TemplatedControl, IItemsPresenterHost
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
@ -53,7 +53,6 @@ namespace Avalonia.Controls
private IEnumerable _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
private IDisposable _itemsCollectionChangedSubscription;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
@ -150,6 +149,19 @@ namespace Avalonia.Controls
ItemContainerGenerator.Clear();
}
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
}
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
ItemsCollectionChanged(sender, e);
}
/// <summary>
/// Gets the item at the specified index in a collection.
/// </summary>
@ -315,12 +327,14 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
_itemsCollectionChangedSubscription?.Dispose();
_itemsCollectionChangedSubscription = null;
var oldValue = e.OldValue as IEnumerable;
var newValue = e.NewValue as IEnumerable;
if (oldValue is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
}
UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
@ -418,11 +432,9 @@ namespace Avalonia.Controls
PseudoClasses.Set(":empty", items == null || items.Count() == 0);
PseudoClasses.Set(":singleitem", items != null && items.Count() == 1);
var incc = items as INotifyCollectionChanged;
if (incc != null)
if (items is INotifyCollectionChanged incc)
{
_itemsCollectionChangedSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
CollectionChangedEventManager.Instance.AddListener(incc, this);
}
}

120
src/Avalonia.Controls/Repeater/ItemsSourceView.cs → src/Avalonia.Controls/ItemsSourceView.cs

@ -7,7 +7,11 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Controls.Utils;
#nullable enable
namespace Avalonia.Controls
{
@ -23,8 +27,13 @@ namespace Avalonia.Controls
/// </remarks>
public class ItemsSourceView : INotifyCollectionChanged, IDisposable
{
private readonly IList _inner;
private INotifyCollectionChanged _notifyCollectionChanged;
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
private protected readonly IList _inner;
private INotifyCollectionChanged? _notifyCollectionChanged;
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
@ -32,7 +41,7 @@ namespace Avalonia.Controls
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable source)
{
Contract.Requires<ArgumentNullException>(source != null);
source = source ?? throw new ArgumentNullException(nameof(source));
if (source is IList list)
{
@ -63,10 +72,17 @@ namespace Avalonia.Controls
/// </remarks>
public bool HasKeyIndexMapping => false;
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
public object? this[int index] => GetAt(index);
/// <summary>
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
/// </summary>
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event NotifyCollectionChangedEventHandler? CollectionChanged;
/// <inheritdoc/>
public void Dispose()
@ -81,10 +97,26 @@ namespace Avalonia.Controls
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>the item.</returns>
public object GetAt(int index) => _inner[index];
/// <returns>The item.</returns>
public object? GetAt(int index) => _inner[index];
public int IndexOf(object item) => _inner.IndexOf(item);
public int IndexOf(object? item) => _inner.IndexOf(item);
public static ItemsSourceView GetOrCreate(IEnumerable? items)
{
if (items is ItemsSourceView isv)
{
return isv;
}
else if (items is null)
{
return Empty;
}
else
{
return new ItemsSourceView(items);
}
}
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
@ -112,6 +144,22 @@ namespace Avalonia.Controls
throw new NotImplementedException();
}
internal void AddListener(ICollectionChangedListener listener)
{
if (_inner is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.AddListener(incc, listener);
}
}
internal void RemoveListener(ICollectionChangedListener listener)
{
if (_inner is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.RemoveListener(incc, listener);
}
}
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
@ -131,4 +179,62 @@ namespace Avalonia.Controls
OnItemsSourceChanged(e);
}
}
public class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
{
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public new static ItemsSourceView<T> Empty { get; } = new ItemsSourceView<T>(Array.Empty<T>());
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable<T> source)
: base(source)
{
}
private ItemsSourceView(IEnumerable source)
: base(source)
{
}
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
#pragma warning disable CS8603
public new T this[int index] => GetAt(index);
#pragma warning restore CS8603
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
[return: MaybeNull]
public new T GetAt(int index) => (T)_inner[index];
public IEnumerator<T> GetEnumerator() => _inner.Cast<T>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items)
{
if (items is ItemsSourceView<T> isv)
{
return isv;
}
else if (items is null)
{
return Empty;
}
else
{
return new ItemsSourceView<T>(items);
}
}
}
}

7
src/Avalonia.Controls/ListBox.cs

@ -2,6 +2,7 @@ using System.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.VisualTree;
@ -76,9 +77,7 @@ namespace Avalonia.Controls
set => base.SelectedItems = value;
}
/// <summary>
/// Gets or sets a model holding the current selection.
/// </summary>
/// <inheritdoc/>
public new ISelectionModel Selection
{
get => base.Selection;
@ -115,7 +114,7 @@ namespace Avalonia.Controls
/// <summary>
/// Deselects all items in the <see cref="ListBox"/>.
/// </summary>
public void UnselectAll() => Selection.ClearSelection();
public void UnselectAll() => Selection.Clear();
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()

553
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -3,9 +3,9 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
@ -13,6 +13,8 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
@ -24,8 +26,8 @@ namespace Avalonia.Controls.Primitives
/// that maintain a selection (single or multiple). By default only its
/// <see cref="SelectedIndex"/> and <see cref="SelectedItem"/> properties are visible; the
/// current multiple <see cref="Selection"/> and <see cref="SelectedItems"/> together with the
/// <see cref="SelectionMode"/> and properties are protected, however a derived class can
/// expose these if it wishes to support multiple selection.
/// <see cref="SelectionMode"/> properties are protected, however a derived class can expose
/// these if it wishes to support multiple selection.
/// </para>
/// <para>
/// <see cref="SelectingItemsControl"/> maintains a selection respecting the current
@ -58,8 +60,8 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="SelectedItem"/> property.
/// </summary>
public static readonly DirectProperty<SelectingItemsControl, object> SelectedItemProperty =
AvaloniaProperty.RegisterDirect<SelectingItemsControl, object>(
public static readonly DirectProperty<SelectingItemsControl, object?> SelectedItemProperty =
AvaloniaProperty.RegisterDirect<SelectingItemsControl, object?>(
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v,
@ -77,7 +79,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="Selection"/> property.
/// </summary>
public static readonly DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
protected static readonly DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
AvaloniaProperty.RegisterDirect<SelectingItemsControl, ISelectionModel>(
nameof(Selection),
o => o.Selection,
@ -109,21 +111,12 @@ namespace Avalonia.Controls.Primitives
RoutingStrategies.Bubble);
private static readonly IList Empty = Array.Empty<object>();
private readonly SelectedItemsSync _selectedItems;
private ISelectionModel _selection;
private int _selectedIndex = -1;
private object _selectedItem;
private SelectedItemsSync? _selectedItemsSync;
private ISelectionModel? _selection;
private int _oldSelectedIndex;
private object? _oldSelectedItem;
private int _initializing;
private bool _ignoreContainerSelectionChanged;
private int _updateCount;
private int _updateSelectedIndex;
private object _updateSelectedItem;
public SelectingItemsControl()
{
// Setting Selection to null causes a default SelectionModel to be created.
Selection = null;
_selectedItems = new SelectedItemsSync(Selection);
}
/// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@ -156,42 +149,17 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public int SelectedIndex
{
get => Selection.SelectedIndex != default ? Selection.SelectedIndex.GetAt(0) : -1;
set
{
if (_updateCount == 0)
{
if (value != SelectedIndex)
{
Selection.SelectedIndex = new IndexPath(value);
}
}
else
{
_updateSelectedIndex = value;
_updateSelectedItem = null;
}
}
get => Selection.SelectedIndex;
set => Selection.SelectedIndex = value;
}
/// <summary>
/// Gets or sets the selected item.
/// </summary>
public object SelectedItem
public object? SelectedItem
{
get => Selection.SelectedItem;
set
{
if (_updateCount == 0)
{
SelectedIndex = IndexOf(Items, value);
}
else
{
_updateSelectedItem = value;
_updateSelectedIndex = int.MinValue;
}
}
set => Selection.SelectedItem = value;
}
/// <summary>
@ -199,46 +167,40 @@ namespace Avalonia.Controls.Primitives
/// </summary>
protected IList SelectedItems
{
get => _selectedItems.GetOrCreateItems();
set => _selectedItems.SetItems(value);
get => SelectedItemsSync.SelectedItems;
set => SelectedItemsSync.SelectedItems = value;
}
/// <summary>
/// Gets or sets a model holding the current selection.
/// Gets or sets the model that holds the current selection.
/// </summary>
protected ISelectionModel Selection
protected ISelectionModel Selection
{
get => _selection;
set
get
{
value ??= new SelectionModel
if (_selection is null)
{
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
RetainSelectionOnReset = true,
};
_selection = CreateDefaultSelectionModel();
InitializeSelectionModel(_selection);
}
return _selection;
}
set
{
value ??= CreateDefaultSelectionModel();
if (_selection != value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value), "Cannot set Selection to null.");
}
else if (value.Source != null && value.Source != Items)
{
throw new ArgumentException("Selection has invalid Source.");
}
List<object> oldSelection = null;
if (_selection != null)
if (value.Source != null && value.Source != Items)
{
oldSelection = Selection.SelectedItems.ToList();
_selection.PropertyChanged -= OnSelectionModelPropertyChanged;
_selection.SelectionChanged -= OnSelectionModelSelectionChanged;
MarkContainersUnselected();
throw new ArgumentException(
"The supplied ISelectionModel already has an assigned Source but this " +
"collection is different to the Items on the control.");
}
var oldSelection = _selection?.SelectedItems.ToList();
DeinitializeSelectionModel(_selection);
_selection = value;
if (oldSelection?.Count > 0)
@ -249,55 +211,7 @@ namespace Avalonia.Controls.Primitives
Array.Empty<object>()));
}
if (_selection != null)
{
_selection.Source = Items;
_selection.PropertyChanged += OnSelectionModelPropertyChanged;
_selection.SelectionChanged += OnSelectionModelSelectionChanged;
if (_selection.SingleSelect)
{
SelectionMode &= ~SelectionMode.Multiple;
}
else
{
SelectionMode |= SelectionMode.Multiple;
}
if (_selection.AutoSelect)
{
SelectionMode |= SelectionMode.AlwaysSelected;
}
else
{
SelectionMode &= ~SelectionMode.AlwaysSelected;
}
UpdateContainerSelection();
var selectedIndex = SelectedIndex;
var selectedItem = SelectedItem;
if (_selectedIndex != selectedIndex)
{
RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, selectedIndex);
_selectedIndex = selectedIndex;
}
if (_selectedItem != selectedItem)
{
RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem);
_selectedItem = selectedItem;
}
if (selectedIndex != -1)
{
RaiseEvent(new SelectionChangedEventArgs(
SelectionChangedEvent,
Array.Empty<object>(),
Selection.SelectedItems.ToList()));
}
}
InitializeSelectionModel(_selection);
}
}
}
@ -320,20 +234,20 @@ namespace Avalonia.Controls.Primitives
/// </summary>
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection);
/// <inheritdoc/>
public override void BeginInit()
{
base.BeginInit();
InternalBeginInit();
++_initializing;
}
/// <inheritdoc/>
public override void EndInit()
{
InternalEndInit();
base.EndInit();
--_initializing;
}
/// <summary>
@ -353,7 +267,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <returns>The container or null if the event did not originate in a container.</returns>
protected IControl GetContainerFromEventSource(IInteractive eventSource)
protected IControl? GetContainerFromEventSource(IInteractive eventSource)
{
var parent = (IVisual)eventSource;
@ -371,21 +285,14 @@ namespace Avalonia.Controls.Primitives
return null;
}
/// <inheritdoc/>
protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
if (_updateCount == 0)
{
Selection.Source = e.NewValue;
}
base.ItemsChanged(e);
}
/// <inheritdoc/>
protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
base.ItemsCollectionChanged(sender, e);
if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
{
SelectedIndex = 0;
}
}
/// <inheritdoc/>
@ -400,9 +307,10 @@ namespace Avalonia.Controls.Primitives
Selection.Select(container.Index);
MarkContainerSelected(container.ContainerControl, true);
}
else if (Selection.IsSelected(container.Index) == true)
else
{
MarkContainerSelected(container.ContainerControl, true);
var selected = Selection.IsSelected(container.Index);
MarkContainerSelected(container.ContainerControl, selected);
}
}
}
@ -433,7 +341,7 @@ namespace Avalonia.Controls.Primitives
{
if (i.ContainerControl != null && i.Item != null)
{
bool selected = Selection.IsSelected(i.Index) == true;
bool selected = Selection.IsSelected(i.Index);
MarkContainerSelected(i.ContainerControl, selected);
}
}
@ -443,27 +351,39 @@ namespace Avalonia.Controls.Primitives
protected override void OnDataContextBeginUpdate()
{
base.OnDataContextBeginUpdate();
++_initializing;
InternalBeginInit();
if (_selection is object)
{
_selection.Source = null;
}
}
/// <inheritdoc/>
protected override void OnDataContextEndUpdate()
{
base.OnDataContextEndUpdate();
--_initializing;
if (_selection is object && _initializing == 0)
{
_selection.Source = Items;
InternalEndInit();
if (Items is null)
{
_selection.Clear();
_selectedItemsSync?.SelectedItems?.Clear();
}
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
protected override void OnInitialized()
{
base.OnPropertyChanged(change);
base.OnInitialized();
if (change.Property == SelectionModeProperty)
if (_selection is object)
{
var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
_selection.Source = Items;
}
}
@ -487,6 +407,29 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == ItemsProperty &&
_initializing == 0 &&
_selection is object)
{
var newValue = change.NewValue.GetValueOrDefault<IEnumerable>();
_selection.Source = newValue;
if (newValue is null)
{
_selection.Clear();
}
}
else if (change.Property == SelectionModeProperty && _selection is object)
{
var newValue = change.NewValue.GetValueOrDefault<SelectionMode>();
_selection.SingleSelect = !newValue.HasFlagCustom(SelectionMode.Multiple);
}
}
/// <summary>
/// Moves the selection in the specified direction relative to the current selection.
/// </summary>
@ -506,7 +449,7 @@ namespace Avalonia.Controls.Primitives
/// <param name="direction">The direction to move.</param>
/// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
/// <returns>True if the selection was moved; otherwise false.</returns>
protected bool MoveSelection(IControl from, NavigationDirection direction, bool wrap)
protected bool MoveSelection(IControl? from, NavigationDirection direction, bool wrap)
{
if (Presenter?.Panel is INavigableContainer container &&
GetNextControl(container, direction, from, wrap) is IControl next)
@ -538,71 +481,62 @@ namespace Avalonia.Controls.Primitives
bool toggleModifier = false,
bool rightButton = false)
{
if (index != -1)
if (index < 0 || index >= ItemCount)
{
if (select)
{
var mode = SelectionMode;
var multi = (mode & SelectionMode.Multiple) != 0;
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
if (rightButton)
{
if (Selection.IsSelected(index) == false)
{
SelectedIndex = index;
}
}
else if (range)
{
using var operation = Selection.Update();
var anchor = Selection.AnchorIndex;
if (anchor.GetSize() == 0)
{
anchor = new IndexPath(0);
}
return;
}
Selection.ClearSelection();
Selection.AnchorIndex = anchor;
Selection.SelectRangeFromAnchor(index);
}
else if (multi && toggle)
{
if (Selection.IsSelected(index) == true)
{
Selection.Deselect(index);
}
else
{
Selection.Select(index);
}
}
else if (toggle)
{
SelectedIndex = (SelectedIndex == index) ? -1 : index;
}
else
{
using var operation = Selection.Update();
Selection.ClearSelection();
Selection.Select(index);
}
var mode = SelectionMode;
var multi = (mode & SelectionMode.Multiple) != 0;
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
if (Presenter?.Panel != null)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
container);
}
if (!select)
{
Selection.Deselect(index);
}
else if (rightButton)
{
if (Selection.IsSelected(index) == false)
{
SelectedIndex = index;
}
}
else if (range)
{
using var operation = Selection.BatchUpdate();
Selection.Clear();
Selection.SelectRange(Selection.AnchorIndex, index);
}
else if (multi && toggle)
{
if (Selection.IsSelected(index) == true)
{
Selection.Deselect(index);
}
else
{
LostSelection();
Selection.Select(index);
}
}
else if (toggle)
{
SelectedIndex = (SelectedIndex == index) ? -1 : index;
}
else
{
using var operation = Selection.BatchUpdate();
Selection.Clear();
Selection.Select(index);
}
if (Presenter?.Panel != null)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
container);
}
}
/// <summary>
@ -660,23 +594,35 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Called when <see cref="SelectionModel.PropertyChanged"/> is raised.
/// Called when <see cref="INotifyPropertyChanged.PropertyChanged"/> is raised on
/// <see cref="Selection"/>.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
{
if (Selection.AnchorIndex.GetSize() > 0)
if (Selection.AnchorIndex > 0)
{
ScrollIntoView(Selection.AnchorIndex.GetAt(0));
ScrollIntoView(Selection.AnchorIndex);
}
}
else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex))
{
RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex);
_oldSelectedIndex = SelectedIndex;
}
else if (e.PropertyName == nameof(ISelectionModel.SelectedItem))
{
RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem);
_oldSelectedItem = SelectedItem;
}
}
/// <summary>
/// Called when <see cref="SelectionModel.SelectionChanged"/> is raised.
/// Called when <see cref="ISelectionModel.SelectionChanged"/> event is raised on
/// <see cref="Selection"/>.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
@ -692,46 +638,40 @@ namespace Avalonia.Controls.Primitives
}
}
if (e.SelectedIndices.Count > 0 || e.DeselectedIndices.Count > 0)
foreach (var i in e.SelectedIndexes)
{
foreach (var i in e.SelectedIndices)
{
Mark(i.GetAt(0), true);
}
foreach (var i in e.DeselectedIndices)
{
Mark(i.GetAt(0), false);
}
Mark(i, true);
}
else if (e.DeselectedItems.Count > 0)
foreach (var i in e.DeselectedIndexes)
{
// (De)selected indices being empty means that a selected item was removed from
// the Items (it can't tell us the index of the item because the index is no longer
// valid). In this case, we just update the selection state of all containers.
UpdateContainerSelection();
Mark(i, false);
}
var newSelectedIndex = SelectedIndex;
var newSelectedItem = SelectedItem;
var route = BuildEventRoute(SelectionChangedEvent);
if (newSelectedIndex != _selectedIndex)
if (route.HasHandlers)
{
RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, newSelectedIndex);
_selectedIndex = newSelectedIndex;
var ev = new SelectionChangedEventArgs(
SelectionChangedEvent,
e.DeselectedItems.ToList(),
e.SelectedItems.ToList());
RaiseEvent(ev);
}
}
if (newSelectedItem != _selectedItem)
/// <summary>
/// Called when <see cref="ISelectionModel.LostSelection"/> event is raised on
/// <see cref="Selection"/>.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void OnSelectionModelLostSelection(object sender, EventArgs e)
{
if (AlwaysSelected && Items is object)
{
RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem);
_selectedItem = newSelectedItem;
SelectedIndex = 0;
}
var ev = new SelectionChangedEventArgs(
SelectionChangedEvent,
e.DeselectedItems.ToList(),
e.SelectedItems.ToList());
RaiseEvent(ev);
}
/// <summary>
@ -760,23 +700,6 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Called when the currently selected item is lost and the selection must be changed
/// depending on the <see cref="SelectionMode"/> property.
/// </summary>
private void LostSelection()
{
var items = Items?.Cast<object>();
var index = -1;
if (items != null && AlwaysSelected)
{
index = Math.Min(SelectedIndex, items.Count() - 1);
}
SelectedIndex = index;
}
/// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
@ -819,16 +742,6 @@ namespace Avalonia.Controls.Primitives
}
}
private void UpdateContainerSelection()
{
foreach (var container in ItemContainerGenerator.Containers)
{
MarkContainerSelected(
container.ContainerControl,
Selection.IsSelected(container.Index) != false);
}
}
/// <summary>
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
@ -844,52 +757,92 @@ namespace Avalonia.Controls.Primitives
}
}
private void UpdateFinished()
/// <summary>
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="selected">Whether the item should be selected or deselected.</param>
private int MarkItemSelected(object item, bool selected)
{
Selection.Source = Items;
var index = IndexOf(Items, item);
if (_updateSelectedItem != null)
if (index != -1)
{
SelectedItem = _updateSelectedItem;
MarkItemSelected(index, selected);
}
else
return index;
}
private void UpdateContainerSelection()
{
if (Presenter?.Panel is IPanel panel)
{
if (ItemCount == 0 && SelectedIndex != -1)
foreach (var container in panel.Children)
{
SelectedIndex = -1;
}
else
{
if (_updateSelectedIndex != int.MinValue)
{
SelectedIndex = _updateSelectedIndex;
}
if (AlwaysSelected && SelectedIndex == -1)
{
SelectedIndex = 0;
}
MarkContainerSelected(
container,
Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container)));
}
}
}
private void InternalBeginInit()
private ISelectionModel CreateDefaultSelectionModel()
{
return new SelectionModel<object>
{
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
};
}
private void InitializeSelectionModel(ISelectionModel model)
{
if (_updateCount == 0)
if (_initializing == 0)
{
_updateSelectedIndex = int.MinValue;
model.Source = Items;
}
++_updateCount;
model.PropertyChanged += OnSelectionModelPropertyChanged;
model.SelectionChanged += OnSelectionModelSelectionChanged;
model.LostSelection += OnSelectionModelLostSelection;
if (model.SingleSelect)
{
SelectionMode &= ~SelectionMode.Multiple;
}
else
{
SelectionMode |= SelectionMode.Multiple;
}
_oldSelectedIndex = model.SelectedIndex;
_oldSelectedItem = model.SelectedItem;
if (AlwaysSelected && model.Count == 0)
{
model.SelectedIndex = 0;
}
UpdateContainerSelection();
_selectedItemsSync ??= new SelectedItemsSync(model);
_selectedItemsSync.SelectionModel = model;
if (SelectedIndex != -1)
{
RaiseEvent(new SelectionChangedEventArgs(
SelectionChangedEvent,
Array.Empty<object>(),
Selection.SelectedItems.ToList()));
}
}
private void InternalEndInit()
private void DeinitializeSelectionModel(ISelectionModel? model)
{
Debug.Assert(_updateCount > 0);
if (--_updateCount == 0)
if (model is object)
{
UpdateFinished();
model.PropertyChanged -= OnSelectionModelPropertyChanged;
model.SelectionChanged -= OnSelectionModelSelectionChanged;
}
}
}

49
src/Avalonia.Controls/SelectedItems.cs

@ -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<TValue, Tinfo> : IReadOnlyList<TValue>
where Tinfo : ISelectedItemInfo
{
private readonly List<Tinfo> _infos;
private readonly Func<List<Tinfo>, int, TValue> _getAtImpl;
public SelectedItems(
List<Tinfo> infos,
int count,
Func<List<Tinfo>, int, TValue> getAtImpl)
{
_infos = infos;
_getAtImpl = getAtImpl;
Count = count;
}
public TValue this[int index] => _getAtImpl(_infos, index);
public int Count { get; }
public IEnumerator<TValue> GetEnumerator()
{
for (var i = 0; i < Count; ++i)
{
yield return this[i];
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

66
src/Avalonia.Controls/Selection/ISelectionModel.cs

@ -0,0 +1,66 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
#nullable enable
namespace Avalonia.Controls.Selection
{
public interface ISelectionModel : INotifyPropertyChanged
{
IEnumerable? Source { get; set; }
bool SingleSelect { get; set; }
int SelectedIndex { get; set; }
IReadOnlyList<int> SelectedIndexes { get; }
object? SelectedItem { get; set; }
IReadOnlyList<object?> SelectedItems { get; }
int AnchorIndex { get; set; }
int Count { get; }
public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged;
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged;
public event EventHandler? LostSelection;
public event EventHandler? SourceReset;
public void BeginBatchUpdate();
public void EndBatchUpdate();
bool IsSelected(int index);
void Select(int index);
void Deselect(int index);
void SelectRange(int start, int end);
void DeselectRange(int start, int end);
void SelectAll();
void Clear();
}
public static class SelectionModelExtensions
{
public static IDisposable BatchUpdate(this ISelectionModel model)
{
return new BatchUpdateOperation(model);
}
public struct BatchUpdateOperation : IDisposable
{
private readonly ISelectionModel _owner;
private bool _isDisposed;
public BatchUpdateOperation(ISelectionModel owner)
{
_owner = owner;
_isDisposed = false;
owner.BeginBatchUpdate();
}
public void Dispose()
{
if (!_isDisposed)
{
_owner?.EndBatchUpdate();
_isDisposed = true;
}
}
}
}
}

82
src/Avalonia.Controls/IndexRange.cs → src/Avalonia.Controls/Selection/IndexRange.cs

@ -8,12 +8,18 @@ using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
namespace Avalonia.Controls.Selection
{
internal readonly struct IndexRange : IEquatable<IndexRange>
{
private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue);
public IndexRange(int index)
{
Begin = index;
End = index;
}
public IndexRange(int begin, int end)
{
// Accept out of order begin/end pairs, just swap them.
@ -87,6 +93,43 @@ namespace Avalonia.Controls
public static bool operator ==(IndexRange left, IndexRange right) => left.Equals(right);
public static bool operator !=(IndexRange left, IndexRange right) => !(left == right);
public static bool Contains(IReadOnlyList<IndexRange>? ranges, int index)
{
if (ranges is null || index < 0)
{
return false;
}
foreach (var range in ranges)
{
if (range.Contains(index))
{
return true;
}
}
return false;
}
public static int GetAt(IReadOnlyList<IndexRange> ranges, int index)
{
var currentIndex = 0;
foreach (var range in ranges)
{
var currentCount = range.Count;
if (index >= currentIndex && index < currentIndex + currentCount)
{
return range.Begin + (index - currentIndex);
}
currentIndex += currentCount;
}
throw new IndexOutOfRangeException("The index was out of range.");
}
public static int Add(
IList<IndexRange> ranges,
IndexRange range,
@ -132,6 +175,21 @@ namespace Avalonia.Controls
return result;
}
public static int Add(
IList<IndexRange> destination,
IReadOnlyList<IndexRange> source,
IList<IndexRange>? added = null)
{
var result = 0;
foreach (var range in source)
{
result += Add(destination, range, added);
}
return result;
}
public static int Intersect(
IList<IndexRange> ranges,
IndexRange range,
@ -180,10 +238,15 @@ namespace Avalonia.Controls
}
public static int Remove(
IList<IndexRange> ranges,
IList<IndexRange>? ranges,
IndexRange range,
IList<IndexRange>? removed = null)
{
if (ranges is null)
{
return 0;
}
var result = 0;
for (var i = 0; i < ranges.Count; ++i)
@ -224,15 +287,16 @@ namespace Avalonia.Controls
return result;
}
public static IEnumerable<IndexRange> Subtract(
IndexRange lhs,
IEnumerable<IndexRange> rhs)
public static int Remove(
IList<IndexRange> destination,
IReadOnlyList<IndexRange> source,
IList<IndexRange>? added = null)
{
var result = new List<IndexRange> { lhs };
foreach (var range in rhs)
var result = 0;
foreach (var range in source)
{
Remove(result, range);
result += Remove(destination, range, added);
}
return result;

82
src/Avalonia.Controls/Selection/SelectedIndexes.cs

@ -0,0 +1,82 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Avalonia.Controls.Selection
{
internal class SelectedIndexes<T> : IReadOnlyList<int>
{
private readonly SelectionModel<T>? _owner;
private readonly IReadOnlyList<IndexRange>? _ranges;
public SelectedIndexes(SelectionModel<T> owner) => _owner = owner;
public SelectedIndexes(IReadOnlyList<IndexRange> ranges) => _ranges = ranges;
public int this[int index]
{
get
{
if (index >= Count)
{
throw new IndexOutOfRangeException("The index was out of range.");
}
if (_owner?.SingleSelect == true)
{
return _owner.SelectedIndex;
}
else
{
return IndexRange.GetAt(Ranges!, index);
}
}
}
public int Count
{
get
{
if (_owner?.SingleSelect == true)
{
return _owner.SelectedIndex == -1 ? 0 : 1;
}
else
{
return IndexRange.GetCount(Ranges!);
}
}
}
private IReadOnlyList<IndexRange> Ranges => _ranges ?? _owner!.Ranges!;
public IEnumerator<int> GetEnumerator()
{
IEnumerator<int> SingleSelect()
{
if (_owner.SelectedIndex >= 0)
{
yield return _owner.SelectedIndex;
}
}
if (_owner?.SingleSelect == true)
{
return SingleSelect();
}
else
{
return IndexRange.EnumerateIndices(Ranges).GetEnumerator();
}
}
public static SelectedIndexes<T>? Create(IReadOnlyList<IndexRange>? ranges)
{
return ranges is object ? new SelectedIndexes<T>(ranges) : null;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

121
src/Avalonia.Controls/Selection/SelectedItems.cs

@ -0,0 +1,121 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
#nullable enable
namespace Avalonia.Controls.Selection
{
internal class SelectedItems<T> : IReadOnlyList<T>
{
private readonly SelectionModel<T>? _owner;
private readonly ItemsSourceView<T>? _items;
private readonly IReadOnlyList<IndexRange>? _ranges;
public SelectedItems(SelectionModel<T> owner) => _owner = owner;
public SelectedItems(IReadOnlyList<IndexRange> ranges, ItemsSourceView<T>? items)
{
_ranges = ranges ?? throw new ArgumentNullException(nameof(ranges));
_items = items;
}
[MaybeNull]
public T this[int index]
{
#pragma warning disable CS8766
get
#pragma warning restore CS8766
{
if (index >= Count)
{
throw new IndexOutOfRangeException("The index was out of range.");
}
if (_owner?.SingleSelect == true)
{
return _owner.SelectedItem;
}
else if (Items is object)
{
return Items[index];
}
else
{
return default;
}
}
}
public int Count
{
get
{
if (_owner?.SingleSelect == true)
{
return _owner.SelectedIndex == -1 ? 0 : 1;
}
else
{
return Ranges is object ? IndexRange.GetCount(Ranges) : 0;
}
}
}
private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView;
private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges;
public IEnumerator<T> GetEnumerator()
{
if (_owner?.SingleSelect == true)
{
if (_owner.SelectedIndex >= 0)
{
#pragma warning disable CS8603
yield return _owner.SelectedItem;
#pragma warning restore CS8603
}
}
else
{
var items = Items;
foreach (var range in Ranges!)
{
for (var i = range.Begin; i <= range.End; ++i)
{
#pragma warning disable CS8603
yield return items is object ? items[i] : default;
#pragma warning restore CS8603
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public static SelectedItems<T>? Create(
IReadOnlyList<IndexRange>? ranges,
ItemsSourceView<T>? items)
{
return ranges is object ? new SelectedItems<T>(ranges, items) : null;
}
public class Untyped : IReadOnlyList<object?>
{
private readonly IReadOnlyList<T> _source;
public Untyped(IReadOnlyList<T> source) => _source = source;
public object? this[int index] => _source[index];
public int Count => _source.Count;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<object?> GetEnumerator()
{
foreach (var i in _source)
{
yield return i;
}
}
}
}
}

726
src/Avalonia.Controls/Selection/SelectionModel.cs

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

18
src/Avalonia.Controls/Selection/SelectionModelIndexesChangedEventArgs.cs

@ -0,0 +1,18 @@
using System;
#nullable enable
namespace Avalonia.Controls.Selection
{
public class SelectionModelIndexesChangedEventArgs : EventArgs
{
public SelectionModelIndexesChangedEventArgs(int startIndex, int delta)
{
StartIndex = startIndex;
Delta = delta;
}
public int StartIndex { get; }
public int Delta { get; }
}
}

85
src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs

@ -0,0 +1,85 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Controls.Selection;
#nullable enable
namespace Avalonia.Controls.Selection
{
public abstract class SelectionModelSelectionChangedEventArgs : EventArgs
{
/// <summary>
/// Gets the indexes of the items that were removed from the selection.
/// </summary>
public abstract IReadOnlyList<int> DeselectedIndexes { get; }
/// <summary>
/// Gets the indexes of the items that were added to the selection.
/// </summary>
public abstract IReadOnlyList<int> SelectedIndexes { get; }
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public IReadOnlyList<object?> DeselectedItems => GetUntypedDeselectedItems();
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public IReadOnlyList<object?> SelectedItems => GetUntypedSelectedItems();
protected abstract IReadOnlyList<object?> GetUntypedDeselectedItems();
protected abstract IReadOnlyList<object?> GetUntypedSelectedItems();
}
public class SelectionModelSelectionChangedEventArgs<T> : SelectionModelSelectionChangedEventArgs
{
private IReadOnlyList<object?>? _deselectedItems;
private IReadOnlyList<object?>? _selectedItems;
public SelectionModelSelectionChangedEventArgs(
IReadOnlyList<int>? deselectedIndices = null,
IReadOnlyList<int>? selectedIndices = null,
IReadOnlyList<T>? deselectedItems = null,
IReadOnlyList<T>? selectedItems = null)
{
DeselectedIndexes = deselectedIndices ?? Array.Empty<int>();
SelectedIndexes = selectedIndices ?? Array.Empty<int>();
DeselectedItems = deselectedItems ?? Array.Empty<T>();
SelectedItems = selectedItems ?? Array.Empty<T>();
}
/// <summary>
/// Gets the indexes of the items that were removed from the selection.
/// </summary>
public override IReadOnlyList<int> DeselectedIndexes { get; }
/// <summary>
/// Gets the indexes of the items that were added to the selection.
/// </summary>
public override IReadOnlyList<int> SelectedIndexes { get; }
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public new IReadOnlyList<T> DeselectedItems { get; }
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public new IReadOnlyList<T> SelectedItems { get; }
protected override IReadOnlyList<object?> GetUntypedDeselectedItems()
{
return _deselectedItems ??= (DeselectedItems as IReadOnlyList<object?>) ??
new SelectedItems<T>.Untyped(DeselectedItems);
}
protected override IReadOnlyList<object?> GetUntypedSelectedItems()
{
return _selectedItems ??= (SelectedItems as IReadOnlyList<object?>) ??
new SelectedItems<T>.Untyped(SelectedItems);
}
}
}

286
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -0,0 +1,286 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Controls.Utils;
#nullable enable
namespace Avalonia.Controls.Selection
{
public abstract class SelectionNodeBase<T> : ICollectionChangedListener
{
private IEnumerable? _source;
private bool _rangesEnabled;
private List<IndexRange>? _ranges;
private int _collectionChanging;
protected IEnumerable? Source
{
get => _source;
set
{
if (_source != value)
{
ItemsView?.RemoveListener(this);
_source = value;
ItemsView = value is object ? ItemsSourceView<T>.GetOrCreate(value) : null;
ItemsView?.AddListener(this);
}
}
}
protected bool IsSourceCollectionChanging => _collectionChanging > 0;
protected bool RangesEnabled
{
get => _rangesEnabled;
set
{
if (_rangesEnabled != value)
{
_rangesEnabled = value;
if (!_rangesEnabled)
{
_ranges = null;
}
}
}
}
internal ItemsSourceView<T>? ItemsView { get; set; }
internal IReadOnlyList<IndexRange> Ranges
{
get
{
if (!RangesEnabled)
{
throw new InvalidOperationException("Ranges not enabled.");
}
return _ranges ??= new List<IndexRange>();
}
}
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
++_collectionChanging;
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
OnSourceCollectionChanged(e);
}
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
if (--_collectionChanging == 0)
{
OnSourceCollectionChangeFinished();
}
}
protected abstract void OnSourceCollectionChangeFinished();
private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta);
private protected abstract void OnSourceReset();
private protected abstract void OnSelectionChanged(IReadOnlyList<T> deselectedItems);
private protected int CommitSelect(IndexRange range)
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Add(_ranges, range);
}
return 0;
}
private protected int CommitSelect(IReadOnlyList<IndexRange> ranges)
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Add(_ranges, ranges);
}
return 0;
}
private protected int CommitDeselect(IndexRange range)
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Remove(_ranges, range);
}
return 0;
}
private protected int CommitDeselect(IReadOnlyList<IndexRange> ranges)
{
if (RangesEnabled && _ranges is object)
{
return IndexRange.Remove(_ranges, ranges);
}
return 0;
}
private protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
{
var count = items.Count;
var shifted = false;
if (_ranges is object)
{
List<IndexRange>? toAdd = null;
for (var i = 0; i < Ranges!.Count; ++i)
{
var range = Ranges[i];
// The range is after the inserted items, need to shift the range right
if (range.End >= index)
{
int begin = range.Begin;
// If the index left of newIndex is inside the range,
// Split the range and remember the left piece to add later
if (range.Contains(index - 1))
{
range.Split(index - 1, out var before, out _);
(toAdd ??= new List<IndexRange>()).Add(before);
begin = index;
}
// Shift the range to the right
_ranges[i] = new IndexRange(begin + count, range.End + count);
shifted = true;
}
}
if (toAdd is object)
{
foreach (var range in toAdd)
{
IndexRange.Add(_ranges, range);
}
}
}
return new CollectionChangeState
{
ShiftIndex = index,
ShiftDelta = shifted ? count : 0,
};
}
private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items)
{
var count = items.Count;
var removedRange = new IndexRange(index, index + count - 1);
bool shifted = false;
List<T>? removed = null;
if (_ranges is object)
{
var deselected = new List<IndexRange>();
if (IndexRange.Remove(_ranges, removedRange, deselected) > 0)
{
removed = new List<T>();
foreach (var range in deselected)
{
for (var i = range.Begin; i <= range.End; ++i)
{
#pragma warning disable CS8604
removed.Add((T)items[i - index]);
#pragma warning restore CS8604
}
}
}
for (var i = 0; i < Ranges!.Count; ++i)
{
var existing = Ranges[i];
if (existing.End > removedRange.Begin)
{
_ranges[i] = new IndexRange(existing.Begin - count, existing.End - count);
shifted = true;
}
}
}
return new CollectionChangeState
{
ShiftIndex = index,
ShiftDelta = shifted ? -count : 0,
RemovedItems = removed,
};
}
private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var shiftDelta = 0;
var shiftIndex = -1;
List<T>? removed = null;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
var change = OnItemsAdded(e.NewStartingIndex, e.NewItems);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
break;
}
case NotifyCollectionChangedAction.Remove:
{
var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
removed = change.RemovedItems;
break;
}
case NotifyCollectionChangedAction.Replace:
{
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems);
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems);
shiftIndex = removeChange.ShiftIndex;
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
removed = removeChange.RemovedItems;
}
break;
case NotifyCollectionChangedAction.Reset:
OnSourceReset();
break;
}
if (shiftDelta != 0)
{
OnIndexesChanged(shiftIndex, shiftDelta);
}
if (removed is object)
{
OnSelectionChanged(removed);
}
}
private protected struct CollectionChangeState
{
public int ShiftIndex;
public int ShiftDelta;
public List<T>? RemovedItems;
}
}
}

894
src/Avalonia.Controls/SelectionModel.cs

@ -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<IndexPath>? _selectedIndicesCached;
private IReadOnlyList<object?>? _selectedItemsCached;
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
public event EventHandler<SelectionModelChildrenRequestedEventArgs>? ChildrenRequested;
public event PropertyChangedEventHandler? PropertyChanged;
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged;
public SelectionModel()
{
_rootNode = new SelectionNode(this, null);
SharedLeafNode = new SelectionNode(this, null);
}
public object? Source
{
get => _rootNode.Source;
set
{
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<int>();
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<object?> SelectedItems
{
get
{
if (_selectedItemsCached == null)
{
var selectedInfos = new List<SelectedItemInfo>();
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<object?, SelectedItemInfo> (
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<IndexPath> SelectedIndices
{
get
{
if (_selectedIndicesCached == null)
{
var selectedInfos = new List<SelectedItemInfo>();
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<IndexPath, SelectedItemInfo>(
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<object?>? removedItems)
{
SelectionModelSelectionChangedEventArgs? e = null;
if (selectionInvalidated)
{
e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null);
}
OnSelectionChanged(e);
ApplyAutoSelect(true);
}
internal IObservable<object?>? ResolvePath(
object data,
IndexPath dataIndexPath,
IndexPath finalIndexPath)
{
IObservable<object?>? 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<SelectionNodeOperation>();
_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();
}
}
}

170
src/Avalonia.Controls/SelectionModelChangeSet.cs

@ -1,170 +0,0 @@
using System;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
{
internal class SelectionModelChangeSet
{
private readonly List<SelectionNodeOperation> _changes;
public SelectionModelChangeSet(List<SelectionNodeOperation> 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<IndexPath, SelectionNodeOperation>(
_changes,
deselectedIndexCount,
GetDeselectedIndexAt);
var selectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
_changes,
selectedIndexCount,
GetSelectedIndexAt);
var deselectedItems = new SelectedItems<object?, SelectionNodeOperation>(
_changes,
deselectedItemCount,
GetDeselectedItemAt);
var selectedItems = new SelectedItems<object?, SelectionNodeOperation>(
_changes,
selectedItemCount,
GetSelectedItemAt);
return new SelectionModelSelectionChangedEventArgs(
deselectedIndices,
selectedIndices,
deselectedItems,
selectedItems);
}
private IndexPath GetDeselectedIndexAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.DeselectedCount;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private IndexPath GetSelectedIndexAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.SelectedCount;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private object? GetDeselectedItemAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.DeselectedCount : 0;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private object? GetSelectedItemAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.SelectedCount : 0;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private IndexPath GetIndexAt(
List<SelectionNodeOperation> infos,
int index,
Func<SelectionNodeOperation, int> getCount,
Func<SelectionNodeOperation, List<IndexRange>?> 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<SelectionNodeOperation> infos,
int index,
Func<SelectionNodeOperation, int> getCount,
Func<SelectionNodeOperation, List<IndexRange>?> 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<IndexRange>? 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();
}
}
}

103
src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs

@ -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
{
/// <summary>
/// Provides data for the <see cref="SelectionModel.ChildrenRequested"/> event.
/// </summary>
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);
}
/// <summary>
/// Gets or sets an observable which produces the children of the <see cref="Source"/>
/// object.
/// </summary>
public IObservable<object?>? Children { get; set; }
/// <summary>
/// Gets the object whose children are being requested.
/// </summary>
public object Source
{
get
{
if (_throwOnAccess)
{
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
}
return _source!;
}
}
/// <summary>
/// Gets the index of the object whose children are being requested.
/// </summary>
public IndexPath SourceIndex
{
get
{
if (_throwOnAccess)
{
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
}
return _sourceIndexPath;
}
}
/// <summary>
/// Gets the index of the final object which is being attempted to be retrieved.
/// </summary>
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;
}
}
}

47
src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs

@ -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<IndexPath>? deselectedIndices,
IReadOnlyList<IndexPath>? selectedIndices,
IReadOnlyList<object?>? deselectedItems,
IReadOnlyList<object?>? selectedItems)
{
DeselectedIndices = deselectedIndices ?? Array.Empty<IndexPath>();
SelectedIndices = selectedIndices ?? Array.Empty<IndexPath>();
DeselectedItems = deselectedItems ?? Array.Empty<object?>();
SelectedItems= selectedItems ?? Array.Empty<object?>();
}
/// <summary>
/// Gets the indices of the items that were removed from the selection.
/// </summary>
public IReadOnlyList<IndexPath> DeselectedIndices { get; }
/// <summary>
/// Gets the indices of the items that were added to the selection.
/// </summary>
public IReadOnlyList<IndexPath> SelectedIndices { get; }
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public IReadOnlyList<object?> DeselectedItems { get; }
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public IReadOnlyList<object?> SelectedItems { get; }
}
}

971
src/Avalonia.Controls/SelectionNode.cs

@ -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
{
/// <summary>
/// Tracks nested selection.
/// </summary>
/// <remarks>
/// SelectionNode is the internal tree data structure that we keep track of for selection in
/// a nested scenario. This would map to one ItemsSourceView/Collection. This node reacts to
/// collection changes and keeps the selected indices up to date. This can either be a leaf
/// node or a non leaf node.
/// </remarks>
internal class SelectionNode : IDisposable
{
private readonly SelectionModel _manager;
private readonly List<SelectionNode?> _childrenNodes = new List<SelectionNode?>();
private readonly SelectionNode? _parent;
private readonly List<IndexRange> _selected = new List<IndexRange>();
private readonly List<int> _selectedIndicesCached = new List<int>();
private IDisposable? _childrenSubscription;
private SelectionNodeOperation? _operation;
private object? _source;
private bool _selectedIndicesCacheIsValid;
private bool _retainSelectionOnReset;
private List<object?>? _selectedItems;
public SelectionNode(SelectionModel manager, SelectionNode? parent)
{
_manager = manager;
_parent = parent;
}
public int AnchorIndex { get; set; } = -1;
public bool RetainSelectionOnReset
{
get => _retainSelectionOnReset;
set
{
if (_retainSelectionOnReset != value)
{
_retainSelectionOnReset = value;
if (_retainSelectionOnReset)
{
_selectedItems = new List<object?>();
PopulateSelectedItemsFromSelectedIndices();
}
else
{
_selectedItems = null;
}
foreach (var child in _childrenNodes)
{
if (child != null)
{
child.RetainSelectionOnReset = value;
}
}
}
}
}
public object? Source
{
get => _source;
set
{
if (_source != value)
{
if (_source != null)
{
ClearSelection();
ClearChildNodes();
UnhookCollectionChangedHandler();
}
_source = value;
// Setup ItemsSourceView
var newDataSource = value as ItemsSourceView;
if (value != null && newDataSource == null)
{
newDataSource = new ItemsSourceView((IEnumerable)value);
}
ItemsSourceView = newDataSource;
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<IndexRange>();
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<int>(); ;
var parent = _parent;
var child = this;
while (parent != null)
{
var childNodes = parent._childrenNodes;
var index = childNodes.IndexOf(child);
// We are walking up to the parent, so the path will be backwards
path.Insert(0, index);
child = parent;
parent = parent._parent;
}
return new IndexPath(path);
}
}
// For a genuine tree view, we dont know which node is leaf until we
// actually walk to it, so currently the tree builds up to the leaf. I don't
// create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid
// an explosion of node objects. However, I'm still creating the m_childrenNodes
// collection unfortunately.
public SelectionNode? GetAt(int index, bool realizeChild, 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<object?>? 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<object> || childData is IList)
{
child = new SelectionNode(_manager, parent: this);
child.Source = childData;
}
else
{
child = _manager.SharedLeafNode;
}
if (_operation != null && child != _manager.SharedLeafNode)
{
child.BeginOperation();
}
_childrenNodes[index] = child;
RealizedChildrenNodeCount++;
}
else
{
child = _childrenNodes[index];
}
}
else
{
if (_childrenNodes.Count > 0)
{
child = _childrenNodes[index];
}
}
return child;
}
public void SetChildrenObservable(IObservable<object?> resolver)
{
_childrenSubscription = resolver.Subscribe(x =>
{
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<int> SelectedIndices
{
get
{
if (!_selectedIndicesCacheIsValid)
{
_selectedIndicesCacheIsValid = true;
foreach (var range in _selected)
{
for (int index = range.Begin; index <= range.End; index++)
{
// Avoid duplicates
if (!_selectedIndicesCached.Contains(index))
{
_selectedIndicesCached.Add(index);
}
}
}
// Sort the list for easy consumption
_selectedIndicesCached.Sort();
}
return _selectedIndicesCached;
}
}
public IEnumerable<object> SelectedItems
{
get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x));
}
public void Dispose()
{
_childrenSubscription?.Dispose();
ItemsSourceView?.Dispose();
ClearChildNodes();
UnhookCollectionChangedHandler();
}
public void BeginOperation()
{
if (_operation != null)
{
throw new AvaloniaInternalException("Selection operation already in progress.");
}
_operation = new SelectionNodeOperation(this);
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null && child != _manager.SharedLeafNode)
{
child.BeginOperation();
}
}
}
public void EndOperation(List<SelectionNodeOperation> changes)
{
if (_operation == null)
{
throw new AvaloniaInternalException("No selection operation in progress.");
}
if (_operation.HasChanges)
{
changes.Add(_operation);
}
_operation = null;
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null && child != _manager.SharedLeafNode)
{
child.EndOperation(changes);
}
}
}
public bool Cleanup()
{
var result = SelectedCount == 0;
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null)
{
if (child.Cleanup())
{
child.Dispose();
_childrenNodes[i] = null;
}
else
{
result = false;
}
}
}
return result;
}
public bool Select(int index, bool select)
{
return Select(index, select, raiseOnSelectionChanged: true);
}
public bool ToggleSelect(int index)
{
return Select(index, !IsSelected(index));
}
public void SelectAll()
{
if (ItemsSourceView != null)
{
var size = ItemsSourceView.Count;
if (size > 0)
{
SelectRange(new IndexRange(0, size - 1), select: true);
}
}
}
public void Clear() => ClearSelection();
public bool SelectRange(IndexRange range, bool select)
{
if (IsValidIndex(range.Begin) && IsValidIndex(range.End))
{
if (select)
{
AddRange(range, raiseOnSelectionChanged: true);
}
else
{
RemoveRange(range, raiseOnSelectionChanged: true);
}
return true;
}
return false;
}
private void HookupCollectionChangedHandler()
{
if (ItemsSourceView != null)
{
ItemsSourceView.CollectionChanged += OnSourceListChanged;
}
}
private void UnhookCollectionChangedHandler()
{
if (ItemsSourceView != null)
{
ItemsSourceView.CollectionChanged -= OnSourceListChanged;
}
}
private bool IsValidIndex(int index)
{
return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count);
}
private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged)
{
var selected = new List<IndexRange>();
SelectedCount += IndexRange.Add(_selected, addRange, selected);
if (selected.Count > 0)
{
_operation?.Selected(selected);
if (_selectedItems != null && ItemsSourceView != null)
{
for (var i = addRange.Begin; i <= addRange.End; ++i)
{
_selectedItems.Add(ItemsSourceView!.GetAt(i));
}
}
if (raiseOnSelectionChanged)
{
OnSelectionChanged();
}
}
}
private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged)
{
var removed = new List<IndexRange>();
SelectedCount -= IndexRange.Remove(_selected, removeRange, removed);
if (removed.Count > 0)
{
_operation?.Deselected(removed);
if (_selectedItems != null)
{
for (var i = removeRange.Begin; i <= removeRange.End; ++i)
{
_selectedItems.Remove(ItemsSourceView!.GetAt(i));
}
}
if (raiseOnSelectionChanged)
{
OnSelectionChanged();
}
}
}
private void ClearSelection()
{
// Deselect all items
if (_selected.Count > 0)
{
_operation?.Deselected(_selected);
_selected.Clear();
OnSelectionChanged();
}
_selectedItems?.Clear();
SelectedCount = 0;
AnchorIndex = -1;
}
private void ClearChildNodes()
{
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<object?>? removed = null;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
{
selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
}
case NotifyCollectionChangedAction.Remove:
{
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
break;
}
case NotifyCollectionChangedAction.Reset:
{
if (_selectedItems == null)
{
ClearSelection();
}
else
{
removed = RecreateSelectionFromSelectedItems();
}
selectionInvalidated = true;
break;
}
case NotifyCollectionChangedAction.Replace:
{
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
}
}
if (selectionInvalidated)
{
OnSelectionChanged();
}
_manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed);
}
private bool OnItemsAdded(int index, int count)
{
var selectionInvalidated = false;
// Update ranges for leaf items
var toAdd = new List<IndexRange>();
for (int i = 0; i < _selected.Count; i++)
{
var range = _selected[i];
// The range is after the inserted items, need to shift the range right
if (range.End >= index)
{
int begin = range.Begin;
// If the index left of newIndex is inside the range,
// Split the range and remember the left piece to add later
if (range.Contains(index - 1))
{
range.Split(index - 1, out var before, out _);
toAdd.Add(before);
begin = index;
}
// Shift the range to the right
_selected[i] = new IndexRange(begin + count, range.End + count);
selectionInvalidated = true;
}
}
// Add the left sides of the split ranges
_selected.AddRange(toAdd);
// Update for non-leaf if we are tracking non-leaf nodes
if (_childrenNodes.Count > 0)
{
selectionInvalidated = true;
for (int i = 0; i < count; i++)
{
_childrenNodes.Insert(index, null);
}
}
// Adjust the anchor
if (AnchorIndex >= index)
{
AnchorIndex += count;
}
// Check if adding a node invalidated an ancestors
// selection state. For example if parent was selected before
// adding a new item makes the parent partially selected now.
if (!selectionInvalidated)
{
var parent = _parent;
while (parent != null)
{
var isSelected = parent.IsSelectedWithPartial();
// If a parent is selected, then it will become partially selected.
// If it is not selected or partially selected - there is no change.
if (isSelected == true)
{
selectionInvalidated = true;
break;
}
parent = parent._parent;
}
}
return selectionInvalidated;
}
private (bool, List<object?>) OnItemsRemoved(int index, IList items)
{
var selectionInvalidated = false;
var removed = new List<object?>();
var count = items.Count;
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<object?> RecreateSelectionFromSelectedItems()
{
var removed = new List<object?>();
_selected.Clear();
SelectedCount = 0;
for (var i = 0; i < _selectedItems!.Count; ++i)
{
var item = _selectedItems[i];
var index = ItemsSourceView!.IndexOf(item);
if (index != -1)
{
IndexRange.Add(_selected, new IndexRange(index, index));
++SelectedCount;
}
else
{
removed.Add(item);
_selectedItems.RemoveAt(i--);
}
}
return removed;
}
public enum SelectionState
{
Selected,
NotSelected,
PartiallySelected
}
}
}

110
src/Avalonia.Controls/SelectionNodeOperation.cs

@ -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<IndexRange>? _selected;
private List<IndexRange>? _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<IndexRange>? SelectedRanges => _selected;
public List<IndexRange>? 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<IndexRange> ranges)
{
foreach (var range in ranges)
{
Selected(range);
}
}
public void Deselected(IndexRange range)
{
Add(range, ref _deselected, _selected);
_deselectedCount = -1;
}
public void Deselected(IEnumerable<IndexRange> ranges)
{
foreach (var range in ranges)
{
Deselected(range);
}
}
private static void Add(
IndexRange range,
ref List<IndexRange>? add,
List<IndexRange>? remove)
{
if (remove != null)
{
var removed = new List<IndexRange>();
IndexRange.Remove(remove, range, removed);
var selected = IndexRange.Subtract(range, removed);
if (selected.Any())
{
add ??= new List<IndexRange>();
foreach (var r in selected)
{
IndexRange.Add(add, r);
}
}
}
else
{
add ??= new List<IndexRange>();
IndexRange.Add(add, range);
}
}
}
}

669
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);
/// <summary>
/// Defines the <see cref="Selection"/> property.
/// </summary>
public static readonly DirectProperty<TreeView, ISelectionModel> SelectionProperty =
SelectingItemsControl.SelectionProperty.AddOwner<TreeView>(
o => o.Selection,
(o, v) => o.Selection = v);
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
/// </summary>
public static readonly StyledProperty<SelectionMode> SelectionModeProperty =
ListBox.SelectionModeProperty.AddOwner<TreeView>();
/// <summary>
/// Defines the <see cref="SelectionChanged"/> property.
/// </summary>
public static RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
SelectingItemsControl.SelectionChangedEvent;
private static readonly IList Empty = Array.Empty<object>();
private object _selectedItem;
private ISelectionModel _selection;
private readonly SelectedItemsSync _selectedItems;
private IList _selectedItems;
private bool _syncingSelectedItems;
/// <summary>
/// Initializes static members of the <see cref="TreeView"/> 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);
}
/// <summary>
/// Occurs when the control's selection changes.
/// </summary>
@ -125,94 +107,56 @@ namespace Avalonia.Controls
/// </remarks>
public object SelectedItem
{
get => Selection.SelectedItem;
set => Selection.SelectedIndex = IndexFromItem(value);
}
get => _selectedItem;
set
{
var selectedItems = SelectedItems;
/// <summary>
/// Gets or sets the selected items.
/// </summary>
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();
}
}
}
/// <summary>
/// Gets or sets a model holding the current selection.
/// Gets or sets the selected items.
/// </summary>
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<object> 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<object>();
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<object>();
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.
/// </remarks>
public void SelectAll() => Selection.SelectAll();
public void SelectAll()
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
}
/// <summary>
/// Deselects all items in the <see cref="TreeView"/>.
/// </summary>
public void UnselectAll() => Selection.ClearSelection();
public void UnselectAll()
{
SelectedItems.Clear();
}
/// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
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);
}
/// <summary>
/// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
IList added = null;
IList removed = null;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast<object>().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]);
}
}
/// <summary>
/// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
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
}
}
/// <summary>
/// Called when <see cref="SelectionModel.PropertyChanged"/> is raised.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
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);
}
}
}
/// <summary>
/// Called when <see cref="SelectionModel.SelectionChanged"/> is raised.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
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);
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
@ -494,18 +525,6 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == SelectionModeProperty)
{
var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
}
}
/// <summary>
/// Updates the selection for an item based on user interaction.
/// </summary>
@ -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
}
}
/// <summary>
/// Find which node is first in hierarchy.
/// </summary>
/// <param name="treeView">Search root.</param>
/// <param name="nodeA">Nodes to find.</param>
/// <param name="nodeB">Node to find.</param>
/// <returns>Found first node.</returns>
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<ItemContainerInfo> 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);
}
/// <summary>
/// Returns all items that belong to containers between <paramref name="from"/> and <paramref name="to"/>.
/// The range is inclusive.
/// </summary>
/// <param name="from">From container.</param>
/// <param name="to">To container.</param>
private List<object> GetItemsInRange(TreeViewItem from, TreeViewItem to)
{
var items = new List<object>();
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;
}
/// <summary>
/// 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<int>();
while (true)
{
if (container.Level == 0)
{
var treeView = container.FindAncestorOfType<TreeView>();
if (treeView == null)
{
return default;
}
result.Add(treeView.ItemContainerGenerator.IndexFromContainer(container));
result.Reverse();
return new IndexPath(result);
}
else
{
var parent = container.FindAncestorOfType<TreeViewItem>();
if (parent == null)
{
return default;
}
result.Add(parent.ItemContainerGenerator.IndexFromContainer(container));
container = parent;
}
}
}
private IndexPath IndexFromItem(object item)
/// <summary>
/// Makes a list of objects equal another (though doesn't preserve order).
/// </summary>
/// <param name="items">The items collection.</param>
/// <param name="desired">The desired items.</param>
private static void SynchronizeItems(IList items, IEnumerable<object> desired)
{
var container = ItemContainerGenerator.Index.ContainerFromItem(item) as TreeViewItem;
var list = items.Cast<object>().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;
}
}
}

140
src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Threading;
using Avalonia.Utilities;
#nullable enable
namespace Avalonia.Controls.Utils
{
internal interface ICollectionChangedListener
{
void PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e);
void Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e);
void PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e);
}
internal class CollectionChangedEventManager : IWeakSubscriber<NotifyCollectionChangedEventArgs>
{
public static CollectionChangedEventManager Instance { get; } = new CollectionChangedEventManager();
private ConditionalWeakTable<INotifyCollectionChanged, List<WeakReference<ICollectionChangedListener>>> _entries =
new ConditionalWeakTable<INotifyCollectionChanged, List<WeakReference<ICollectionChangedListener>>>();
private CollectionChangedEventManager()
{
}
public void AddListener(INotifyCollectionChanged collection, ICollectionChangedListener listener)
{
collection = collection ?? throw new ArgumentNullException(nameof(collection));
listener = listener ?? throw new ArgumentNullException(nameof(listener));
Dispatcher.UIThread.VerifyAccess();
if (!_entries.TryGetValue(collection, out var listeners))
{
listeners = new List<WeakReference<ICollectionChangedListener>>();
_entries.Add(collection, listeners);
WeakSubscriptionManager.Subscribe(
collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
}
foreach (var l in listeners)
{
if (l.TryGetTarget(out var target) && target == listener)
{
throw new InvalidOperationException(
"Collection listener already added for this collection/listener combination.");
}
}
listeners.Add(new WeakReference<ICollectionChangedListener>(listener));
}
public void RemoveListener(INotifyCollectionChanged collection, ICollectionChangedListener listener)
{
collection = collection ?? throw new ArgumentNullException(nameof(collection));
listener = listener ?? throw new ArgumentNullException(nameof(listener));
Dispatcher.UIThread.VerifyAccess();
if (_entries.TryGetValue(collection, out var listeners))
{
for (var i = 0; i < listeners.Count; ++i)
{
if (listeners[i].TryGetTarget(out var target) && target == listener)
{
listeners.RemoveAt(i);
if (listeners.Count == 0)
{
WeakSubscriptionManager.Unsubscribe(
collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
_entries.Remove(collection);
}
return;
}
}
}
throw new InvalidOperationException(
"Collection listener not registered for this collection/listener combination.");
}
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object sender, NotifyCollectionChangedEventArgs e)
{
static void Notify(
INotifyCollectionChanged incc,
NotifyCollectionChangedEventArgs args,
List<WeakReference<ICollectionChangedListener>> listeners)
{
foreach (var l in listeners)
{
if (l.TryGetTarget(out var target))
{
target.PreChanged(incc, args);
}
}
foreach (var l in listeners)
{
if (l.TryGetTarget(out var target))
{
target.Changed(incc, args);
}
}
foreach (var l in listeners)
{
if (l.TryGetTarget(out var target))
{
target.PostChanged(incc, args);
}
}
}
if (sender is INotifyCollectionChanged incc && _entries.TryGetValue(incc, out var listeners))
{
var l = listeners.ToList();
if (Dispatcher.UIThread.CheckAccess())
{
Notify(incc, e, l);
}
else
{
var inccCapture = incc;
var eCapture = e;
Dispatcher.UIThread.Post(() => Notify(inccCapture, eCapture, l));
}
}
}
}
}

211
src/Avalonia.Controls/Utils/SelectedItemsSync.cs

@ -4,6 +4,7 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Selection;
#nullable enable
@ -12,121 +13,118 @@ namespace Avalonia.Controls.Utils
/// <summary>
/// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
/// </summary>
internal class SelectedItemsSync
internal class SelectedItemsSync : IDisposable
{
private IList? _items;
private ISelectionModel _selectionModel;
private IList _selectedItems;
private bool _updatingItems;
private bool _updatingModel;
private bool _initializeOnSourceAssignment;
public SelectedItemsSync(ISelectionModel model)
{
model = model ?? throw new ArgumentNullException(nameof(model));
Model = model;
_selectionModel = model ?? throw new ArgumentNullException(nameof(model));
_selectedItems = new AvaloniaList<object?>();
SyncSelectedItemsWithSelectionModel();
SubscribeToSelectedItems(_selectedItems);
SubscribeToSelectionModel(model);
}
public ISelectionModel Model { get; private set; }
public IList GetOrCreateItems()
public ISelectionModel SelectionModel
{
if (_items == null)
get => _selectionModel;
set
{
var items = new AvaloniaList<object>(Model.SelectedItems);
items.CollectionChanged += ItemsCollectionChanged;
Model.SelectionChanged += SelectionModelSelectionChanged;
_items = items;
if (_selectionModel != value)
{
value = value ?? throw new ArgumentNullException(nameof(value));
UnsubscribeFromSelectionModel(_selectionModel);
_selectionModel = value;
SubscribeToSelectionModel(_selectionModel);
SyncSelectedItemsWithSelectionModel();
}
}
return _items;
}
public void SetItems(IList? items)
public IList SelectedItems
{
items ??= new AvaloniaList<object>();
if (items.IsFixedSize)
get => _selectedItems;
set
{
throw new NotSupportedException(
"Cannot assign fixed size selection to SelectedItems.");
}
value ??= new AvaloniaList<object?>();
if (_items is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= ItemsCollectionChanged;
}
if (_selectedItems != value)
{
if (value.IsFixedSize)
{
throw new NotSupportedException(
"Cannot assign fixed size selection to SelectedItems.");
}
if (_items == null)
{
Model.SelectionChanged += SelectionModelSelectionChanged;
UnsubscribeFromSelectedItems(_selectedItems);
_selectedItems = value;
SubscribeToSelectedItems(_selectedItems);
SyncSelectionModelWithSelectedItems();
}
}
}
public void Dispose()
{
UnsubscribeFromSelectedItems(_selectedItems);
UnsubscribeFromSelectionModel(_selectionModel);
}
private void SyncSelectedItemsWithSelectionModel()
{
_updatingItems = true;
try
{
_updatingModel = true;
_items = items;
_selectedItems.Clear();
if (Model.Source is object)
if (_selectionModel.Source is object)
{
using (Model.Update())
foreach (var i in _selectionModel.SelectedItems)
{
Model.ClearSelection();
Add(items);
_selectedItems.Add(i);
}
}
else if (!_initializeOnSourceAssignment)
{
Model.PropertyChanged += SelectionModelPropertyChanged;
_initializeOnSourceAssignment = true;
}
if (_items is INotifyCollectionChanged incc2)
{
incc2.CollectionChanged += ItemsCollectionChanged;
}
}
finally
{
_updatingModel = false;
_updatingItems = false;
}
}
public void SetModel(ISelectionModel model)
private void SyncSelectionModelWithSelectedItems()
{
model = model ?? throw new ArgumentNullException(nameof(model));
_updatingModel = true;
if (_items != null)
try
{
Model.PropertyChanged -= SelectionModelPropertyChanged;
Model.SelectionChanged -= SelectionModelSelectionChanged;
Model = model;
Model.SelectionChanged += SelectionModelSelectionChanged;
_initializeOnSourceAssignment = false;
try
if (_selectionModel.Source is object)
{
_updatingItems = true;
_items.Clear();
foreach (var i in model.SelectedItems)
using (_selectionModel.BatchUpdate())
{
_items.Add(i);
SelectionModel.Clear();
Add(_selectedItems);
}
}
finally
{
_updatingItems = false;
}
}
finally
{
_updatingModel = false;
}
}
private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_updatingItems)
{
return;
}
if (_items == null)
if (_selectedItems == null)
{
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
}
@ -135,18 +133,18 @@ namespace Avalonia.Controls.Utils
{
foreach (var i in e.OldItems)
{
var index = IndexOf(Model.Source, i);
var index = IndexOf(SelectionModel.Source, i);
if (index != -1)
{
Model.Deselect(index);
SelectionModel.Deselect(index);
}
}
}
try
{
using var operation = Model.Update();
using var operation = SelectionModel.BatchUpdate();
_updatingModel = true;
@ -163,8 +161,8 @@ namespace Avalonia.Controls.Utils
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
Model.ClearSelection();
Add(_items);
SelectionModel.Clear();
Add(_selectedItems);
break;
}
}
@ -178,46 +176,37 @@ namespace Avalonia.Controls.Utils
{
foreach (var i in newItems)
{
var index = IndexOf(Model.Source, i);
var index = IndexOf(SelectionModel.Source, i);
if (index != -1)
{
Model.Select(index);
SelectionModel.Select(index);
}
}
}
private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_initializeOnSourceAssignment &&
_items != null &&
e.PropertyName == nameof(SelectionModel.Source))
if (e.PropertyName == nameof(ISelectionModel.Source))
{
try
if (_selectedItems.Count > 0)
{
_updatingModel = true;
Add(_items);
_initializeOnSourceAssignment = false;
SyncSelectionModelWithSelectedItems();
}
finally
else
{
_updatingModel = false;
SyncSelectedItemsWithSelectionModel();
}
}
}
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
if (_updatingModel)
if (_updatingModel || _selectionModel.Source is null)
{
return;
}
if (_items == null)
{
throw new AvaloniaInternalException("SelectionModelChanged raised but we don't have items.");
}
try
{
var deselected = e.DeselectedItems.ToList();
@ -227,12 +216,12 @@ namespace Avalonia.Controls.Utils
foreach (var i in deselected)
{
_items.Remove(i);
_selectedItems.Remove(i);
}
foreach (var i in selected)
{
_items.Add(i);
_selectedItems.Add(i);
}
}
finally
@ -241,7 +230,43 @@ namespace Avalonia.Controls.Utils
}
}
private static int IndexOf(object source, object item)
private void SelectionModelSourceReset(object sender, EventArgs e)
{
SyncSelectionModelWithSelectedItems();
}
private void SubscribeToSelectedItems(IList selectedItems)
{
if (selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged += SelectedItemsCollectionChanged;
}
}
private void SubscribeToSelectionModel(ISelectionModel model)
{
model.PropertyChanged += SelectionModelPropertyChanged;
model.SelectionChanged += SelectionModelSelectionChanged;
model.SourceReset += SelectionModelSourceReset;
}
private void UnsubscribeFromSelectedItems(IList selectedItems)
{
if (selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= SelectedItemsCollectionChanged;
}
}
private void UnsubscribeFromSelectionModel(ISelectionModel model)
{
model.PropertyChanged -= SelectionModelPropertyChanged;
model.SelectionChanged -= SelectionModelSelectionChanged;
model.SourceReset -= SelectionModelSourceReset;
}
private static int IndexOf(object? source, object? item)
{
if (source is IList l)
{

189
src/Avalonia.Controls/Utils/SelectionTreeHelper.cs

@ -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<SelectionNode, IndexPath, int, int> nodeAction)
{
var node = root;
for (int depth = 0; depth < path.GetSize(); depth++)
{
int childIndex = path.GetAt(depth);
nodeAction(node, path, depth, childIndex);
if (depth < path.GetSize() - 1)
{
node = node.GetAt(childIndex, realizeChildren, path)!;
}
}
}
public static void Traverse(
SelectionNode root,
bool realizeChildren,
Action<TreeWalkNodeInfo> nodeAction)
{
var pendingNodes = new List<TreeWalkNodeInfo>();
var current = new IndexPath(null);
pendingNodes.Add(new TreeWalkNodeInfo(root, current));
while (pendingNodes.Count > 0)
{
var nextNode = pendingNodes.Last();
pendingNodes.RemoveAt(pendingNodes.Count - 1);
int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount;
for (int i = count - 1; i >= 0; i--)
{
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<TreeWalkNodeInfo> nodeAction)
{
var pendingNodes = new List<TreeWalkNodeInfo>();
var current = start;
// Build up the stack to account for the depth first walk up to the
// start index path.
TraverseIndexPath(
root,
start,
true,
(node, path, depth, childIndex) =>
{
var currentPath = StartPath(path, depth);
bool isStartPath = IsSubSet(start, currentPath);
bool isEndPath = IsSubSet(end, currentPath);
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0;
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : node.DataCount - 1;
for (int i = endIndex; i >= startIndex; i--)
{
var child = node.GetAt(i, 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<int>();
for (int i = 0; i < length; i++)
{
subPath.Add(path.GetAt(i));
}
return new IndexPath(subPath);
}
public struct TreeWalkNodeInfo
{
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode? parent)
{
node = node ?? throw new ArgumentNullException(nameof(node));
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; }
};
}
}

21
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<int>();
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();

15
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Selection;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
@ -14,24 +15,12 @@ namespace Avalonia.Diagnostics.ViewModels
{
MainView = mainView;
Nodes = nodes;
Selection = new SelectionModel
{
SingleSelect = true,
Source = Nodes
};
Selection.SelectionChanged += (s, e) =>
{
SelectedNode = (TreeNode)Selection.SelectedItem;
};
}
public MainViewModel MainView { get; }
public TreeNode[] Nodes { get; protected set; }
public SelectionModel Selection { get; }
public TreeNode SelectedNode
{
get => _selectedNode;
@ -106,8 +95,8 @@ namespace Avalonia.Diagnostics.ViewModels
if (node != null)
{
SelectedNode = node;
ExpandNode(node.Parent);
Selection.SelectedIndex = node.Index;
}
}

2
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@ -6,7 +6,7 @@
<TreeView Name="tree"
BorderThickness="0"
Items="{Binding Nodes}"
Selection="{Binding Selection}">
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode"
ItemsSource="{Binding Children}">

101
src/Avalonia.Themes.Default/TextBox.xaml

@ -1,5 +1,8 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<StreamGeometry x:Key="TextBoxClearButtonData">M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z</StreamGeometry>
<StreamGeometry x:Key="PasswordBoxRevealButtonData">m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z</StreamGeometry>
<StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
<ContextMenu x:Key="DefaultTextBoxContextMenu" x:Name="TextBoxContextMenu">
<MenuItem x:Name="TextBoxContextMenuCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextMenuCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
@ -88,7 +91,99 @@
<Style Selector="TextBox /template/ DockPanel">
<Setter Property="Cursor" Value="IBeam" />
</Style>
<Style Selector="TextBox:disabled /template/ Border#border">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
</Style>
<Style Selector="TextBox:disabled /template/ Border#border">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
</Style>
<Style Selector="TextBox.clearButton[AcceptsReturn=False][IsReadOnly=False]:focus:not(TextBox:empty)">
<Setter Property="InnerRightContent">
<Template>
<Button Classes="textBoxClearButton"
Command="{Binding $parent[TextBox].Clear}" />
</Template>
</Setter>
</Style>
<Style Selector="Button.textBoxClearButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="PART_ButtonLayoutBorder"
BorderThickness="{TemplateBinding BorderThickness}"
Background="Transparent"
Cursor="Arrow">
<Path x:Name="PART_GlyphElement"
Fill="{DynamicResource ThemeForegroundColor}"
Data="{StaticResource TextBoxClearButtonData}"
Height="10"
Width="10"
Stretch="Uniform"
VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style Selector="TextBox.revealPasswordButton[AcceptsReturn=False][IsReadOnly=False]:not(TextBox:empty)">
<Setter Property="InnerRightContent">
<Template>
<Panel Margin="0,0,4,0">
<Panel.Styles>
<Style Selector="ToggleButton[IsChecked=True]">
<Setter Property="(ToolTip.Tip)" Value="Hide Password" />
</Style>
<Style Selector="ToggleButton[IsChecked=False]">
<Setter Property="(ToolTip.Tip)" Value="Show Password" />
</Style>
</Panel.Styles>
<ToggleButton Classes="passwordBoxRevealButton" Background="Transparent" Cursor="Arrow" IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
</Panel>
</Template>
</Setter>
</Style>
<Style Selector="ToggleButton.passwordBoxRevealButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="PART_ButtonLayoutBorder"
Background="Transparent"
Width="12">
<Panel>
<Path x:Name="PART_GlyphElement_Reveal"
Fill="{DynamicResource ThemeForegroundColor}"
Data="{DynamicResource PasswordBoxRevealButtonData}"
Height="8"
Width="12"
Stretch="Uniform"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
<Path x:Name="PART_GlyphElement_Hide"
Fill="{DynamicResource ThemeForegroundColor}"
Data="{DynamicResource PasswordBoxHideButtonData}"
Height="12"
Width="12"
Stretch="Uniform"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Panel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style Selector="ToggleButton.passwordBoxRevealButton:not(ToggleButton:checked) /template/ Path#PART_GlyphElement_Hide">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ToggleButton.passwordBoxRevealButton:checked /template/ Path#PART_GlyphElement_Reveal">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="Button.textBoxClearButton, ToggleButton.passwordBoxRevealButton">
<Setter Property="Focusable" Value="False" />
</Style>
</Styles>

54
src/Avalonia.Themes.Fluent/TextBox.xaml

@ -170,7 +170,7 @@
<Style Selector="TextBox.revealPasswordButton[AcceptsReturn=False][IsReadOnly=False]:not(TextBox:empty)">
<Setter Property="InnerRightContent">
<Template>
<ToggleButton Classes="PasswordBoxRevealButton"
<ToggleButton Classes="passwordBoxRevealButton"
IsChecked="{Binding $parent[TextBox].RevealPassword, Mode=TwoWay}" />
</Template>
</Setter>
@ -179,13 +179,13 @@
<Style Selector="TextBox.clearButton[AcceptsReturn=False][IsReadOnly=False]:focus:not(TextBox:empty)">
<Setter Property="InnerRightContent">
<Template>
<Button Classes="TextBoxClearButton"
<Button Classes="textBoxClearButton"
Command="{Binding $parent[TextBox].Clear}" />
</Template>
</Setter>
</Style>
<Style Selector="ToggleButton.PasswordBoxRevealButton">
<Style Selector="ToggleButton.passwordBoxRevealButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
@ -213,7 +213,7 @@
</Setter>
</Style>
<Style Selector="Button.TextBoxClearButton">
<Style Selector="Button.textBoxClearButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
@ -233,60 +233,60 @@
</Style>
<!-- TextBox.Button Normal State -->
<Style Selector="Button.TextBoxClearButton, ToggleButton.PasswordBoxRevealButton">
<Style Selector="Button.textBoxClearButton, ToggleButton.passwordBoxRevealButton">
<Setter Property="MinWidth" Value="34" />
<Setter Property="Width" Value="{Binding $self.Bounds.Height}"/>
<Setter Property="Focusable" Value="False" />
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="Button.TextBoxClearButton /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.PasswordBoxRevealButton /template/ Border#PART_ButtonLayoutBorder">
<Style Selector="Button.textBoxClearButton /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.passwordBoxRevealButton /template/ Border#PART_ButtonLayoutBorder">
<Setter Property="Background" Value="{DynamicResource TextControlButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource TextControlButtonBorderBrush}" />
</Style>
<Style Selector="Button.TextBoxClearButton /template/ Path#PART_GlyphElement,
ToggleButton.PasswordBoxRevealButton /template/ Path#PART_GlyphElement_Reveal,
ToggleButton.PasswordBoxRevealButton /template/ Path#PART_GlyphElement_Hide">
<Style Selector="Button.textBoxClearButton /template/ Path#PART_GlyphElement,
ToggleButton.passwordBoxRevealButton /template/ Path#PART_GlyphElement_Reveal,
ToggleButton.passwordBoxRevealButton /template/ Path#PART_GlyphElement_Hide">
<Setter Property="Fill" Value="{DynamicResource TextControlButtonForeground}" />
</Style>
<!-- TextBox.Button PointerOver State -->
<Style Selector="Button.TextBoxClearButton:pointerover /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.PasswordBoxRevealButton:pointerover /template/ Border#PART_ButtonLayoutBorder">
<Style Selector="Button.textBoxClearButton:pointerover /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.passwordBoxRevealButton:pointerover /template/ Border#PART_ButtonLayoutBorder">
<Setter Property="Background" Value="{DynamicResource TextControlButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource TextControlButtonBorderBrushPointerOver}" />
</Style>
<Style Selector="Button.TextBoxClearButton:pointerover /template/ Path#PART_GlyphElement,
ToggleButton.PasswordBoxRevealButton:pointerover /template/ Path#PART_GlyphElement_Reveal,
ToggleButton.PasswordBoxRevealButton:pointerover /template/ Path#PART_GlyphElement_Hide">
<Style Selector="Button.textBoxClearButton:pointerover /template/ Path#PART_GlyphElement,
ToggleButton.passwordBoxRevealButton:pointerover /template/ Path#PART_GlyphElement_Reveal,
ToggleButton.passwordBoxRevealButton:pointerover /template/ Path#PART_GlyphElement_Hide">
<Setter Property="Fill" Value="{DynamicResource TextControlButtonForegroundPointerOver}" />
</Style>
<!-- TextBox.Button Pressed State -->
<Style Selector="Button.TextBoxClearButton:pressed /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.PasswordBoxRevealButton:pressed /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.PasswordBoxRevealButton:checked /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.PasswordBoxRevealButton:indeterminate /template/ Border#PART_ButtonLayoutBorder">
<Style Selector="Button.textBoxClearButton:pressed /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.passwordBoxRevealButton:pressed /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.passwordBoxRevealButton:checked /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.passwordBoxRevealButton:indeterminate /template/ Border#PART_ButtonLayoutBorder">
<Setter Property="Background" Value="{DynamicResource TextControlButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource TextControlButtonBorderBrushPressed}" />
</Style>
<Style Selector="Button.TextBoxClearButton:pressed /template/ Path#PART_GlyphElement,
ToggleButton.PasswordBoxRevealButton:pressed /template/ Path#PART_GlyphElement_Reveal,
ToggleButton.PasswordBoxRevealButton:checked /template/ Path#PART_GlyphElement_Hide,
ToggleButton.PasswordBoxRevealButton:indeterminate /template/ Path#PART_GlyphElement_Reveal">
<Style Selector="Button.textBoxClearButton:pressed /template/ Path#PART_GlyphElement,
ToggleButton.passwordBoxRevealButton:pressed /template/ Path#PART_GlyphElement_Reveal,
ToggleButton.passwordBoxRevealButton:checked /template/ Path#PART_GlyphElement_Hide,
ToggleButton.passwordBoxRevealButton:indeterminate /template/ Path#PART_GlyphElement_Reveal">
<Setter Property="Fill" Value="{DynamicResource TextControlButtonForegroundPressed}" />
</Style>
<!-- TextBox.Button Disabled State -->
<Style Selector="Button.TextBoxClearButton:disabled /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.PasswordBoxRevealButton:disabled /template/ Border#PART_ButtonLayoutBorder">
<Style Selector="Button.textBoxClearButton:disabled /template/ Border#PART_ButtonLayoutBorder,
ToggleButton.passwordBoxRevealButton:disabled /template/ Border#PART_ButtonLayoutBorder">
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="ToggleButton.PasswordBoxRevealButton:not(ToggleButton:checked) /template/ Path#PART_GlyphElement_Hide">
<Style Selector="ToggleButton.passwordBoxRevealButton:not(ToggleButton:checked) /template/ Path#PART_GlyphElement_Hide">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ToggleButton.PasswordBoxRevealButton:checked /template/ Path#PART_GlyphElement_Reveal">
<Style Selector="ToggleButton.passwordBoxRevealButton:checked /template/ Path#PART_GlyphElement_Reveal">
<Setter Property="IsVisible" Value="False" />
</Style>

6
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs

@ -31,10 +31,11 @@ namespace Avalonia.Skia
public GlGpuSession(GRContext grContext,
GRBackendRenderTarget backendRenderTarget,
SKSurface surface,
SKSurface surface,
IGlPlatformSurfaceRenderingSession glSession)
{
GrContext = grContext;
GrContext.PurgeResources();
_backendRenderTarget = backendRenderTarget;
_surface = surface;
_glSession = glSession;
@ -45,6 +46,7 @@ namespace Avalonia.Skia
_surface.Dispose();
_backendRenderTarget.Dispose();
GrContext.Flush();
GrContext.PurgeResources();
_glSession.Dispose();
}
@ -93,7 +95,7 @@ namespace Avalonia.Skia
}
finally
{
if(!success)
if (!success)
glSession.Dispose();
}
}

2
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@ -9,7 +9,6 @@ using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
class SystemDialogImpl : ISystemDialogImpl
{
private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE |
@ -113,6 +112,7 @@ namespace Avalonia.Win32
frm.GetOptions(out options);
options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | DefaultDialogOptions);
frm.SetOptions(options);
frm.SetTitle(dialog.Title ?? "");
if (dialog.Directory != null)
{

2
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@ -168,7 +168,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Selected_Index_Changes_To_When_Items_Assigned_Null()
public void Selected_Index_Changes_To_None_When_Items_Assigned_Null()
{
var items = new ObservableCollection<string>
{

39
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Platform;
@ -132,6 +133,44 @@ namespace Avalonia.Controls.UnitTests
popupImpl.Verify(x => x.Show(), Times.Exactly(2));
}
}
[Fact]
public void Context_Menu_Can_Be_Shared_Between_Controls_Even_After_A_Control_Is_Removed_From_Visual_Tree()
{
using (Application())
{
var sut = new ContextMenu();
var target1 = new Panel
{
ContextMenu = sut
};
var target2 = new Panel
{
ContextMenu = sut
};
var sp = new StackPanel { Children = { target1, target2 } };
var window = new Window { Content = sp };
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
_mouse.Click(target1, MouseButton.Right);
Assert.True(sut.IsOpen);
_mouse.Click(target2, MouseButton.Left);
Assert.False(sut.IsOpen);
sp.Children.Remove(target1);
_mouse.Click(target2, MouseButton.Right);
Assert.True(sut.IsOpen);
}
}
[Fact]
public void Cancelling_Opening_Does_Not_Show_ContextMenu()

95
tests/Avalonia.Controls.UnitTests/IndexPathTests.cs

@ -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);
}
}
}

389
tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs

@ -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<IndexRange>();
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(0, 4) };
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10) };
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(0, 4), new IndexRange(14, 16) };
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10) };
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10) };
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10) };
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14) };
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14) };
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10) };
var selected = new List<IndexRange>();
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<IndexRange> { new IndexRange(0, 10) };
var removed = new List<IndexRange>();
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<IndexRange> { new IndexRange(0, 10) };
var removed = new List<IndexRange>();
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<IndexRange> { new IndexRange(0, 5), new IndexRange(6, 10) };
var removed = new List<IndexRange>();
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<IndexRange> { new IndexRange(0, 5), new IndexRange(6, 10) };
var removed = new List<IndexRange>();
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<IndexRange>
{
new IndexRange(0, 2),
new IndexRange(3, 7),
new IndexRange(8, 10)
};
var removed = new List<IndexRange>();
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<IndexRange>
{
new IndexRange(0, 2),
new IndexRange(3, 7),
new IndexRange(8, 10)
};
var removed = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10) };
var deselected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 12) };
var deselected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 12) };
var deselected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 12) };
var deselected = new List<IndexRange>();
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<IndexRange> { new IndexRange(10, 20) };
var deselected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
var deselected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
var deselected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
var deselected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
var deselected = new List<IndexRange>();
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<IndexRange> { new IndexRange(8, 10) };
var deselected = new List<IndexRange>();
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<IndexRange>();
var expected = new List<int>();
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();
}
}
}
}

2
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -385,7 +385,7 @@ 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);
Assert.Equal(1, target.Selection.AnchorIndex);
// We're going to be clicking on item 9.
var item = (ListBoxItem)target.Presenter.Panel.Children[9];

187
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -7,6 +7,7 @@ using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
@ -19,7 +20,7 @@ using Xunit;
namespace Avalonia.Controls.UnitTests.Primitives
{
public class SelectingItemsControlTests
public partial class SelectingItemsControlTests
{
private MouseTestHelper _helper = new MouseTestHelper();
@ -56,7 +57,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Template = Template(),
};
target.ApplyTemplate();
Prepare(target);
Assert.False(items[0].IsSelected);
Assert.False(items[1].IsSelected);
@ -77,8 +78,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
target.SelectedItem = items[1];
Assert.False(items[0].IsSelected);
@ -101,8 +102,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
};
target.SelectedItem = items[1];
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
Assert.False(items[0].IsSelected);
Assert.True(items[1].IsSelected);
@ -159,6 +159,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Template = Template();
target.EndInit();
Prepare(target);
Assert.Equal(0, target.SelectedIndex);
}
@ -181,11 +183,13 @@ namespace Avalonia.Controls.UnitTests.Primitives
listBox.EndInit();
Prepare(listBox);
Assert.Equal("B", listBox.SelectedItem);
}
[Fact]
public void Setting_SelectedIndex_Before_Initialize_Should_Retain()
public void Setting_SelectedIndex_Before_Initialize_Should_Retain_Selection()
{
var listBox = new ListBox
{
@ -223,7 +227,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
public void Setting_SelectedItem_Before_Initialize_Should_Retain()
public void Setting_SelectedItem_Before_Initialize_Should_Retain_Selection()
{
var listBox = new ListBox
{
@ -242,7 +246,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
[Fact]
public void Setting_SelectedItems_Before_Initialize_Should_Retain()
public void Setting_SelectedItems_Before_Initialize_Should_Retain_Selection()
{
var listBox = new ListBox
{
@ -290,7 +294,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain()
public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain_Selection()
{
var listBox = new ListBox
{
@ -324,8 +328,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
};
target.SelectedIndex = 1;
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
Assert.False(items[0].IsSelected);
Assert.True(items[1].IsSelected);
@ -480,8 +483,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
items.Add(new Item { IsSelected = true });
Assert.Equal(2, target.SelectedIndex);
@ -530,8 +532,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
target.SelectedIndex = 1;
Assert.Equal(items[1], target.SelectedItem);
@ -568,8 +569,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Template = Template();
target.EndInit();
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
target.SelectedIndex = 0;
Assert.Equal(items[0], target.SelectedItem);
@ -635,8 +635,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
target.SelectedItem = items[1];
Assert.False(items[0].IsSelected);
@ -666,8 +665,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
target.SelectedItem = items[1];
Assert.False(items[0].IsSelected);
@ -757,8 +755,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectedIndex = 1,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
var called = false;
@ -785,6 +782,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Items = items;
((ISupportInitialize)target).EndInit();
Prepare(target);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
}
@ -800,6 +799,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Items = items;
((ISupportInitialize)target).EndInit();
Prepare(target);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
}
@ -897,8 +898,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Items = new[] { "Foo", "Bar", "Baz " },
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
_helper.Down((Interactive)target.Presenter.Panel.Children[1]);
var panel = target.Presenter.Panel;
@ -919,8 +919,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Items = items,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
_helper.Down(target.Presenter.Panel.Children[1]);
@ -1014,8 +1013,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
_helper.Down((Interactive)target.Presenter.Panel.Children[3]);
Assert.Equal(3, target.SelectedIndex);
@ -1030,8 +1028,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
_helper.Down((Interactive)target.Presenter.Panel.Children[3]);
Assert.Equal(new[] { ":pressed", ":selected" }, target.Presenter.Panel.Children[3].Classes);
@ -1054,8 +1051,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectedIndex = 1,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
items.Insert(0, "Qux");
@ -1080,8 +1076,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectedIndex = 1,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
items.RemoveAt(0);
@ -1089,6 +1084,65 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal("Bar", target.SelectedItem);
}
[Fact]
public void Binding_SelectedIndex_Selects_Correct_Item()
{
// Issue #4496 (part 2)
var items = new ObservableCollection<string>();
var other = new ListBox
{
Template = Template(),
Items = items,
SelectionMode = SelectionMode.AlwaysSelected,
};
var target = new ListBox
{
Template = Template(),
Items = items,
[!ListBox.SelectedIndexProperty] = other[!ListBox.SelectedIndexProperty],
};
Prepare(other);
Prepare(target);
items.Add("Foo");
Assert.Equal(0, other.SelectedIndex);
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Binding_SelectedItem_Selects_Correct_Item()
{
// Issue #4496 (part 2)
var items = new ObservableCollection<string>();
var other = new ListBox
{
Template = Template(),
Items = items,
SelectionMode = SelectionMode.AlwaysSelected,
};
var target = new ListBox
{
Template = Template(),
Items = items,
[!ListBox.SelectedItemProperty] = other[!ListBox.SelectedItemProperty],
};
Prepare(target);
other.ApplyTemplate();
other.Presenter.ApplyTemplate();
items.Add("Foo");
Assert.Equal(0, other.SelectedIndex);
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Replacing_Selected_Item_Should_Update_SelectedItem()
{
@ -1106,8 +1160,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectedIndex = 1,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
items[1] = "Qux";
@ -1131,8 +1184,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Items = items,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
var raised = false;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
@ -1195,6 +1247,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.SelectedItem = "Bar";
target.EndInit();
Prepare(target);
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
Assert.Same(selectedItems, target.SelectedItems);
@ -1261,16 +1315,49 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Items = items;
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Prepare(target);
Assert.Equal(second, target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
}
private FuncControlTemplate Template()
[Fact]
public void Setting_SelectionMode_Should_Update_SelectionModel()
{
var target = new TestSelector();
var model = target.Selection;
Assert.True(model.SingleSelect);
target.SelectionMode = SelectionMode.Multiple;
Assert.False(model.SingleSelect);
}
private static void Prepare(SelectingItemsControl target)
{
var root = new TestRoot
{
Child = target,
Width = 100,
Height = 100,
Styles =
{
new Style(x => x.Is<SelectingItemsControl>())
{
Setters =
{
new Setter(ListBox.TemplateProperty, Template()),
},
},
},
};
root.LayoutManager.ExecuteInitialLayoutPass();
}
private static FuncControlTemplate Template()
{
return new FuncControlTemplate<SelectingItemsControl>((control, scope) =>
new ItemsPresenter
@ -1328,6 +1415,18 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectionMode = selectionMode;
}
public new ISelectionModel Selection
{
get => base.Selection;
set => base.Selection = value;
}
public new SelectionMode SelectionMode
{
get => base.SelectionMode;
set => base.SelectionMode = value;
}
public new bool MoveSelection(NavigationDirection direction, bool wrap)
{
return base.MoveSelection(direction, wrap);

73
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -6,6 +6,7 @@ using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
@ -367,7 +368,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.SelectedIndex = 3;
target.SelectRange(1);
Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems.Cast<object>().ToList());
Assert.Equal(new[] { "qux", "bar", "baz" }, target.SelectedItems.Cast<object>().ToList());
}
[Fact]
@ -516,7 +517,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
/// DataContext is in the process of changing.
/// </remarks>
[Fact]
public void Should_Not_Write_To_Old_DataContext()
public void Should_Not_Write_SelectedItems_To_Old_DataContext()
{
var vm = new OldDataContextViewModel();
var target = new TestSelector();
@ -552,6 +553,46 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Empty(target.SelectedItems);
}
/// <summary>
/// See <see cref="Should_Not_Write_SelectedItems_To_Old_DataContext"/>.
/// </summary>
[Fact]
public void Should_Not_Write_SelectionModel_To_Old_DataContext()
{
var vm = new OldDataContextViewModel();
var target = new TestSelector();
var itemsBinding = new Binding
{
Path = "Items",
Mode = BindingMode.OneWay,
};
var selectionBinding = new Binding
{
Path = "Selection",
Mode = BindingMode.OneWay,
};
// Bind Items and Selection to the VM.
target.Bind(TestSelector.ItemsProperty, itemsBinding);
target.Bind(TestSelector.SelectionProperty, selectionBinding);
// Set DataContext and SelectedIndex
target.DataContext = vm;
target.SelectedIndex = 1;
// Make sure selection is written to selection model
Assert.Equal(1, vm.Selection.SelectedIndex);
// Clear DataContext and ensure that selection is still set in model.
target.DataContext = null;
Assert.Equal(1, vm.Selection.SelectedIndex);
// Ensure target's SelectedItems is now clear.
Assert.Empty(target.SelectedItems);
}
[Fact]
public void Unbound_SelectedItems_Should_Be_Cleared_When_DataContext_Cleared()
{
@ -1259,7 +1300,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
};
target.ApplyTemplate();
target.Selection.Select(1);
target.SelectedItems.Add("bar");
Assert.Equal(1, target.SelectedIndex);
}
@ -1290,7 +1331,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Template = Template(),
};
var selection = new SelectionModel { Source = new[] { "baz" } };
var selection = new SelectionModel<string> { Source = new[] { "baz" } };
Assert.Throws<ArgumentException>(() => target.Selection = selection);
}
@ -1303,7 +1344,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Template = Template(),
};
var selection = new SelectionModel();
var selection = new SelectionModel<string>();
target.Selection = selection;
Assert.Same(target.Items, selection.Source);
@ -1321,7 +1362,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var selection = new SelectionModel { Source = target.Items };
var selection = new SelectionModel<string> { SingleSelect = false };
selection.Select(1);
target.Selection = selection;
@ -1342,8 +1383,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var selection = new SelectionModel { Source = target.Items };
selection.SelectRange(new IndexPath(0), new IndexPath(2));
var selection = new SelectionModel<string> { SingleSelect = false };
selection.SelectRange(0, 2);
target.Selection = selection;
Assert.Equal(0, target.SelectedIndex);
@ -1362,7 +1403,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.ApplyTemplate();
target.Selection.Select(1);
target.Selection = new SelectionModel();
target.Selection = new SelectionModel<string>();
Assert.Equal(-1, target.SelectedIndex);
Assert.Null(target.SelectedItem);
@ -1387,8 +1428,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var selection = new SelectionModel { Source = items };
selection.SelectRange(new IndexPath(0), new IndexPath(1));
var selection = new SelectionModel<object> { SingleSelect = false };
selection.SelectRange(0, 1);
target.Selection = selection;
Assert.True(items[0].IsSelected);
@ -1429,15 +1470,13 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var selection = new SelectionModel { Source = items };
var selection = new SelectionModel<string> { Source = items, SingleSelect = false };
selection.Select(0);
selection.Select(2);
target.Selection = selection;
Assert.Equal(2, raised);
}
private IEnumerable<int> SelectedContainers(SelectingItemsControl target)
{
return target.Presenter.Panel.Children
@ -1460,6 +1499,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
public static readonly new AvaloniaProperty<IList> SelectedItemsProperty =
SelectingItemsControl.SelectedItemsProperty;
public static readonly new DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
SelectingItemsControl.SelectionProperty;
public TestSelector()
{
@ -1485,7 +1526,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
public void SelectAll() => Selection.SelectAll();
public void UnselectAll() => Selection.ClearSelection();
public void UnselectAll() => Selection.Clear();
public void SelectRange(int index) => UpdateSelection(index, true, true);
public void Toggle(int index) => UpdateSelection(index, true, false, true);
}
@ -1496,10 +1537,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Items = new List<string> { "foo", "bar" };
SelectedItems = new List<string>();
Selection = new SelectionModel<string>();
}
public List<string> Items { get; }
public List<string> SelectedItems { get; }
public SelectionModel<string> Selection { get; }
}
private class ItemContainer : Control, ISelectable

1584
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs

File diff suppressed because it is too large

1210
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

File diff suppressed because it is too large

2322
tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

File diff suppressed because it is too large

16
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<Node>());
Assert.Equal(new[] { item1, item2 }, target.SelectedItems.OfType<Node>());
ClickContainer(item1Container, KeyModifiers.Control);
Assert.False(item1Container.IsSelected);
Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType<Node>());
Assert.DoesNotContain(item1, target.SelectedItems.OfType<Node>());
}
}
@ -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);
}
}

131
tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

@ -1,7 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;
using Xunit;
@ -13,7 +14,7 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Initial_Items_Are_From_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
Assert.Equal(new[] { "bar", "baz" }, items);
}
@ -22,9 +23,9 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Selecting_On_Model_Adds_Item()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
target.Model.Select(0);
target.SelectionModel.Select(0);
Assert.Equal(new[] { "bar", "baz", "foo" }, items);
}
@ -33,9 +34,9 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Selecting_Duplicate_On_Model_Adds_Item()
{
var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
target.Model.Select(4);
target.SelectionModel.Select(4);
Assert.Equal(new[] { "bar", "baz", "bar" }, items);
}
@ -44,9 +45,9 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Deselecting_On_Model_Removes_Item()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
target.Model.Deselect(1);
target.SelectionModel.Deselect(1);
Assert.Equal(new[] { "baz" }, items);
}
@ -55,10 +56,10 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Deselecting_Duplicate_On_Model_Removes_Item()
{
var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
target.Model.Select(4);
target.Model.Deselect(4);
target.SelectionModel.Select(4);
target.SelectionModel.Deselect(4);
Assert.Equal(new[] { "baz", "bar" }, items);
}
@ -67,13 +68,18 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Reassigning_Model_Resets_Items()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
var newModel = new SelectionModel<string>
{
Source = (string[])target.SelectionModel.Source,
SingleSelect = false
};
var newModel = new SelectionModel { Source = target.Model.Source };
newModel.Select(0);
newModel.Select(1);
target.SetModel(newModel);
target.SelectionModel = newModel;
Assert.Equal(new[] { "foo", "bar" }, items);
}
@ -82,10 +88,15 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Reassigning_Model_Tracks_New_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
var newModel = new SelectionModel<string>
{
Source = (string[])target.SelectionModel.Source,
SingleSelect = false
};
var newModel = new SelectionModel { Source = target.Model.Source };
target.SetModel(newModel);
target.SelectionModel = newModel;
newModel.Select(0);
newModel.Select(1);
@ -97,13 +108,11 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Adding_To_Items_Selects_On_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
items.Add("foo");
Assert.Equal(
new[] { new IndexPath(0), new IndexPath(1), new IndexPath(2) },
target.Model.SelectedIndices);
Assert.Equal(new[] { 0, 1, 2 }, target.SelectionModel.SelectedIndexes);
Assert.Equal(new[] { "bar", "baz", "foo" }, items);
}
@ -111,11 +120,11 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Removing_From_Items_Deselects_On_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
items.Remove("baz");
Assert.Equal(new[] { new IndexPath(1) }, target.Model.SelectedIndices);
Assert.Equal(new[] { 1 }, target.SelectionModel.SelectedIndexes);
Assert.Equal(new[] { "bar" }, items);
}
@ -123,11 +132,11 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Replacing_Item_Updates_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
items[0] = "foo";
Assert.Equal(new[] { new IndexPath(0), new IndexPath(2) }, target.Model.SelectedIndices);
Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes);
Assert.Equal(new[] { "foo", "baz" }, items);
}
@ -135,25 +144,25 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Clearing_Items_Updates_Model()
{
var target = CreateTarget();
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
items.Clear();
Assert.Empty(target.Model.SelectedIndices);
Assert.Empty(target.SelectionModel.SelectedIndexes);
}
[Fact]
public void Setting_Items_Updates_Model()
{
var target = CreateTarget();
var oldItems = target.GetOrCreateItems();
var oldItems = target.SelectedItems;
var newItems = new AvaloniaList<string> { "foo", "baz" };
target.SetItems(newItems);
target.SelectedItems = 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[] { 0, 2 }, target.SelectionModel.SelectedIndexes);
Assert.Same(newItems, target.SelectedItems);
Assert.NotSame(oldItems, target.SelectedItems);
Assert.Equal(new[] { "foo", "baz" }, newItems);
}
@ -163,8 +172,8 @@ namespace Avalonia.Controls.UnitTests.Utils
var target = CreateTarget();
var items = new AvaloniaList<string> { "foo", "baz" };
target.SetItems(items);
target.Model.Select(1);
target.SelectedItems = items;
target.SelectionModel.Select(1);
Assert.Equal(new[] { "foo", "baz", "bar" }, items);
}
@ -173,11 +182,11 @@ namespace Avalonia.Controls.UnitTests.Utils
public void Setting_Items_To_Null_Creates_Empty_Items()
{
var target = CreateTarget();
var oldItems = target.GetOrCreateItems();
var oldItems = target.SelectedItems;
target.SetItems(null);
target.SelectedItems = null;
var newItems = Assert.IsType<AvaloniaList<object>>(target.GetOrCreateItems());
var newItems = Assert.IsType<AvaloniaList<object>>(target.SelectedItems);
Assert.NotSame(oldItems, newItems);
}
@ -185,11 +194,11 @@ namespace Avalonia.Controls.UnitTests.Utils
[Fact]
public void Handles_Null_Model_Source()
{
var model = new SelectionModel();
var model = new SelectionModel<string> { SingleSelect = false };
model.Select(1);
var target = new SelectedItemsSync(model);
var items = target.GetOrCreateItems();
var items = target.SelectedItems;
Assert.Empty(items);
@ -205,21 +214,34 @@ namespace Avalonia.Controls.UnitTests.Utils
var target = CreateTarget();
Assert.Throws<NotSupportedException>(() =>
target.SetItems(new[] { "foo", "bar", "baz" }));
target.SelectedItems = new[] { "foo", "bar", "baz" });
}
[Fact]
public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source()
{
var model = new SelectionModel();
var model = new SelectionModel<string>();
var target = new SelectedItemsSync(model);
var items = new AvaloniaList<string> { "foo", "bar", "baz" };
var selectedItems = new AvaloniaList<string> { "bar" };
target.SetItems(selectedItems);
target.SelectedItems = selectedItems;
model.Source = items;
Assert.Equal(new IndexPath(1), model.SelectedIndex);
Assert.Equal(1, model.SelectedIndex);
}
[Fact]
public void Restores_Selection_On_Items_Reset()
{
var items = new ResettingCollection(new[] { "foo", "bar", "baz" });
var model = new SelectionModel<string> { Source = items };
var target = new SelectedItemsSync(model);
model.SelectedIndex = 1;
items.Reset(new[] { "baz", "foo", "bar" });
Assert.Equal(2, model.SelectedIndex);
}
private static SelectedItemsSync CreateTarget(
@ -227,11 +249,30 @@ namespace Avalonia.Controls.UnitTests.Utils
{
items ??= new[] { "foo", "bar", "baz" };
var model = new SelectionModel { Source = items };
model.SelectRange(new IndexPath(1), new IndexPath(2));
var model = new SelectionModel<string> { Source = items, SingleSelect = false };
model.SelectRange(1, 2);
var target = new SelectedItemsSync(model);
return target;
}
private class ResettingCollection : List<string>, INotifyCollectionChanged
{
public ResettingCollection(IEnumerable<string> items)
{
AddRange(items);
}
public void Reset(IEnumerable<string> items)
{
Clear();
AddRange(items);
CollectionChanged?.Invoke(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
}
}

Loading…
Cancel
Save