Browse Source

Refactored SelectionModel.

`SelectionModel` as ported from WinUI is unsuitable for use with `TreeView` because it treats a node as being selected when all its child nodes are selected, which is fine for a tree of check boxes, but incorrect for basic `TreeView` selection.

Because fixing this requires moving away from the semantics/API of WinUI's `SelectionModel` I've to also simplify/sanitize the API a bit:

- What was previously `IsSelected` is now called `IsTreeSelected`
- Removed the `groupIndex`/`itemIndex` overloads; they extraneous from my POV and could be implemented on extension methods
- Removed the "At" prefix on some methods that take an `IndexPath`; I have no idea why that was there.
wip/use-selectionmodel
Steven Kirk 6 years ago
parent
commit
9b77a737e4
  1. 20
      src/Avalonia.Controls/ISelectionModel.cs
  2. 131
      src/Avalonia.Controls/SelectionModel.cs
  3. 52
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

20
src/Avalonia.Controls/ISelectionModel.cs

@ -25,26 +25,20 @@ namespace Avalonia.Controls
void ClearSelection();
void Deselect(int index);
void Deselect(int groupIndex, int itemIndex);
void DeselectAt(IndexPath index);
void Deselect(IndexPath index);
void DeselectRange(IndexPath start, IndexPath end);
void DeselectRangeFromAnchor(int index);
void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex);
void DeselectRangeFromAnchorTo(IndexPath index);
void DeselectRangeFromAnchor(IndexPath index);
void Dispose();
bool? IsSelected(int index);
bool? IsSelected(int groupIndex, int itemIndex);
bool? IsSelectedAt(IndexPath index);
bool IsSelected(int index);
bool IsSelected(IndexPath index);
bool? IsTreeSelected(IndexPath index);
void Select(int index);
void Select(int groupIndex, int itemIndex);
void Select(IndexPath index);
void SelectAll();
void SelectAt(IndexPath index);
void SelectRange(IndexPath start, IndexPath end);
void SelectRangeFromAnchor(int index);
void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex);
void SelectRangeFromAnchorTo(IndexPath index);
void SetAnchorIndex(int index);
void SetAnchorIndex(int groupIndex, int index);
void SelectRangeFromAnchor(IndexPath index);
IDisposable Update();
}
}

131
src/Avalonia.Controls/SelectionModel.cs

@ -174,9 +174,7 @@ namespace Avalonia.Controls
}
set
{
var isSelected = IsSelectedAt(value);
if (isSelected != true || SelectedItems.Count > 1)
if (!IsSelected(value) || SelectedItems.Count > 1)
{
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
@ -351,23 +349,13 @@ namespace Avalonia.Controls
_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)
public void Select(IndexPath index)
{
using var operation = new Operation(this);
SelectWithPathImpl(index, select: true);
@ -380,55 +368,35 @@ namespace Avalonia.Controls
ApplyAutoSelect();
}
public void Deselect(int groupIndex, int itemIndex)
{
using var operation = new Operation(this);
SelectWithGroupImpl(groupIndex, itemIndex, select: false);
ApplyAutoSelect();
}
public void DeselectAt(IndexPath index)
public void Deselect(IndexPath index)
{
using var operation = new Operation(this);
SelectWithPathImpl(index, select: false);
ApplyAutoSelect();
}
public bool? IsSelected(int index)
{
if (index < 0)
{
throw new ArgumentException("Index must be >= 0", nameof(index));
}
public bool IsSelected(int index) => _rootNode.IsSelected(index);
var isSelected = _rootNode.IsSelectedWithPartial(index);
return isSelected;
}
public bool? IsSelected(int groupIndex, int itemIndex)
public bool IsSelected(IndexPath index)
{
if (groupIndex < 0)
{
throw new ArgumentException("Group index must be >= 0", nameof(groupIndex));
}
var path = index;
SelectionNode? node = _rootNode;
if (itemIndex < 0)
for (int i = 0; i < path.GetSize() - 1; i++)
{
throw new ArgumentException("Item index must be >= 0", nameof(itemIndex));
}
var isSelected = (bool?)false;
var childNode = _rootNode.GetAt(groupIndex, realizeChild: false);
var childIndex = path.GetAt(i);
node = node.GetAt(childIndex, realizeChild: false);
if (childNode != null)
{
isSelected = childNode.IsSelectedWithPartial(itemIndex);
if (node == null)
{
return false;
}
}
return isSelected;
return node.IsSelected(index.GetAt(index.GetSize() - 1));
}
public bool? IsSelectedAt(IndexPath index)
public bool? IsTreeSelected(IndexPath index)
{
var path = index;
var isRealized = true;
@ -470,13 +438,7 @@ namespace Avalonia.Controls
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)
public void SelectRangeFromAnchor(IndexPath index)
{
using var operation = new Operation(this);
SelectRangeImpl(AnchorIndex, index, select: true);
@ -487,14 +449,8 @@ namespace Avalonia.Controls
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)
public void DeselectRangeFromAnchor(IndexPath index)
{
using var operation = new Operation(this);
SelectRangeImpl(AnchorIndex, index, select: false);
@ -650,22 +606,6 @@ namespace Avalonia.Controls
}
}
private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select)
{
if (_singleSelect)
{
ClearSelection(resetAnchor: true);
}
var childNode = _rootNode.GetAt(groupIndex, realizeChild: true);
var selected = childNode!.Select(itemIndex, select);
if (selected)
{
AnchorIndex = new IndexPath(groupIndex, itemIndex);
}
}
private void SelectWithPathImpl(IndexPath index, bool select)
{
bool selected = false;
@ -707,39 +647,6 @@ namespace Avalonia.Controls
_rootNode.SelectRange(new IndexRange(anchorIndex, index), select);
}
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, realizeChild: true)!;
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0;
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1;
groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);
}
}
private void SelectRangeImpl(IndexPath start, IndexPath end, bool select)
{
var winrtStart = start;

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

@ -476,7 +476,7 @@ namespace Avalonia.Controls.UnitTests
var selectionModel = new SelectionModel();
selectionModel.Source = data;
selectionModel.Select(1, 1);
selectionModel.Select(new IndexPath(1, 1));
ValidateSelection(selectionModel,
new List<IndexPath>()
{
@ -593,8 +593,8 @@ namespace Avalonia.Controls.UnitTests
var selectionModel = new SelectionModel();
selectionModel.Source = data;
selectionModel.Select(1, 1);
selectionModel.Select(1, 2);
selectionModel.Select(new IndexPath(1, 1));
selectionModel.Select(new IndexPath(1, 2));
ValidateSelection(selectionModel,
new List<IndexPath>()
{
@ -688,7 +688,7 @@ namespace Avalonia.Controls.UnitTests
var selectionModel = new SelectionModel();
selectionModel.Source = data;
selectionModel.Select(1, 1);
selectionModel.Select(new IndexPath(1, 1));
ValidateSelection(selectionModel,
new List<IndexPath>()
{
@ -743,7 +743,7 @@ namespace Avalonia.Controls.UnitTests
var selectionModel = new SelectionModel();
selectionModel.Source = data;
selectionModel.Select(1, 1);
selectionModel.Select(new IndexPath(1, 1));
ValidateSelection(selectionModel,
new List<IndexPath>()
{
@ -776,9 +776,9 @@ namespace Avalonia.Controls.UnitTests
selectionModel.Source = data;
selectionModel.SelectionChanged += (sender, args) => { selectionChangedRaised = true; };
selectionModel.Select(1, 0);
selectionModel.Select(1, 1);
selectionModel.Select(1, 2);
selectionModel.Select(new IndexPath(1, 0));
selectionModel.Select(new IndexPath(1, 1));
selectionModel.Select(new IndexPath(1, 2));
ValidateSelection(selectionModel,
new List<IndexPath>()
{
@ -1034,13 +1034,13 @@ namespace Avalonia.Controls.UnitTests
++raised;
};
target.Select(1, 1);
target.Select(new IndexPath(1, 1));
Assert.Equal(1, raised);
}
[Fact]
public void SelectAt_Raises_SelectionChanged()
public void Select_Path_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
@ -1055,7 +1055,7 @@ namespace Avalonia.Controls.UnitTests
++raised;
};
target.SelectAt(new IndexPath(1, 1));
target.Select(new IndexPath(1, 1));
Assert.Equal(1, raised);
}
@ -1147,7 +1147,7 @@ namespace Avalonia.Controls.UnitTests
};
target.AnchorIndex = new IndexPath(1, 1);
target.SelectRangeFromAnchor(1, 6);
target.SelectRangeFromAnchor(new IndexPath(1, 6));
Assert.Equal(1, raised);
}
@ -1170,7 +1170,7 @@ namespace Avalonia.Controls.UnitTests
};
target.AnchorIndex = new IndexPath(1, 1);
target.SelectRangeFromAnchorTo(new IndexPath(1, 6));
target.SelectRangeFromAnchor(new IndexPath(1, 6));
Assert.Equal(1, raised);
}
@ -1207,7 +1207,7 @@ namespace Avalonia.Controls.UnitTests
var raised = 0;
target.Source = CreateNestedData(1, 2, 3);
target.Select(1, 1);
target.Select(new IndexPath(1, 1));
target.SelectionChanged += (s, e) =>
{
@ -1958,11 +1958,11 @@ namespace Avalonia.Controls.UnitTests
Log.Comment((select ? "Selecting " : "DeSelecting ") + groupIndex + "." + itemIndex);
if (select)
{
manager.Select(groupIndex, itemIndex);
manager.Select(new IndexPath(groupIndex, itemIndex));
}
else
{
manager.Deselect(groupIndex, itemIndex);
manager.Deselect(new IndexPath(groupIndex, itemIndex));
}
}
@ -1971,11 +1971,11 @@ namespace Avalonia.Controls.UnitTests
Log.Comment((select ? "Selecting " : "DeSelecting ") + index);
if (select)
{
manager.SelectAt(index);
manager.Select(index);
}
else
{
manager.DeselectAt(index);
manager.Deselect(index);
}
}
@ -1997,11 +1997,11 @@ namespace Avalonia.Controls.UnitTests
Log.Comment("SelectRangeFromAnchor " + groupIndex + "." + itemIndex + " select:" + select.ToString());
if (select)
{
manager.SelectRangeFromAnchor(groupIndex, itemIndex);
manager.SelectRangeFromAnchor(new IndexPath(groupIndex, itemIndex));
}
else
{
manager.DeselectRangeFromAnchor(groupIndex, itemIndex);
manager.DeselectRangeFromAnchor(new IndexPath(groupIndex, itemIndex));
}
}
@ -2010,11 +2010,11 @@ namespace Avalonia.Controls.UnitTests
Log.Comment("SelectRangeFromAnchor " + index + " select: " + select.ToString());
if (select)
{
manager.SelectRangeFromAnchorTo(index);
manager.SelectRangeFromAnchor(index);
}
else
{
manager.DeselectRangeFromAnchorTo(index);
manager.DeselectRangeFromAnchor(index);
}
}
@ -2027,13 +2027,13 @@ namespace Avalonia.Controls.UnitTests
private void SetAnchorIndex(SelectionModel manager, int index)
{
Log.Comment("SetAnchorIndex " + index);
manager.SetAnchorIndex(index);
manager.AnchorIndex = new IndexPath(index);
}
private void SetAnchorIndex(SelectionModel manager, int groupIndex, int itemIndex)
{
Log.Comment("SetAnchor " + groupIndex + "." + itemIndex);
manager.SetAnchorIndex(groupIndex, itemIndex);
manager.AnchorIndex = new IndexPath(groupIndex, itemIndex);
}
private void SetAnchorIndex(SelectionModel manager, IndexPath index)
@ -2067,7 +2067,7 @@ namespace Avalonia.Controls.UnitTests
List<IndexPath> allIndices = GetIndexPathsInSource(selectionModel.Source);
foreach (var index in allIndices)
{
bool? isSelected = selectionModel.IsSelectedAt(index);
bool? isSelected = selectionModel.IsTreeSelected(index);
if (Contains(expectedSelected, index))
{
Assert.True(isSelected.Value, index + " is Selected");
@ -2094,7 +2094,7 @@ namespace Avalonia.Controls.UnitTests
{
foreach (var index in expectedSelected)
{
Assert.True(selectionModel.IsSelectedAt(index).Value, index + " is Selected");
Assert.True(selectionModel.IsTreeSelected(index).Value, index + " is Selected");
}
}
if (expectedSelected.Count > 0)

Loading…
Cancel
Save