Browse Source

Materialize TreeViewItems on selection.

If we're selecting a particular tree view item, then materialize and expand the item's ancestors as `SelectionModel` requests children. Only do this if a particular item is being selected, not when an item is selected as part of a range select.

To do this, needed to add a `FinalIndex`  property to `SelectionModelChildrenRequestedEventArgs` in order to know if we're selecting a descendent of the item whose children are being requested.

This is a massive hack, but I can't think of a better way to do it with the current `TreeView` implementation.
fixes/treeview-nonmaterialized-selection
Steven Kirk 6 years ago
parent
commit
87868cd2bd
  1. 20
      src/Avalonia.Controls/IndexPath.cs
  2. 27
      src/Avalonia.Controls/SelectionModel.cs
  3. 22
      src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
  4. 6
      src/Avalonia.Controls/SelectionNode.cs
  5. 9
      src/Avalonia.Controls/TreeView.cs
  6. 8
      src/Avalonia.Controls/Utils/SelectionTreeHelper.cs

20
src/Avalonia.Controls/IndexPath.cs

@ -123,6 +123,26 @@ namespace Avalonia.Controls
} }
} }
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() public override string ToString()
{ {
if (_path != null) if (_path != null)

27
src/Avalonia.Controls/SelectionModel.cs

@ -141,7 +141,7 @@ namespace Avalonia.Controls
while (current?.AnchorIndex >= 0) while (current?.AnchorIndex >= 0)
{ {
path.Add(current.AnchorIndex); path.Add(current.AnchorIndex);
current = current.GetAt(current.AnchorIndex, false); current = current.GetAt(current.AnchorIndex, false, default);
} }
anchor = new IndexPath(path); anchor = new IndexPath(path);
@ -420,7 +420,7 @@ namespace Avalonia.Controls
for (int i = 0; i < path.GetSize() - 1; i++) for (int i = 0; i < path.GetSize() - 1; i++)
{ {
var childIndex = path.GetAt(i); var childIndex = path.GetAt(i);
node = node.GetAt(childIndex, realizeChild: false); node = node.GetAt(childIndex, false, default);
if (node == null) if (node == null)
{ {
@ -455,7 +455,7 @@ namespace Avalonia.Controls
} }
var isSelected = (bool?)false; var isSelected = (bool?)false;
var childNode = _rootNode.GetAt(groupIndex, realizeChild: false); var childNode = _rootNode.GetAt(groupIndex, false, default);
if (childNode != null) if (childNode != null)
{ {
@ -474,7 +474,7 @@ namespace Avalonia.Controls
for (int i = 0; i < path.GetSize() - 1; i++) for (int i = 0; i < path.GetSize() - 1; i++)
{ {
var childIndex = path.GetAt(i); var childIndex = path.GetAt(i);
node = node.GetAt(childIndex, realizeChild: false); node = node.GetAt(childIndex, false, default);
if (node == null) if (node == null)
{ {
@ -598,7 +598,10 @@ namespace Avalonia.Controls
ApplyAutoSelect(true); ApplyAutoSelect(true);
} }
internal IObservable<object?>? ResolvePath(object data, IndexPath dataIndexPath) internal IObservable<object?>? ResolvePath(
object data,
IndexPath dataIndexPath,
IndexPath finalIndexPath)
{ {
IObservable<object?>? resolved = null; IObservable<object?>? resolved = null;
@ -607,18 +610,22 @@ namespace Avalonia.Controls
{ {
if (_childrenRequestedEventArgs == null) if (_childrenRequestedEventArgs == null)
{ {
_childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, dataIndexPath, false); _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(
data,
dataIndexPath,
finalIndexPath,
false);
} }
else else
{ {
_childrenRequestedEventArgs.Initialize(data, dataIndexPath, false); _childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false);
} }
ChildrenRequested(this, _childrenRequestedEventArgs); ChildrenRequested(this, _childrenRequestedEventArgs);
resolved = _childrenRequestedEventArgs.Children; resolved = _childrenRequestedEventArgs.Children;
// Clear out the values in the args so that it cannot be used after the event handler call. // Clear out the values in the args so that it cannot be used after the event handler call.
_childrenRequestedEventArgs.Initialize(null, default, true); _childrenRequestedEventArgs.Initialize(null, default, default, true);
} }
return resolved; return resolved;
@ -683,7 +690,7 @@ namespace Avalonia.Controls
ClearSelection(resetAnchor: true); ClearSelection(resetAnchor: true);
} }
var childNode = _rootNode.GetAt(groupIndex, realizeChild: true); var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex));
var selected = childNode!.Select(itemIndex, select); var selected = childNode!.Select(itemIndex, select);
if (selected) if (selected)
@ -764,7 +771,7 @@ namespace Avalonia.Controls
for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++) for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++)
{ {
var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!; var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!;
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0; int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0;
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1; int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1;
groupNode.SelectRange(new IndexRange(startIndex, endIndex), select); groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);

22
src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs

@ -16,15 +16,17 @@ namespace Avalonia.Controls
{ {
private object? _source; private object? _source;
private IndexPath _sourceIndexPath; private IndexPath _sourceIndexPath;
private IndexPath _finalIndexPath;
private bool _throwOnAccess; private bool _throwOnAccess;
internal SelectionModelChildrenRequestedEventArgs( internal SelectionModelChildrenRequestedEventArgs(
object source, object source,
IndexPath sourceIndexPath, IndexPath sourceIndexPath,
IndexPath finalIndexPath,
bool throwOnAccess) bool throwOnAccess)
{ {
source = source ?? throw new ArgumentNullException(nameof(source)); source = source ?? throw new ArgumentNullException(nameof(source));
Initialize(source, sourceIndexPath, throwOnAccess); Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess);
} }
/// <summary> /// <summary>
@ -65,9 +67,26 @@ namespace Avalonia.Controls
} }
} }
/// <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( internal void Initialize(
object? source, object? source,
IndexPath sourceIndexPath, IndexPath sourceIndexPath,
IndexPath finalIndexPath,
bool throwOnAccess) bool throwOnAccess)
{ {
if (!throwOnAccess && source == null) if (!throwOnAccess && source == null)
@ -77,6 +96,7 @@ namespace Avalonia.Controls
_source = source; _source = source;
_sourceIndexPath = sourceIndexPath; _sourceIndexPath = sourceIndexPath;
_finalIndexPath = finalIndexPath;
_throwOnAccess = throwOnAccess; _throwOnAccess = throwOnAccess;
} }
} }

6
src/Avalonia.Controls/SelectionNode.cs

@ -162,7 +162,7 @@ namespace Avalonia.Controls
// create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid // 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 // an explosion of node objects. However, I'm still creating the m_childrenNodes
// collection unfortunately. // collection unfortunately.
public SelectionNode? GetAt(int index, bool realizeChild) public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath)
{ {
SelectionNode? child = null; SelectionNode? child = null;
@ -192,7 +192,7 @@ namespace Avalonia.Controls
if (childData != null) if (childData != null)
{ {
var childDataIndexPath = IndexPath.CloneWithChildIndex(index); var childDataIndexPath = IndexPath.CloneWithChildIndex(index);
resolver = _manager.ResolvePath(childData, childDataIndexPath); resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath);
} }
if (resolver != null) if (resolver != null)
@ -864,7 +864,7 @@ namespace Avalonia.Controls
int notSelectedCount = 0; int notSelectedCount = 0;
for (int i = 0; i < ChildrenNodeCount; i++) for (int i = 0; i < ChildrenNodeCount; i++)
{ {
var child = GetAt(i, realizeChild: false); var child = GetAt(i, false, default);
if (child != null) if (child != null)
{ {

9
src/Avalonia.Controls/TreeView.cs

@ -395,10 +395,17 @@ namespace Avalonia.Controls
private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e) private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e)
{ {
var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl; var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as TreeViewItem;
if (container is object) if (container is object)
{ {
if (e.SourceIndex.IsAncestorOf(e.FinalIndex))
{
container.IsExpanded = true;
container.ApplyTemplate();
container.Presenter?.ApplyTemplate();
}
e.Children = Observable.CombineLatest( e.Children = Observable.CombineLatest(
container.GetObservable(TreeViewItem.IsExpandedProperty), container.GetObservable(TreeViewItem.IsExpandedProperty),
container.GetObservable(ItemsProperty), container.GetObservable(ItemsProperty),

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

@ -28,7 +28,7 @@ namespace Avalonia.Controls.Utils
if (depth < path.GetSize() - 1) if (depth < path.GetSize() - 1)
{ {
node = node.GetAt(childIndex, realizeChildren)!; node = node.GetAt(childIndex, realizeChildren, path)!;
} }
} }
} }
@ -50,7 +50,7 @@ namespace Avalonia.Controls.Utils
int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount; int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount;
for (int i = count - 1; i >= 0; i--) for (int i = count - 1; i >= 0; i--)
{ {
var child = nextNode.Node.GetAt(i, realizeChildren); var child = nextNode.Node.GetAt(i, realizeChildren, nextNode.Path);
var childPath = nextNode.Path.CloneWithChildIndex(i); var childPath = nextNode.Path.CloneWithChildIndex(i);
if (child != null) if (child != null)
{ {
@ -90,7 +90,7 @@ namespace Avalonia.Controls.Utils
for (int i = endIndex; i >= startIndex; i--) for (int i = endIndex; i >= startIndex; i--)
{ {
var child = node.GetAt(i, realizeChild: true); var child = node.GetAt(i, true, end);
if (child != null) if (child != null)
{ {
var childPath = currentPath.CloneWithChildIndex(i); var childPath = currentPath.CloneWithChildIndex(i);
@ -112,7 +112,7 @@ namespace Avalonia.Controls.Utils
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1; int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1;
for (int i = endIndex; i >= startIndex; i--) for (int i = endIndex; i >= startIndex; i--)
{ {
var child = info.Node.GetAt(i, realizeChild: true); var child = info.Node.GetAt(i, true, end);
if (child != null) if (child != null)
{ {
var childPath = info.Path.CloneWithChildIndex(i); var childPath = info.Path.CloneWithChildIndex(i);

Loading…
Cancel
Save