FancyTree.xaml.cs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Numerics;
  3. using Content.Client.Resources;
  4. using Robust.Client.AutoGenerated;
  5. using Robust.Client.Graphics;
  6. using Robust.Client.ResourceManagement;
  7. using Robust.Client.UserInterface;
  8. using Robust.Client.UserInterface.Controls;
  9. using Robust.Client.UserInterface.XAML;
  10. using Robust.Shared.Timing;
  11. using Robust.Shared.Utility;
  12. namespace Content.Client.UserInterface.Controls.FancyTree;
  13. /// <summary>
  14. /// Functionally similar to <see cref="Tree"/>, but with collapsible sections,
  15. /// </summary>
  16. [GenerateTypedNameReferences]
  17. public sealed partial class FancyTree : Control
  18. {
  19. [Dependency] private readonly IResourceCache _resCache = default!;
  20. public const string StylePropertyLineWidth = "LineWidth";
  21. public const string StylePropertyLineColor = "LineColor";
  22. public const string StylePropertyIconColor = "IconColor";
  23. public const string StylePropertyIconExpanded = "IconExpanded";
  24. public const string StylePropertyIconCollapsed = "IconCollapsed";
  25. public const string StylePropertyIconNoChildren = "IconNoChildren";
  26. public readonly List<TreeItem> Items = new();
  27. public event Action<TreeItem?>? OnSelectedItemChanged;
  28. public int? SelectedIndex { get; private set; }
  29. private bool _rowStyleUpdateQueued = true;
  30. /// <summary>
  31. /// Whether or not to draw the lines connecting parents & children.
  32. /// </summary>
  33. public bool DrawLines = true;
  34. /// <summary>
  35. /// Colour of the lines connecting parents & their child entries.
  36. /// </summary>
  37. public Color LineColor = Color.White;
  38. /// <summary>
  39. /// Color used to modulate the icon textures.
  40. /// </summary>
  41. public Color IconColor = Color.White;
  42. /// <summary>
  43. /// Width of the lines connecting parents & their child entries.
  44. /// </summary>
  45. public int LineWidth = 2;
  46. // If people ever want to customize this, this should be a style parameter/
  47. public const int Indentation = 16;
  48. public const string DefaultIconExpanded = "/Textures/Interface/Nano/inverted_triangle.svg.png";
  49. public const string DefaultIconCollapsed = "/Textures/Interface/Nano/triangle_right.png";
  50. public const string DefaultIconNoChildren = "/Textures/Interface/Nano/triangle_right_hollow.svg.png";
  51. public Texture? IconExpanded;
  52. public Texture? IconCollapsed;
  53. public Texture? IconNoChildren;
  54. /// <summary>
  55. /// If true, tree entries will hide their icon if the texture is set to null. If the icon is hidden then the
  56. /// text of that entry will no longer be aligned with sibling entries that do have an icon.
  57. /// </summary>
  58. public bool HideEmptyIcon
  59. {
  60. get => _hideEmptyIcon;
  61. set => SetHideEmptyIcon(value);
  62. }
  63. private bool _hideEmptyIcon;
  64. public TreeItem? SelectedItem => SelectedIndex == null ? null : Items[SelectedIndex.Value];
  65. /// <summary>
  66. /// If true, a collapsed item will automatically expand when first selected. If false, it has to be manually expanded by
  67. /// clicking on it a second time.
  68. /// </summary>
  69. public bool AutoExpand = true;
  70. public FancyTree()
  71. {
  72. RobustXamlLoader.Load(this);
  73. IoCManager.InjectDependencies(this);
  74. LoadIcons();
  75. }
  76. private void LoadIcons()
  77. {
  78. IconColor = TryGetStyleProperty(StylePropertyIconColor, out Color color) ? color : Color.White;
  79. if (!TryGetStyleProperty(StylePropertyIconExpanded, out IconExpanded))
  80. IconExpanded = _resCache.GetTexture(DefaultIconExpanded);
  81. if (!TryGetStyleProperty(StylePropertyIconCollapsed, out IconCollapsed))
  82. IconCollapsed = _resCache.GetTexture(DefaultIconCollapsed);
  83. if (!TryGetStyleProperty(StylePropertyIconNoChildren, out IconNoChildren))
  84. IconNoChildren = _resCache.GetTexture(DefaultIconNoChildren);
  85. foreach (var item in Body.Children)
  86. {
  87. RecursiveUpdateIcon((TreeItem) item);
  88. }
  89. }
  90. public TreeItem AddItem(TreeItem? parent = null)
  91. {
  92. if (parent != null)
  93. {
  94. if (parent.Tree != this)
  95. throw new ArgumentException("Parent must be owned by this tree.", nameof(parent));
  96. DebugTools.Assert(Items[parent.Index] == parent);
  97. }
  98. var item = new TreeItem()
  99. {
  100. Tree = this,
  101. Index = Items.Count,
  102. };
  103. Items.Add(item);
  104. item.Icon.SetSize = new Vector2(Indentation, Indentation);
  105. item.Button.OnPressed += (_) => OnPressed(item);
  106. if (parent == null)
  107. Body.AddChild(item);
  108. else
  109. {
  110. item.Padding.MinWidth = parent.Padding.MinWidth + Indentation;
  111. parent.Body.AddChild(item);
  112. }
  113. item.UpdateIcon();
  114. QueueRowStyleUpdate();
  115. return item;
  116. }
  117. private void OnPressed(TreeItem item)
  118. {
  119. if (SelectedIndex == item.Index)
  120. {
  121. item.SetExpanded(!item.Expanded);
  122. return;
  123. }
  124. SetSelectedIndex(item.Index);
  125. }
  126. public void SetSelectedIndex(int? value)
  127. {
  128. if (value == null || value < 0 || value >= Items.Count)
  129. value = null;
  130. if (SelectedIndex == value)
  131. return;
  132. SelectedItem?.SetSelected(false);
  133. SelectedIndex = value;
  134. var newSelection = SelectedItem;
  135. if (newSelection != null)
  136. {
  137. newSelection.SetSelected(true);
  138. if (AutoExpand && !newSelection.Expanded)
  139. newSelection.SetExpanded(true);
  140. }
  141. OnSelectedItemChanged?.Invoke(newSelection);
  142. }
  143. /// <summary>
  144. /// Recursively expands or collapse all entries, optionally up to some depth.
  145. /// </summary>
  146. /// <param name="value">Whether to expand or collapse the entries</param>
  147. /// <param name="depth">The recursion depth. If negative, implies no limit. Zero will expand only the top-level entries.</param>
  148. public void SetAllExpanded(bool value, int depth = -1)
  149. {
  150. foreach (var item in Body.Children)
  151. {
  152. RecursiveSetExpanded((TreeItem) item, value, depth);
  153. }
  154. }
  155. public void RecursiveSetExpanded(TreeItem item, bool value, int depth)
  156. {
  157. item.SetExpanded(value);
  158. if (depth == 0)
  159. return;
  160. depth--;
  161. foreach (var child in item.Body.Children)
  162. {
  163. RecursiveSetExpanded((TreeItem) child, value, depth);
  164. }
  165. }
  166. public bool TryGetIndexFromMetadata(object metadata, [NotNullWhen(true)] out int? index)
  167. {
  168. index = null;
  169. foreach (var item in Items)
  170. {
  171. if (item.Metadata?.Equals(metadata) ?? false)
  172. {
  173. index = item.Index;
  174. break;
  175. }
  176. }
  177. return index != null;
  178. }
  179. public void ExpandParentEntries(int index)
  180. {
  181. Control? current = Items[index];
  182. while (current != null)
  183. {
  184. if (current is TreeItem item)
  185. item.SetExpanded(true);
  186. current = current.Parent;
  187. }
  188. }
  189. public void Clear()
  190. {
  191. foreach (var item in Items)
  192. {
  193. item.Dispose();
  194. }
  195. Items.Clear();
  196. Body.Children.Clear();
  197. SelectedIndex = null;
  198. }
  199. public void QueueRowStyleUpdate()
  200. {
  201. _rowStyleUpdateQueued = true;
  202. }
  203. protected override void FrameUpdate(FrameEventArgs args)
  204. {
  205. if (!_rowStyleUpdateQueued)
  206. return;
  207. _rowStyleUpdateQueued = false;
  208. int index = 0;
  209. foreach (var item in Body.Children)
  210. {
  211. RecursivelyUpdateRowStyle((TreeItem) item, ref index);
  212. }
  213. }
  214. private void RecursivelyUpdateRowStyle(TreeItem item, ref int index)
  215. {
  216. if (int.IsOddInteger(index))
  217. {
  218. item.Button.RemoveStyleClass(TreeItem.StyleClassEvenRow);
  219. item.Button.AddStyleClass(TreeItem.StyleClassOddRow);
  220. }
  221. else
  222. {
  223. item.Button.AddStyleClass(TreeItem.StyleClassEvenRow);
  224. item.Button.RemoveStyleClass(TreeItem.StyleClassOddRow);
  225. }
  226. index++;
  227. if (!item.Expanded)
  228. return;
  229. foreach (var child in item.Body.Children)
  230. {
  231. RecursivelyUpdateRowStyle((TreeItem) child, ref index);
  232. }
  233. }
  234. private void SetHideEmptyIcon(bool value)
  235. {
  236. if (value == _hideEmptyIcon)
  237. return;
  238. _hideEmptyIcon = value;
  239. foreach (var item in Body.Children)
  240. {
  241. RecursiveUpdateIcon((TreeItem) item);
  242. }
  243. }
  244. private void RecursiveUpdateIcon(TreeItem item)
  245. {
  246. item.UpdateIcon();
  247. foreach (var child in item.Body.Children)
  248. {
  249. RecursiveUpdateIcon((TreeItem) child);
  250. }
  251. }
  252. protected override void StylePropertiesChanged()
  253. {
  254. LoadIcons();
  255. LineColor = TryGetStyleProperty(StylePropertyLineColor, out Color color) ? color: Color.White;
  256. LineWidth = TryGetStyleProperty(StylePropertyLineWidth, out int width) ? width : 2;
  257. base.StylePropertiesChanged();
  258. }
  259. }