TreeViewItem.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Avalonia.Controls.Metadata;
  5. using Avalonia.Controls.Mixins;
  6. using Avalonia.Controls.Primitives;
  7. using Avalonia.Controls.Templates;
  8. using Avalonia.Data;
  9. using Avalonia.Input;
  10. using Avalonia.LogicalTree;
  11. using Avalonia.Threading;
  12. namespace Avalonia.Controls
  13. {
  14. /// <summary>
  15. /// An item in a <see cref="TreeView"/>.
  16. /// </summary>
  17. [TemplatePart("PART_Header", typeof(Control))]
  18. [PseudoClasses(":pressed", ":selected")]
  19. public class TreeViewItem : HeaderedItemsControl, ISelectable
  20. {
  21. /// <summary>
  22. /// Defines the <see cref="IsExpanded"/> property.
  23. /// </summary>
  24. public static readonly StyledProperty<bool> IsExpandedProperty =
  25. AvaloniaProperty.Register<TreeViewItem, bool>(
  26. nameof(IsExpanded),
  27. defaultBindingMode: BindingMode.TwoWay);
  28. /// <summary>
  29. /// Defines the <see cref="IsSelected"/> property.
  30. /// </summary>
  31. public static readonly StyledProperty<bool> IsSelectedProperty =
  32. SelectingItemsControl.IsSelectedProperty.AddOwner<TreeViewItem>();
  33. /// <summary>
  34. /// Defines the <see cref="Level"/> property.
  35. /// </summary>
  36. public static readonly DirectProperty<TreeViewItem, int> LevelProperty =
  37. AvaloniaProperty.RegisterDirect<TreeViewItem, int>(
  38. nameof(Level), o => o.Level);
  39. private static readonly FuncTemplate<Panel?> DefaultPanel =
  40. new(() => new StackPanel());
  41. private TreeView? _treeView;
  42. private Control? _header;
  43. private int _level;
  44. private bool _templateApplied;
  45. private bool _deferredBringIntoViewFlag;
  46. /// <summary>
  47. /// Initializes static members of the <see cref="TreeViewItem"/> class.
  48. /// </summary>
  49. static TreeViewItem()
  50. {
  51. SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty);
  52. PressedMixin.Attach<TreeViewItem>();
  53. FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
  54. ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
  55. RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>((x, e) => x.OnRequestBringIntoView(e));
  56. }
  57. /// <summary>
  58. /// Gets or sets a value indicating whether the item is expanded to show its children.
  59. /// </summary>
  60. public bool IsExpanded
  61. {
  62. get => GetValue(IsExpandedProperty);
  63. set => SetValue(IsExpandedProperty, value);
  64. }
  65. /// <summary>
  66. /// Gets or sets the selection state of the item.
  67. /// </summary>
  68. public bool IsSelected
  69. {
  70. get => GetValue(IsSelectedProperty);
  71. set => SetValue(IsSelectedProperty, value);
  72. }
  73. /// <summary>
  74. /// Gets the level/indentation of the item.
  75. /// </summary>
  76. public int Level
  77. {
  78. get => _level;
  79. private set => SetAndRaise(LevelProperty, ref _level, value);
  80. }
  81. internal TreeView? TreeViewOwner => _treeView;
  82. protected internal override Control CreateContainerForItemOverride()
  83. {
  84. return EnsureTreeView().CreateContainerForItemOverride();
  85. }
  86. protected internal override bool IsItemItsOwnContainerOverride(Control item)
  87. {
  88. return EnsureTreeView().IsItemItsOwnContainerOverride(item);
  89. }
  90. protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)
  91. {
  92. EnsureTreeView().PrepareContainerForItemOverride(container, item, index);
  93. }
  94. protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
  95. {
  96. EnsureTreeView().ContainerForItemPreparedOverride(container, item, index);
  97. }
  98. /// <inheritdoc/>
  99. protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
  100. {
  101. base.OnAttachedToLogicalTree(e);
  102. _treeView = this.GetLogicalAncestors().OfType<TreeView>().FirstOrDefault();
  103. Level = CalculateDistanceFromLogicalParent<TreeView>(this) - 1;
  104. if (ItemTemplate == null && _treeView?.ItemTemplate != null)
  105. {
  106. SetCurrentValue(ItemTemplateProperty, _treeView.ItemTemplate);
  107. }
  108. if (ItemContainerTheme == null && _treeView?.ItemContainerTheme != null)
  109. {
  110. SetCurrentValue(ItemContainerThemeProperty, _treeView.ItemContainerTheme);
  111. }
  112. }
  113. protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
  114. {
  115. if (e.TargetObject == this)
  116. {
  117. if (!_templateApplied)
  118. {
  119. _deferredBringIntoViewFlag = true;
  120. return;
  121. }
  122. if (_header != null)
  123. {
  124. var m = _header.TransformToVisual(this);
  125. if (m.HasValue)
  126. {
  127. var bounds = new Rect(_header.Bounds.Size);
  128. var rect = bounds.TransformToAABB(m.Value);
  129. e.TargetRect = rect;
  130. }
  131. }
  132. }
  133. }
  134. /// <inheritdoc/>
  135. protected override void OnKeyDown(KeyEventArgs e)
  136. {
  137. if (!e.Handled)
  138. {
  139. Func<TreeViewItem, bool>? handler =
  140. e.Key switch
  141. {
  142. Key.Left => ApplyToItemOrRecursivelyIfCtrl(FocusAwareCollapseItem, e.KeyModifiers),
  143. Key.Right => ApplyToItemOrRecursivelyIfCtrl(ExpandItem, e.KeyModifiers),
  144. Key.Enter or Key.Space => ApplyToItemOrRecursivelyIfCtrl(IsExpanded ? CollapseItem : ExpandItem, e.KeyModifiers),
  145. // do not handle CTRL with numpad keys
  146. Key.Subtract => FocusAwareCollapseItem,
  147. Key.Add => ExpandItem,
  148. Key.Divide => ApplyToSubtree(CollapseItem),
  149. Key.Multiply => ApplyToSubtree(ExpandItem),
  150. _ => null,
  151. };
  152. if (handler is not null)
  153. {
  154. e.Handled = handler(this);
  155. }
  156. // NOTE: these local functions do not use the TreeView.Expand/CollapseSubtree
  157. // function because we want to know if any items were in fact expanded to set the
  158. // event handled status. Also the handling here avoids a potential infinite recursion/stack overflow.
  159. static Func<TreeViewItem, bool> ApplyToSubtree(Func<TreeViewItem, bool> f)
  160. {
  161. // Calling toList enumerates all items before applying functions. This avoids a
  162. // potential infinite loop if there is an infinite tree (the control catalog is
  163. // lazily infinite). But also means a lazily loaded tree will not be expanded completely.
  164. return t => SubTree(t)
  165. .ToList()
  166. .Select(treeViewItem => f(treeViewItem))
  167. .Aggregate(false, (p, c) => p || c);
  168. }
  169. static Func<TreeViewItem, bool> ApplyToItemOrRecursivelyIfCtrl(Func<TreeViewItem,bool> f, KeyModifiers keyModifiers)
  170. {
  171. if (keyModifiers.HasAllFlags(KeyModifiers.Control))
  172. {
  173. return ApplyToSubtree(f);
  174. }
  175. return f;
  176. }
  177. static bool ExpandItem(TreeViewItem treeViewItem)
  178. {
  179. if (treeViewItem.ItemCount > 0 && !treeViewItem.IsExpanded)
  180. {
  181. treeViewItem.SetCurrentValue(IsExpandedProperty, true);
  182. return true;
  183. }
  184. return false;
  185. }
  186. static bool CollapseItem(TreeViewItem treeViewItem)
  187. {
  188. if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded)
  189. {
  190. treeViewItem.SetCurrentValue(IsExpandedProperty, false);
  191. return true;
  192. }
  193. return false;
  194. }
  195. static bool FocusAwareCollapseItem(TreeViewItem treeViewItem)
  196. {
  197. if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded)
  198. {
  199. if (treeViewItem.IsFocused)
  200. {
  201. treeViewItem.SetCurrentValue(IsExpandedProperty, false);
  202. }
  203. else
  204. {
  205. FocusManager.Instance?.Focus(treeViewItem, NavigationMethod.Directional);
  206. }
  207. return true;
  208. }
  209. return false;
  210. }
  211. static IEnumerable<TreeViewItem> SubTree(TreeViewItem treeViewItem)
  212. {
  213. return new[] { treeViewItem }.Concat(treeViewItem.LogicalChildren.OfType<TreeViewItem>().SelectMany(child => SubTree(child)));
  214. }
  215. }
  216. // Don't call base.OnKeyDown - let events bubble up to containing TreeView.
  217. }
  218. protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
  219. {
  220. if (_header is InputElement previousInputMethod)
  221. {
  222. previousInputMethod.DoubleTapped -= HeaderDoubleTapped;
  223. }
  224. _header = e.NameScope.Find<Control>("PART_Header");
  225. _templateApplied = true;
  226. if (_header is InputElement im)
  227. {
  228. im.DoubleTapped += HeaderDoubleTapped;
  229. }
  230. if (_deferredBringIntoViewFlag)
  231. {
  232. _deferredBringIntoViewFlag = false;
  233. Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll
  234. }
  235. }
  236. /// <summary>
  237. /// Invoked when the <see cref="InputElement.DoubleTapped"/> event occurs in the header.
  238. /// </summary>
  239. protected virtual void OnHeaderDoubleTapped(TappedEventArgs e)
  240. {
  241. if (ItemCount > 0)
  242. {
  243. SetCurrentValue(IsExpandedProperty, !IsExpanded);
  244. e.Handled = true;
  245. }
  246. }
  247. private static int CalculateDistanceFromLogicalParent<T>(ILogical? logical, int @default = -1) where T : class
  248. {
  249. var result = 0;
  250. while (logical != null && !(logical is T))
  251. {
  252. ++result;
  253. logical = logical.LogicalParent;
  254. }
  255. return logical != null ? result : @default;
  256. }
  257. private TreeView EnsureTreeView() => _treeView ??
  258. throw new InvalidOperationException("The TreeViewItem is not part of a TreeView.");
  259. private void HeaderDoubleTapped(object? sender, TappedEventArgs e)
  260. {
  261. OnHeaderDoubleTapped(e);
  262. }
  263. }
  264. }