Browse Source

Added SelectionMode.AutoSelect.

pull/3509/head
Steven Kirk 7 years ago
parent
commit
0a608d47dc
  1. 87
      src/Avalonia.Controls/SelectionModel.cs
  2. 3
      src/Avalonia.Controls/SelectionNode.cs
  3. 203
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

87
src/Avalonia.Controls/SelectionModel.cs

@ -17,6 +17,8 @@ namespace Avalonia.Controls
{ {
private readonly SelectionNode _rootNode; private readonly SelectionNode _rootNode;
private bool _singleSelect; private bool _singleSelect;
private bool _autoSelect;
private int _operationCount;
private IReadOnlyList<IndexPath>? _selectedIndicesCached; private IReadOnlyList<IndexPath>? _selectedIndicesCached;
private IReadOnlyList<object?>? _selectedItemsCached; private IReadOnlyList<object?>? _selectedItemsCached;
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs; private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
@ -38,21 +40,25 @@ namespace Avalonia.Controls
{ {
if (_rootNode.Source != value) if (_rootNode.Source != value)
{ {
var wasNull = _rootNode.Source == null; var raiseChanged = _rootNode.Source == null && SelectedIndices.Count > 0;
if (_rootNode.Source != null) if (_rootNode.Source != null)
{ {
using (var operation = new Operation(this)) if (_rootNode.Source != null)
{ {
ClearSelection(resetAnchor: true); using (var operation = new Operation(this))
{
ClearSelection(resetAnchor: true);
}
} }
} }
_rootNode.Source = value; _rootNode.Source = value;
ApplyAutoSelect();
RaisePropertyChanged("Source"); RaisePropertyChanged("Source");
if (wasNull) if (raiseChanged)
{ {
var e = new SelectionModelSelectionChangedEventArgs( var e = new SelectionModelSelectionChangedEventArgs(
null, null,
@ -92,12 +98,25 @@ namespace Avalonia.Controls
} }
} }
public bool RetainSelectionOnReset public bool RetainSelectionOnReset
{ {
get => _rootNode.RetainSelectionOnReset; get => _rootNode.RetainSelectionOnReset;
set => _rootNode.RetainSelectionOnReset = value; set => _rootNode.RetainSelectionOnReset = value;
} }
public bool AutoSelect
{
get => _autoSelect;
set
{
if (_autoSelect != value)
{
_autoSelect = value;
ApplyAutoSelect();
}
}
}
public IndexPath AnchorIndex public IndexPath AnchorIndex
{ {
get get
@ -356,18 +375,21 @@ namespace Avalonia.Controls
{ {
using var operation = new Operation(this); using var operation = new Operation(this);
SelectImpl(index, select: false); SelectImpl(index, select: false);
ApplyAutoSelect();
} }
public void Deselect(int groupIndex, int itemIndex) public void Deselect(int groupIndex, int itemIndex)
{ {
using var operation = new Operation(this); using var operation = new Operation(this);
SelectWithGroupImpl(groupIndex, itemIndex, select: false); SelectWithGroupImpl(groupIndex, itemIndex, select: false);
ApplyAutoSelect();
} }
public void DeselectAt(IndexPath index) public void DeselectAt(IndexPath index)
{ {
using var operation = new Operation(this); using var operation = new Operation(this);
SelectWithPathImpl(index, select: false); SelectWithPathImpl(index, select: false);
ApplyAutoSelect();
} }
public bool? IsSelected(int index) public bool? IsSelected(int index)
@ -508,6 +530,7 @@ namespace Avalonia.Controls
{ {
using var operation = new Operation(this); using var operation = new Operation(this);
ClearSelection(resetAnchor: true); ClearSelection(resetAnchor: true);
ApplyAutoSelect();
} }
protected void OnPropertyChanged(string propertyName) protected void OnPropertyChanged(string propertyName)
@ -521,10 +544,18 @@ namespace Avalonia.Controls
} }
public void OnSelectionInvalidatedDueToCollectionChange( public void OnSelectionInvalidatedDueToCollectionChange(
IReadOnlyList<object?>? removedItems) bool selectionInvalidated,
IReadOnlyList<object>? removedItems)
{ {
var e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null); SelectionModelSelectionChangedEventArgs? e = null;
if (selectionInvalidated)
{
e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null);
}
OnSelectionChanged(e); OnSelectionChanged(e);
ApplyAutoSelect();
} }
internal object? ResolvePath(object data, SelectionNode sourceNode) internal object? ResolvePath(object data, SelectionNode sourceNode)
@ -733,24 +764,52 @@ namespace Avalonia.Controls
}); });
} }
private void BeginOperation() => _rootNode.BeginOperation(); private void BeginOperation()
{
if (_operationCount++ == 0)
{
_rootNode.BeginOperation();
}
}
private void EndOperation() private void EndOperation()
{ {
var changes = new List<SelectionNodeOperation>(); if (_operationCount == 0)
_rootNode.EndOperation(changes); {
throw new AvaloniaInternalException("No selection operation in progress.");
}
SelectionModelSelectionChangedEventArgs? e = null; SelectionModelSelectionChangedEventArgs? e = null;
if (changes.Count > 0) if (--_operationCount == 0)
{ {
var changeSet = new SelectionModelChangeSet(changes); var changes = new List<SelectionNodeOperation>();
e = changeSet.CreateEventArgs(); _rootNode.EndOperation(changes);
if (changes.Count > 0)
{
var changeSet = new SelectionModelChangeSet(changes);
e = changeSet.CreateEventArgs();
}
} }
OnSelectionChanged(e); OnSelectionChanged(e);
} }
private void ApplyAutoSelect()
{
if (AutoSelect)
{
_selectedIndicesCached = null;
if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0)
{
using var operation = new Operation(this);
SelectImpl(0, true);
}
}
}
internal class SelectedItemInfo : ISelectedItemInfo internal class SelectedItemInfo : ISelectedItemInfo
{ {
public SelectedItemInfo(SelectionNode node, IndexPath path) public SelectedItemInfo(SelectionNode node, IndexPath path)

3
src/Avalonia.Controls/SelectionNode.cs

@ -587,8 +587,9 @@ namespace Avalonia.Controls
if (selectionInvalidated) if (selectionInvalidated)
{ {
OnSelectionChanged(); OnSelectionChanged();
_manager.OnSelectionInvalidatedDueToCollectionChange(removed);
} }
_manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed);
} }
private bool OnItemsAdded(int index, int count) private bool OnItemsAdded(int index, int count)

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

@ -1569,8 +1569,6 @@ namespace Avalonia.Controls.UnitTests
}; };
data.Reset(); data.Reset();
Assert.Equal(1, raised);
} }
[Fact] [Fact]
@ -1642,6 +1640,207 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new[] { new IndexPath(2) }, target.SelectedIndices); Assert.Equal(new[] { new IndexPath(2) }, target.SelectedIndices);
} }
[Fact]
public void AutoSelect_Selects_When_Enabled()
{
var data = new[] { "foo", "bar", "baz" };
var target = new SelectionModel { Source = data };
var raised = 0;
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices);
Assert.Equal(new[] { "foo" }, e.SelectedItems);
++raised;
};
target.AutoSelect = true;
Assert.Equal(new IndexPath(0), target.SelectedIndex);
Assert.Equal(1, raised);
}
[Fact]
public void AutoSelect_Selects_When_Source_Assigned()
{
var data = new[] { "foo", "bar", "baz" };
var target = new SelectionModel { AutoSelect = true };
var raised = 0;
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices);
Assert.Equal(new[] { "foo" }, e.SelectedItems);
++raised;
};
target.Source = data;
Assert.Equal(new IndexPath(0), target.SelectedIndex);
Assert.Equal(1, raised);
}
[Fact]
public void AutoSelect_Selects_When_New_Source_Assigned_And_Old_Source_Has_Selection()
{
var data = new[] { "foo", "bar", "baz" };
var target = new SelectionModel { AutoSelect = true, Source = data };
var raised = 0;
target.SelectionChanged += (s, e) =>
{
if (raised == 0)
{
Assert.Equal(new[] { new IndexPath(0) }, e.DeselectedIndices);
Assert.Equal(new[] { "foo" }, e.DeselectedItems);
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
}
else
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices);
Assert.Equal(new[] { "newfoo" }, e.SelectedItems);
}
++raised;
};
target.Source = new[] { "newfoo" };
Assert.Equal(new IndexPath(0), target.SelectedIndex);
Assert.Equal(2, raised);
}
[Fact]
public void AutoSelect_Selects_When_First_Item_Added()
{
var data = new ObservableCollection<string>();
var target = new SelectionModel { AutoSelect = true , Source = data };
var raised = 0;
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices);
Assert.Equal(new[] { "foo" }, e.SelectedItems);
++raised;
};
data.Add("foo");
Assert.Equal(new IndexPath(0), target.SelectedIndex);
Assert.Equal(1, raised);
}
[Fact]
public void AutoSelect_Selects_When_Selected_Item_Removed()
{
var data = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new SelectionModel { AutoSelect = true, Source = data };
var raised = 0;
target.SelectedIndex = new IndexPath(2);
target.SelectionChanged += (s, e) =>
{
if (raised == 0)
{
Assert.Empty(e.DeselectedIndices);
Assert.Equal(new[] { "baz" }, e.DeselectedItems);
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
}
else
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices);
Assert.Equal(new[] { "foo" }, e.SelectedItems);
}
++raised;
};
data.RemoveAt(2);
Assert.Equal(new IndexPath(0), target.SelectedIndex);
Assert.Equal(2, raised);
}
[Fact]
public void AutoSelect_Selects_On_Deselection()
{
var data = new[] { "foo", "bar", "baz" };
var target = new SelectionModel { AutoSelect = true, Source = data };
var raised = 0;
target.SelectedIndex = new IndexPath(2);
target.SelectionChanged += (s, e) =>
{
Assert.Equal(new[] { new IndexPath(2) }, e.DeselectedIndices);
Assert.Equal(new[] { "baz" }, e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices);
Assert.Equal(new[] { "foo" }, e.SelectedItems);
++raised;
};
target.Deselect(2);
Assert.Equal(new IndexPath(0), target.SelectedIndex);
Assert.Equal(1, raised);
}
[Fact]
public void AutoSelect_Selects_On_ClearSelection()
{
var data = new[] { "foo", "bar", "baz" };
var target = new SelectionModel { AutoSelect = true, Source = data };
var raised = 0;
target.SelectedIndex = new IndexPath(2);
target.SelectionChanged += (s, e) =>
{
Assert.Equal(new[] { new IndexPath(2) }, e.DeselectedIndices);
Assert.Equal(new[] { "baz" }, e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices);
Assert.Equal(new[] { "foo" }, e.SelectedItems);
++raised;
};
target.ClearSelection();
Assert.Equal(new IndexPath(0), target.SelectedIndex);
Assert.Equal(1, raised);
}
[Fact]
public void AutoSelect_Overrides_Deselecting_First_Item()
{
var data = new[] { "foo", "bar", "baz" };
var target = new SelectionModel { AutoSelect = true, Source = data };
var raised = 0;
target.Select(0);
target.SelectionChanged += (s, e) =>
{
++raised;
};
target.Deselect(0);
Assert.Equal(new IndexPath(0), target.SelectedIndex);
Assert.Equal(0, raised);
}
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