Browse Source

Feature: Double click expand/collapse treeview and numpad keyboard shortcuts

Glen Nicol 2 years ago
parent
commit
2a5dceecbb

+ 20 - 0
src/Avalonia.Controls/TreeView.cs

@@ -179,6 +179,26 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Collapse the specified <see cref="TreeViewItem"/> all descendent <see cref="TreeViewItem"/> s.
+        /// </summary>
+        /// <param name="item">The item to collapse.</param>
+        public void CollapseSubTree(TreeViewItem item)
+        {
+            item.IsExpanded = false;
+
+            if (item.Presenter?.Panel != null)
+            {
+                foreach (var child in item.Presenter.Panel.Children)
+                {
+                    if (child is TreeViewItem treeViewItem)
+                    {
+                        CollapseSubTree(treeViewItem);
+                    }
+                }
+            }
+        }
+
         /// <summary>
         /// Selects all items in the <see cref="TreeView"/>.
         /// </summary>

+ 105 - 19
src/Avalonia.Controls/TreeViewItem.cs

@@ -1,3 +1,5 @@
+using System;
+using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Metadata;
@@ -166,30 +168,94 @@ namespace Avalonia.Controls
         {
             if (!e.Handled)
             {
-                switch (e.Key)
+                Func<TreeViewItem, bool>? handler =
+                    e.Key switch
+                    {
+                        Key.Left => ApplyToItemOrRecursivelyIfCtrl(FocusAwareCollapseItem, e.KeyModifiers),
+                        Key.Right => ApplyToItemOrRecursivelyIfCtrl(ExpandItem, e.KeyModifiers),
+                        Key.Enter or Key.Space => ApplyToItemOrRecursivelyIfCtrl(IsExpanded ? CollapseItem : ExpandItem, e.KeyModifiers),
+
+                        // do not handle CTRL with numpad keys
+                        Key.Subtract => FocusAwareCollapseItem,
+                        Key.Add => ExpandItem,
+                        Key.Divide => ApplyToSubtree(CollapseItem),
+                        Key.Multiply => ApplyToSubtree(ExpandItem),
+                        _ => null,
+                    };
+
+                if (handler is not null)
+                {
+                    e.Handled = handler(this);
+                }
+
+                // NOTE: these local functions do not use the TreeView.Expand/CollapseSubtree
+                // function because we want to know if any items were in fact expanded to set the
+                // event handled status. Also the handling here avoids a potential infinite recursion/stack overflow.
+                static Func<TreeViewItem, bool> ApplyToSubtree(Func<TreeViewItem, bool> f)
+                {
+                    // Calling toList enumerates all items before applying functions. This avoids a
+                    // potential infinite loop if there is an infinite tree (the control catalog is
+                    // lazily infinite). But also means a lazily loaded tree will not be expanded completely.
+                    return t => SubTree(t)
+                        .ToList()
+                        .Select(treeViewItem => f(treeViewItem))
+                        .Aggregate(false, (p, c) => p || c);
+                }
+
+                static Func<TreeViewItem, bool> ApplyToItemOrRecursivelyIfCtrl(Func<TreeViewItem,bool> f, KeyModifiers keyModifiers)
+                {
+                    if (keyModifiers.HasAllFlags(KeyModifiers.Control))
+                    {
+                        return ApplyToSubtree(f);
+                    }
+
+                    return f;
+                }
+
+                static bool ExpandItem(TreeViewItem treeViewItem)
+                {
+                    if (treeViewItem.ItemCount > 0 && !treeViewItem.IsExpanded)
+                    {
+                        treeViewItem.IsExpanded = true;
+                        return true;
+                    }
+
+                    return false;
+                }
+
+                static bool CollapseItem(TreeViewItem treeViewItem)
                 {
-                    case Key.Right:
-                        if (Items != null && Items.Cast<object>().Any() && !IsExpanded)
+                    if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded)
+                    {
+                        treeViewItem.IsExpanded = false;
+                        return true;
+                    }
+
+                    return false;
+                }
+
+                static bool FocusAwareCollapseItem(TreeViewItem treeViewItem)
+                {
+                    if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded)
+                    {
+                        if (treeViewItem.IsFocused)
                         {
-                            IsExpanded = true;
-                            e.Handled = true;
+                            treeViewItem.IsExpanded = false;
                         }
-                        break;
-
-                    case Key.Left:
-                        if (Items is not null && Items.Cast<object>().Any() && IsExpanded)
+                        else
                         {
-                            if (IsFocused)
-                            {
-                                IsExpanded = false;
-                            }
-                            else
-                            {
-                                FocusManager.Instance?.Focus(this, NavigationMethod.Directional);
-                            }
-                            e.Handled = true;
+                            FocusManager.Instance?.Focus(treeViewItem, NavigationMethod.Directional);
                         }
-                        break;
+
+                        return true;
+                    }
+
+                    return false;
+                }
+
+                static IEnumerable<TreeViewItem> SubTree(TreeViewItem treeViewItem)
+                {
+                    return new[] { treeViewItem }.Concat(treeViewItem.LogicalChildren.OfType<TreeViewItem>().SelectMany(child => SubTree(child)));
                 }
             }
 
@@ -198,8 +264,19 @@ namespace Avalonia.Controls
 
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
         {
+            if (_header is InputElement previousInputMethod)
+            {
+                previousInputMethod.DoubleTapped -= HeaderDoubleTapped;
+            }
+
             _header = e.NameScope.Find<Control>("PART_Header");
             _templateApplied = true;
+
+            if (_header is InputElement im)
+            {
+                im.DoubleTapped += HeaderDoubleTapped;
+            }
+
             if (_deferredBringIntoViewFlag)
             {
                 _deferredBringIntoViewFlag = false;
@@ -220,6 +297,15 @@ namespace Avalonia.Controls
             return logical != null ? result : @default;
         }
 
+        private void HeaderDoubleTapped(object? sender, TappedEventArgs e)
+        {
+            if (ItemCount > 0)
+            {
+                IsExpanded = !IsExpanded;
+                e.Handled = true;
+            }
+        }
+
         private void OnParentChanged(AvaloniaPropertyChangedEventArgs e)
         {
             if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null)

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml

@@ -75,6 +75,7 @@
                   MinHeight="{TemplateBinding MinHeight}"
                   TemplatedControl.IsTemplateFocusTarget="True">
             <Grid Name="PART_Header"
+                  Background="Transparent"
                   ColumnDefinitions="Auto, *"
                   Margin="{TemplateBinding Level, Mode=OneWay, Converter={StaticResource TreeViewItemLeftMarginConverter}}">
               <Panel Name="PART_ExpandCollapseChevronContainer"

+ 1 - 0
src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml

@@ -44,6 +44,7 @@
                   Focusable="True"
                   TemplatedControl.IsTemplateFocusTarget="True">
             <Grid Name="PART_Header"
+                  Background="Transparent"
                   Margin="{TemplateBinding Level,
                                            Mode=OneWay,
                                            Converter={StaticResource LeftMarginConverter}}"

+ 596 - 3
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -424,6 +424,587 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Double_Clicking_Item_Header_Should_Expand_It()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                CollapseAll(target);
+
+                var item = tree[0].Children[1];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.False(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                _mouse.DoubleClick(header);
+
+                Assert.True(container.IsExpanded);
+            }
+        }
+
+        [Fact]
+        public void Double_Clicking_Item_Header_With_No_Children_Does_Not_Expand_It()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                CollapseAll(target);
+
+                var item = tree[0].Children[1].Children[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.False(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                _mouse.DoubleClick(header);
+
+                Assert.False(container.IsExpanded);
+            }
+        }
+
+        [Fact]
+        public void Double_Clicking_Item_Header_Should_Collapse_It()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                ExpandAll(target);
+
+                var item = tree[0].Children[1];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.True(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                _mouse.DoubleClick(header);
+
+                Assert.False(container.IsExpanded);
+            }
+        }
+
+        [Fact]
+        public void Enter_Key_Should_Collapse_TreeViewItem()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                ExpandAll(target); // NOTE this line
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.True(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Enter,
+                });
+
+                Assert.False(container.IsExpanded);
+            }
+        }
+
+        [Fact]
+        public void Enter_plus_Ctrl_Key_Should_Collapse_TreeViewItem_Recursively()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                ExpandAll(target); // NOTE this line
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.True(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Enter,
+                    KeyModifiers = KeyModifiers.Control,
+                });
+
+                Assert.False(container.IsExpanded);
+
+                AssertEachItemWithChildrenIsCollapsed(item);
+
+                void AssertEachItemWithChildrenIsCollapsed(Node node)
+                {
+                    var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node);
+                    Assert.NotNull(container);
+                    if (node.Children?.Count > 0)
+                    {
+                        Assert.False(container.IsExpanded);
+                        foreach (var c in node.Children)
+                        {
+                            AssertEachItemWithChildrenIsCollapsed(c);
+                        }
+                    }
+                    else
+                    {
+                        Assert.True(container.IsExpanded);
+                    }
+                }
+            }
+        }
+
+        [Fact]
+        public void Enter_Key_Should_Expand_TreeViewItem()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                CollapseAll(target); // NOTE this line
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.False(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Enter,
+                });
+
+                Assert.True(container.IsExpanded);
+            }
+        }
+
+        [Fact]
+        public void Enter_plus_Ctrl_Key_Should_Expand_TreeViewItem_Recursively()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                CollapseAll(target); // NOTE this line
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.False(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Enter,
+                    KeyModifiers = KeyModifiers.Control,
+                });
+
+                Assert.True(container.IsExpanded);
+
+                AssertEachItemWithChildrenIsExpanded(item);
+
+                void AssertEachItemWithChildrenIsExpanded(Node node)
+                {
+                    var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node);
+                    Assert.NotNull(container);
+                    if (node.Children?.Count > 0)
+                    {
+                        Assert.True(container.IsExpanded);
+                        foreach (var c in node.Children)
+                        {
+                            AssertEachItemWithChildrenIsExpanded(c);
+                        }
+                    }
+                    else
+                    {
+                        Assert.False(container.IsExpanded);
+                    }
+                }
+            }
+        }
+
+        [Fact]
+        public void Space_Key_Should_Collapse_TreeViewItem()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                ExpandAll(target); // NOTE this line
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.True(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Enter,
+                });
+
+                Assert.False(container.IsExpanded);
+            }
+        }
+
+        [Fact]
+        public void Space_plus_Ctrl_Key_Should_Collapse_TreeViewItem_Recursively()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                ExpandAll(target); // NOTE this line
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.True(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Enter,
+                    KeyModifiers = KeyModifiers.Control,
+                });
+
+                Assert.False(container.IsExpanded);
+
+                AssertEachItemWithChildrenIsCollapsed(item);
+
+                void AssertEachItemWithChildrenIsCollapsed(Node node)
+                {
+                    var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node);
+                    Assert.NotNull(container);
+                    if (node.Children?.Count > 0)
+                    {
+                        Assert.False(container.IsExpanded);
+                        foreach (var c in node.Children)
+                        {
+                            AssertEachItemWithChildrenIsCollapsed(c);
+                        }
+                    }
+                    else
+                    {
+                        Assert.True(container.IsExpanded);
+                    }
+                }
+            }
+        }
+
+        [Fact]
+        public void Space_Key_Should_Expand_TreeViewItem()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                CollapseAll(target); // NOTE this line
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.False(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Enter,
+                });
+
+                Assert.True(container.IsExpanded);
+            }
+        }
+
+        [Fact]
+        public void Space_plus_Ctrl_Key_Should_Expand_TreeViewItem_Recursively()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                CollapseAll(target); // NOTE this line
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                Assert.False(container.IsExpanded);
+                var header = container.Header as Interactive;
+                Assert.NotNull(header);
+
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Enter,
+                    KeyModifiers = KeyModifiers.Control,
+                });
+
+                Assert.True(container.IsExpanded);
+
+                AssertEachItemWithChildrenIsExpanded(item);
+
+                void AssertEachItemWithChildrenIsExpanded(Node node)
+                {
+                    var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node);
+                    Assert.NotNull(container);
+                    if (node.Children?.Count > 0)
+                    {
+                        Assert.True(container.IsExpanded);
+                        foreach (var c in node.Children)
+                        {
+                            AssertEachItemWithChildrenIsExpanded(c);
+                        }
+                    }
+                    else
+                    {
+                        Assert.False(container.IsExpanded);
+                    }
+                }
+            }
+        }
+
+        [Fact]
+        public void Numpad_Star_Should_Expand_All_Children_Recursively()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                CollapseAll(target);
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Multiply,
+                });
+
+                AssertEachItemWithChildrenIsExpanded(item);
+
+                void AssertEachItemWithChildrenIsExpanded(Node node)
+                {
+                    var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node);
+                    Assert.NotNull(container);
+                    if (node.Children?.Count > 0)
+                    {
+                        Assert.True(container.IsExpanded);
+                        foreach (var c in node.Children)
+                        {
+                            AssertEachItemWithChildrenIsExpanded(c);
+                        }
+                    }
+                    else
+                    {
+                        Assert.False(container.IsExpanded);
+                    }
+                }
+            }
+        }
+
+        [Fact]
+        public void Numpad_Slash_Should_Collapse_All_Children_Recursively()
+        {
+            using (Application())
+            {
+                var tree = CreateTestTreeData();
+                var target = new TreeView
+                {
+                    Template = CreateTreeViewTemplate(),
+                    Items = tree,
+                };
+
+                var visualRoot = new TestRoot();
+                visualRoot.Child = target;
+
+                CreateNodeDataTemplate(target);
+                ApplyTemplates(target);
+                ExpandAll(target);
+
+                var item = tree[0];
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+                Assert.NotNull(container);
+                container.RaiseEvent(new KeyEventArgs
+                {
+                    RoutedEvent = InputElement.KeyDownEvent,
+                    Key = Key.Divide,
+                });
+
+                AssertEachItemWithChildrenIsCollapsed(item);
+
+                void AssertEachItemWithChildrenIsCollapsed(Node node)
+                {
+                    var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node);
+                    Assert.NotNull(container);
+                    if (node.Children?.Count > 0)
+                    {
+                        Assert.False(container.IsExpanded);
+                        foreach (var c in node.Children)
+                        {
+                            AssertEachItemWithChildrenIsCollapsed(c);
+                        }
+                    }
+                    else
+                    {
+                        Assert.True(container.IsExpanded);
+                    }
+                }
+            }
+        }
+
         [Fact]
         public void Setting_SelectedItem_Should_Set_Container_Selected()
         {
@@ -1313,10 +1894,14 @@ namespace Avalonia.Controls.UnitTests
             {
                 Children =
                 {
-                    new ContentPresenter
+                    new Border
                     {
-                        Name = "PART_HeaderPresenter",
-                        [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty],
+                        Name = "PART_Header",
+                        Child = new ContentPresenter
+                        {
+                            Name = "PART_HeaderPresenter",
+                            [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty],
+                        }.RegisterInNameScope(scope)
                     }.RegisterInNameScope(scope),
                     new ItemsPresenter
                     {
@@ -1335,6 +1920,14 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        private void CollapseAll(TreeView tree)
+        {
+            foreach (var i in tree.ItemContainerGenerator.Containers)
+            {
+                tree.CollapseSubTree((TreeViewItem)i.ContainerControl);
+            }
+        }
+
         private List<string> ExtractItemHeader(TreeView tree, int level)
         {
             return ExtractItemContent(tree.Presenter.Panel, 0, level)

+ 15 - 1
tests/Avalonia.UnitTests/MouseTestHelper.cs

@@ -65,6 +65,7 @@ namespace Avalonia.UnitTests
         }
 
         public void Move(Interactive target, in Point position, KeyModifiers modifiers = default) => Move(target, target, position, modifiers);
+
         public void Move(Interactive target, Interactive source, in Point position, KeyModifiers modifiers = default)
         {
             target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (Visual)target, position,
@@ -98,13 +99,26 @@ namespace Avalonia.UnitTests
         public void Click(Interactive target, MouseButton button = MouseButton.Left, Point position = default,
             KeyModifiers modifiers = default)
             => Click(target, target, button, position, modifiers);
+
         public void Click(Interactive target, Interactive source, MouseButton button = MouseButton.Left, 
             Point position = default, KeyModifiers modifiers = default)
         {
             Down(target, source, button, position, modifiers);
             Up(target, source, button, position, modifiers);
         }
-        
+
+        public void DoubleClick(Interactive target, MouseButton button = MouseButton.Left, Point position = default,
+            KeyModifiers modifiers = default)
+            => DoubleClick(target, target, button, position, modifiers);
+
+        public void DoubleClick(Interactive target, Interactive source, MouseButton button = MouseButton.Left,
+            Point position = default, KeyModifiers modifiers = default)
+        {
+            Down(target, source, button, position, modifiers, clickCount: 1);
+            Up(target, source, button, position, modifiers);
+            Down(target, source, button, position, modifiers, clickCount: 2);
+        }
+
         public void Enter(Interactive target)
         {
             target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnteredEvent, target, _pointer, (Visual)target, default,