using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Threading; namespace Avalonia.Controls { /// /// An item in a . /// [TemplatePart("PART_Header", typeof(Control))] [PseudoClasses(":pressed", ":selected")] public class TreeViewItem : HeaderedItemsControl, ISelectable { /// /// Defines the property. /// public static readonly StyledProperty IsExpandedProperty = AvaloniaProperty.Register( nameof(IsExpanded), defaultBindingMode: BindingMode.TwoWay); /// /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = SelectingItemsControl.IsSelectedProperty.AddOwner(); /// /// Defines the property. /// public static readonly DirectProperty LevelProperty = AvaloniaProperty.RegisterDirect( nameof(Level), o => o.Level); private static readonly FuncTemplate DefaultPanel = new(() => new StackPanel()); private TreeView? _treeView; private Control? _header; private int _level; private bool _templateApplied; private bool _deferredBringIntoViewFlag; /// /// Initializes static members of the class. /// static TreeViewItem() { SelectableMixin.Attach(IsSelectedProperty); PressedMixin.Attach(); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); RequestBringIntoViewEvent.AddClassHandler((x, e) => x.OnRequestBringIntoView(e)); } /// /// Gets or sets a value indicating whether the item is expanded to show its children. /// public bool IsExpanded { get => GetValue(IsExpandedProperty); set => SetValue(IsExpandedProperty, value); } /// /// Gets or sets the selection state of the item. /// public bool IsSelected { get => GetValue(IsSelectedProperty); set => SetValue(IsSelectedProperty, value); } /// /// Gets the level/indentation of the item. /// public int Level { get => _level; private set => SetAndRaise(LevelProperty, ref _level, value); } internal TreeView? TreeViewOwner => _treeView; protected internal override Control CreateContainerForItemOverride() { return EnsureTreeView().CreateContainerForItemOverride(); } protected internal override bool IsItemItsOwnContainerOverride(Control item) { return EnsureTreeView().IsItemItsOwnContainerOverride(item); } protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index) { EnsureTreeView().PrepareContainerForItemOverride(container, item, index); } protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index) { EnsureTreeView().ContainerForItemPreparedOverride(container, item, index); } /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); _treeView = this.GetLogicalAncestors().OfType().FirstOrDefault(); Level = CalculateDistanceFromLogicalParent(this) - 1; if (ItemTemplate == null && _treeView?.ItemTemplate != null) { SetCurrentValue(ItemTemplateProperty, _treeView.ItemTemplate); } if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null) { SetCurrentValue(ItemContainerThemeProperty, _treeView.ItemContainerTheme); } } protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) { if (e.TargetObject == this) { if (!_templateApplied) { _deferredBringIntoViewFlag = true; return; } if (_header != null) { var m = _header.TransformToVisual(this); if (m.HasValue) { var bounds = new Rect(_header.Bounds.Size); var rect = bounds.TransformToAABB(m.Value); e.TargetRect = rect; } } } } /// protected override void OnKeyDown(KeyEventArgs e) { if (!e.Handled) { Func? 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 ApplyToSubtree(Func 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 ApplyToItemOrRecursivelyIfCtrl(Func 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.SetCurrentValue(IsExpandedProperty, true); return true; } return false; } static bool CollapseItem(TreeViewItem treeViewItem) { if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded) { treeViewItem.SetCurrentValue(IsExpandedProperty, false); return true; } return false; } static bool FocusAwareCollapseItem(TreeViewItem treeViewItem) { if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded) { if (treeViewItem.IsFocused) { treeViewItem.SetCurrentValue(IsExpandedProperty, false); } else { FocusManager.Instance?.Focus(treeViewItem, NavigationMethod.Directional); } return true; } return false; } static IEnumerable SubTree(TreeViewItem treeViewItem) { return new[] { treeViewItem }.Concat(treeViewItem.LogicalChildren.OfType().SelectMany(child => SubTree(child))); } } // Don't call base.OnKeyDown - let events bubble up to containing TreeView. } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { if (_header is InputElement previousInputMethod) { previousInputMethod.DoubleTapped -= HeaderDoubleTapped; } _header = e.NameScope.Find("PART_Header"); _templateApplied = true; if (_header is InputElement im) { im.DoubleTapped += HeaderDoubleTapped; } if (_deferredBringIntoViewFlag) { _deferredBringIntoViewFlag = false; Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll } } /// /// Invoked when the event occurs in the header. /// protected virtual void OnHeaderDoubleTapped(TappedEventArgs e) { if (ItemCount > 0) { SetCurrentValue(IsExpandedProperty, !IsExpanded); e.Handled = true; } } private static int CalculateDistanceFromLogicalParent(ILogical? logical, int @default = -1) where T : class { var result = 0; while (logical != null && !(logical is T)) { ++result; logical = logical.LogicalParent; } return logical != null ? result : @default; } private TreeView EnsureTreeView() => _treeView ?? throw new InvalidOperationException("The TreeViewItem is not part of a TreeView."); private void HeaderDoubleTapped(object? sender, TappedEventArgs e) { OnHeaderDoubleTapped(e); } } }