Browse Source

Merge branch 'feature/selectionmodel-changed-notifications' into feature/selectionmodel-reset-handling

pull/3498/head
Steven Kirk 7 years ago
parent
commit
ca324775e3
  1. 7
      src/Avalonia.Controls/IndexPath.cs
  2. 20
      src/Avalonia.Controls/SelectionModel.cs
  3. 75
      src/Avalonia.Controls/SelectionModelChangeSet.cs
  4. 12
      src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs
  5. 7
      src/Avalonia.Controls/SelectionNode.cs
  6. 18
      src/Avalonia.Controls/Utils/SelectionTreeHelper.cs
  7. 14
      tests/Avalonia.Controls.UnitTests/IndexPathTests.cs
  8. 75
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

7
src/Avalonia.Controls/IndexPath.cs

@ -58,6 +58,11 @@ namespace Avalonia.Controls
public int GetAt(int index) public int GetAt(int index)
{ {
if (index >= GetSize())
{
throw new IndexOutOfRangeException();
}
return _path?[index] ?? (_index - 1); return _path?[index] ?? (_index - 1);
} }
@ -169,5 +174,7 @@ namespace Avalonia.Controls
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; }
} }
} }

20
src/Avalonia.Controls/SelectionModel.cs

@ -36,13 +36,29 @@ namespace Avalonia.Controls
get => _rootNode?.Source; get => _rootNode?.Source;
set set
{ {
using (var operation = new Operation(this)) var wasNull = _rootNode.Source == null;
if (_rootNode.Source != null)
{ {
ClearSelection(resetAnchor: true); using (var operation = new Operation(this))
{
ClearSelection(resetAnchor: true);
}
} }
_rootNode.Source = value; _rootNode.Source = value;
RaisePropertyChanged("Source"); RaisePropertyChanged("Source");
if (wasNull)
{
var e = new SelectionModelSelectionChangedEventArgs(
null,
SelectedIndices,
null,
SelectedItems);
OnSelectionChanged(e);
}
} }
} }

75
src/Avalonia.Controls/SelectionModelChangeSet.cs

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
internal class SelectionModelChangeSet internal class SelectionModelChangeSet
@ -14,30 +16,38 @@ namespace Avalonia.Controls
public SelectionModelSelectionChangedEventArgs CreateEventArgs() public SelectionModelSelectionChangedEventArgs CreateEventArgs()
{ {
var deselectedCount = 0; var deselectedIndexCount = 0;
var selectedCount = 0; var selectedIndexCount = 0;
var deselectedItemCount = 0;
var selectedItemCount = 0;
foreach (var change in _changes) foreach (var change in _changes)
{ {
deselectedCount += change.DeselectedCount; deselectedIndexCount += change.DeselectedCount;
selectedCount += change.SelectedCount; selectedIndexCount += change.SelectedCount;
if (change.Items != null)
{
deselectedItemCount += change.DeselectedCount;
selectedItemCount += change.SelectedCount;
}
} }
var deselectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>( var deselectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
_changes, _changes,
deselectedCount, deselectedIndexCount,
GetDeselectedIndexAt); GetDeselectedIndexAt);
var selectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>( var selectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
_changes, _changes,
selectedCount, selectedIndexCount,
GetSelectedIndexAt); GetSelectedIndexAt);
var deselectedItems = new SelectedItems<object, SelectionNodeOperation>( var deselectedItems = new SelectedItems<object?, SelectionNodeOperation>(
_changes, _changes,
deselectedCount, deselectedItemCount,
GetDeselectedItemAt); GetDeselectedItemAt);
var selectedItems = new SelectedItems<object, SelectionNodeOperation>( var selectedItems = new SelectedItems<object?, SelectionNodeOperation>(
_changes, _changes,
selectedCount, selectedItemCount,
GetSelectedItemAt); GetSelectedItemAt);
return new SelectionModelSelectionChangedEventArgs( return new SelectionModelSelectionChangedEventArgs(
@ -52,7 +62,7 @@ namespace Avalonia.Controls
int index) int index)
{ {
static int GetCount(SelectionNodeOperation info) => info.DeselectedCount; static int GetCount(SelectionNodeOperation info) => info.DeselectedCount;
static List<IndexRange> GetRanges(SelectionNodeOperation info) => info.DeselectedRanges; static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
return GetIndexAt(infos, index, GetCount, GetRanges); return GetIndexAt(infos, index, GetCount, GetRanges);
} }
@ -61,25 +71,25 @@ namespace Avalonia.Controls
int index) int index)
{ {
static int GetCount(SelectionNodeOperation info) => info.SelectedCount; static int GetCount(SelectionNodeOperation info) => info.SelectedCount;
static List<IndexRange> GetRanges(SelectionNodeOperation info) => info.SelectedRanges; static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
return GetIndexAt(infos, index, GetCount, GetRanges); return GetIndexAt(infos, index, GetCount, GetRanges);
} }
private object GetDeselectedItemAt( private object? GetDeselectedItemAt(
List<SelectionNodeOperation> infos, List<SelectionNodeOperation> infos,
int index) int index)
{ {
static int GetCount(SelectionNodeOperation info) => info.DeselectedCount; static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.DeselectedCount : 0;
static List<IndexRange> GetRanges(SelectionNodeOperation info) => info.DeselectedRanges; static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
return GetItemAt(infos, index, GetCount, GetRanges); return GetItemAt(infos, index, GetCount, GetRanges);
} }
private object GetSelectedItemAt( private object? GetSelectedItemAt(
List<SelectionNodeOperation> infos, List<SelectionNodeOperation> infos,
int index) int index)
{ {
static int GetCount(SelectionNodeOperation info) => info.SelectedCount; static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.SelectedCount : 0;
static List<IndexRange> GetRanges(SelectionNodeOperation info) => info.SelectedRanges; static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
return GetItemAt(infos, index, GetCount, GetRanges); return GetItemAt(infos, index, GetCount, GetRanges);
} }
@ -87,7 +97,7 @@ namespace Avalonia.Controls
List<SelectionNodeOperation> infos, List<SelectionNodeOperation> infos,
int index, int index,
Func<SelectionNodeOperation, int> getCount, Func<SelectionNodeOperation, int> getCount,
Func<SelectionNodeOperation, List<IndexRange>> getRanges) Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
{ {
var currentIndex = 0; var currentIndex = 0;
IndexPath path = default; IndexPath path = default;
@ -109,14 +119,14 @@ namespace Avalonia.Controls
return path; return path;
} }
private object GetItemAt( private object? GetItemAt(
List<SelectionNodeOperation> infos, List<SelectionNodeOperation> infos,
int index, int index,
Func<SelectionNodeOperation, int> getCount, Func<SelectionNodeOperation, int> getCount,
Func<SelectionNodeOperation, List<IndexRange>> getRanges) Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
{ {
var currentIndex = 0; var currentIndex = 0;
object item = null; object? item = null;
foreach (var info in infos) foreach (var info in infos)
{ {
@ -125,7 +135,7 @@ namespace Avalonia.Controls
if (index >= currentIndex && index < currentIndex + currentCount) if (index >= currentIndex && index < currentIndex + currentCount)
{ {
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex); int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
item = info.Items.GetAt(targetIndex); item = info.Items?.GetAt(targetIndex);
break; break;
} }
@ -135,20 +145,23 @@ namespace Avalonia.Controls
return item; return item;
} }
private int GetIndexAt(List<IndexRange> ranges, int index) private int GetIndexAt(List<IndexRange>? ranges, int index)
{ {
var currentIndex = 0; var currentIndex = 0;
foreach (var range in ranges) if (ranges != null)
{ {
var currentCount = (range.End - range.Begin) + 1; foreach (var range in ranges)
if (index >= currentIndex && index < currentIndex + currentCount)
{ {
return range.Begin + (index - currentIndex); var currentCount = (range.End - range.Begin) + 1;
}
currentIndex += currentCount; if (index >= currentIndex && index < currentIndex + currentCount)
{
return range.Begin + (index - currentIndex);
}
currentIndex += currentCount;
}
} }
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();

12
src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs

@ -15,13 +15,13 @@ namespace Avalonia.Controls
public SelectionModelSelectionChangedEventArgs( public SelectionModelSelectionChangedEventArgs(
IReadOnlyList<IndexPath>? deselectedIndices, IReadOnlyList<IndexPath>? deselectedIndices,
IReadOnlyList<IndexPath>? selectedIndices, IReadOnlyList<IndexPath>? selectedIndices,
IReadOnlyList<object>? deselectedItems, IReadOnlyList<object?>? deselectedItems,
IReadOnlyList<object>? selectedItems) IReadOnlyList<object?>? selectedItems)
{ {
DeselectedIndices = deselectedIndices ?? Array.Empty<IndexPath>(); DeselectedIndices = deselectedIndices ?? Array.Empty<IndexPath>();
SelectedIndices = selectedIndices ?? Array.Empty<IndexPath>(); SelectedIndices = selectedIndices ?? Array.Empty<IndexPath>();
DeselectedItems = deselectedItems ?? Array.Empty<object>(); DeselectedItems = deselectedItems ?? Array.Empty<object?>();
SelectedItems= selectedItems ?? Array.Empty<object>(); SelectedItems= selectedItems ?? Array.Empty<object?>();
} }
/// <summary> /// <summary>
@ -37,11 +37,11 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets the items that were removed from the selection. /// Gets the items that were removed from the selection.
/// </summary> /// </summary>
public IReadOnlyList<object> DeselectedItems { get; } public IReadOnlyList<object?> DeselectedItems { get; }
/// <summary> /// <summary>
/// Gets the items that were added to the selection. /// Gets the items that were added to the selection.
/// </summary> /// </summary>
public IReadOnlyList<object> SelectedItems { get; } public IReadOnlyList<object?> SelectedItems { get; }
} }
} }

7
src/Avalonia.Controls/SelectionNode.cs

@ -84,8 +84,11 @@ namespace Avalonia.Controls
{ {
if (_source != value) if (_source != value)
{ {
ClearSelection(); if (_source != null)
UnhookCollectionChangedHandler(); {
ClearSelection();
UnhookCollectionChangedHandler();
}
_source = value; _source = value;

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

@ -132,15 +132,21 @@ namespace Avalonia.Controls.Utils
private static bool IsSubSet(IndexPath path, IndexPath subset) private static bool IsSubSet(IndexPath path, IndexPath subset)
{ {
bool isSubset = true; var subsetSize = subset.GetSize();
for (int i = 0; i < subset.GetSize(); i++) if (path.GetSize() < subsetSize)
{ {
isSubset = path.GetAt(i) == subset.GetAt(i); return false;
if (!isSubset) }
break;
for (int i = 0; i < subsetSize; i++)
{
if (path.GetAt(i) != subset.GetAt(i))
{
return false;
}
} }
return isSubset; return true;
} }
private static IndexPath StartPath(IndexPath path, int length) private static IndexPath StartPath(IndexPath path, int length)

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

@ -77,5 +77,19 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, a.CompareTo(b)); Assert.Equal(0, a.CompareTo(b));
Assert.Equal(a.GetHashCode(), b.GetHashCode()); 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);
}
} }
} }

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

@ -899,6 +899,38 @@ namespace Avalonia.Controls.UnitTests
}); });
} }
[Fact]
public void SelectRangeRegressionTest()
{
RunOnUIThread.Execute(() =>
{
var selectionModel = new SelectionModel()
{
Source = CreateNestedData(1, 2, 3)
};
// length of start smaller than end used to cause an out of range error.
selectionModel.SelectRange(IndexPath.CreateFrom(0), IndexPath.CreateFrom(1, 1));
ValidateSelection(selectionModel,
new List<IndexPath>()
{
Path(0, 0),
Path(0, 1),
Path(0, 2),
Path(0),
Path(1, 0),
Path(1, 1)
},
new List<IndexPath>()
{
Path(),
Path(1)
},
1 /* selectedInnerNodes */);
});
}
[Fact] [Fact]
public void Selecting_Item_Raises_SelectionChanged() public void Selecting_Item_Raises_SelectionChanged()
{ {
@ -1393,6 +1425,48 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(2, raised); Assert.Equal(2, raised);
} }
[Fact]
public void Raises_SelectionChanged_With_No_Source()
{
var target = new SelectionModel();
var raised = 0;
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices);
Assert.Empty(e.SelectedItems);
++raised;
};
target.Select(1);
Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices);
Assert.Empty(target.SelectedItems);
}
[Fact]
public void Raises_SelectionChanged_With_Items_After_Source_Is_Set()
{
var target = new SelectionModel();
var raised = 0;
target.Select(1);
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices);
Assert.Equal(new[] { "bar" }, e.SelectedItems);
++raised;
};
target.Source = new[] { "foo", "bar", "baz" };
Assert.Equal(1, raised);
}
[Fact] [Fact]
public void RetainSelectionOnReset_Retains_Selection_On_Reset() public void RetainSelectionOnReset_Retains_Selection_On_Reset()
{ {
@ -1494,6 +1568,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, raised); Assert.Equal(1, raised);
} }
private int GetSubscriberCount(AvaloniaList<object> list) private int GetSubscriberCount(AvaloniaList<object> list)
{ {
return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0;

Loading…
Cancel
Save