1
0

GuidebookWindow.xaml.cs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. using System.Linq;
  2. using Content.Client.Guidebook.RichText;
  3. using Content.Client.UserInterface.ControlExtensions;
  4. using Content.Client.UserInterface.Controls;
  5. using Content.Client.UserInterface.Controls.FancyTree;
  6. using Content.Client.UserInterface.Systems.Info;
  7. using Content.Shared.Guidebook;
  8. using Robust.Client.AutoGenerated;
  9. using Robust.Client.UserInterface.Controls;
  10. using Robust.Client.UserInterface.XAML;
  11. using Robust.Shared.ContentPack;
  12. using Robust.Shared.Prototypes;
  13. namespace Content.Client.Guidebook.Controls;
  14. [GenerateTypedNameReferences]
  15. public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
  16. {
  17. [Dependency] private readonly DocumentParsingManager _parsingMan = default!;
  18. [Dependency] private readonly IResourceManager _resourceManager = default!;
  19. private Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> _entries = new();
  20. private readonly ISawmill _sawmill;
  21. public ProtoId<GuideEntryPrototype> LastEntry;
  22. public GuidebookWindow()
  23. {
  24. RobustXamlLoader.Load(this);
  25. IoCManager.InjectDependencies(this);
  26. _sawmill = Logger.GetSawmill("Guidebook");
  27. Tree.OnSelectedItemChanged += OnSelectionChanged;
  28. SearchBar.OnTextChanged += _ =>
  29. {
  30. HandleFilter();
  31. };
  32. }
  33. public void HandleClick(string link)
  34. {
  35. if (!_entries.TryGetValue(link, out var entry))
  36. return;
  37. if (Tree.TryGetIndexFromMetadata(entry, out var index))
  38. {
  39. Tree.ExpandParentEntries(index.Value);
  40. Tree.SetSelectedIndex(index);
  41. }
  42. else
  43. ShowGuide(entry);
  44. }
  45. private void OnSelectionChanged(TreeItem? item)
  46. {
  47. if (item != null && item.Metadata is GuideEntry entry)
  48. {
  49. ShowGuide(entry);
  50. var isRulesEntry = entry.RuleEntry;
  51. ReturnContainer.Visible = isRulesEntry;
  52. HomeButton.OnPressed += _ => ShowGuide(entry);
  53. }
  54. else
  55. ClearSelectedGuide();
  56. }
  57. public void ClearSelectedGuide()
  58. {
  59. Placeholder.Visible = true;
  60. EntryContainer.Visible = false;
  61. SearchContainer.Visible = false;
  62. EntryContainer.RemoveAllChildren();
  63. }
  64. private void ShowGuide(GuideEntry entry)
  65. {
  66. Scroll.SetScrollValue(default);
  67. Placeholder.Visible = false;
  68. EntryContainer.Visible = true;
  69. SearchBar.Text = "";
  70. EntryContainer.RemoveAllChildren();
  71. using var file = _resourceManager.ContentFileReadText(entry.Text);
  72. SearchContainer.Visible = entry.FilterEnabled;
  73. if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd()))
  74. {
  75. // The guidebook will automatically display the in-guidebook error if it fails
  76. _sawmill.Error($"Failed to parse contents of guide document {entry.Id}.");
  77. }
  78. LastEntry = entry.Id;
  79. }
  80. public void UpdateGuides(
  81. Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> entries,
  82. List<ProtoId<GuideEntryPrototype>>? rootEntries = null,
  83. ProtoId<GuideEntryPrototype>? forceRoot = null,
  84. ProtoId<GuideEntryPrototype>? selected = null)
  85. {
  86. _entries = entries;
  87. RepopulateTree(rootEntries, forceRoot);
  88. ClearSelectedGuide();
  89. Split.State = SplitContainer.SplitState.Auto;
  90. if (entries.Count == 1)
  91. {
  92. TreeBox.Visible = false;
  93. Split.ResizeMode = SplitContainer.SplitResizeMode.NotResizable;
  94. selected = entries.Keys.First();
  95. }
  96. else
  97. {
  98. TreeBox.Visible = true;
  99. Split.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
  100. }
  101. if (selected != null)
  102. {
  103. var item = Tree.Items.FirstOrDefault(x => x.Metadata is GuideEntry entry && entry.Id == selected);
  104. Tree.SetSelectedIndex(item?.Index);
  105. }
  106. }
  107. private IEnumerable<GuideEntry> GetSortedEntries(List<ProtoId<GuideEntryPrototype>>? rootEntries)
  108. {
  109. if (rootEntries == null)
  110. {
  111. HashSet<ProtoId<GuideEntryPrototype>> entries = new(_entries.Keys);
  112. foreach (var entry in _entries.Values)
  113. {
  114. entries.ExceptWith(entry.Children);
  115. }
  116. rootEntries = entries.ToList();
  117. }
  118. // Only roots need to be sorted.
  119. // As defined in the SS14 Dev Wiki, children are already sorted based on their child field order within their parent's prototype definition.
  120. // Roots are sorted by priority. If there is no defined priority for a root then it is by definition sorted undefined.
  121. return rootEntries
  122. .Select(rootEntryId => _entries[rootEntryId])
  123. .OrderBy(rootEntry => rootEntry.Priority)
  124. .ThenBy(rootEntry => Loc.GetString(rootEntry.Name));
  125. }
  126. private void RepopulateTree(List<ProtoId<GuideEntryPrototype>>? roots = null,
  127. ProtoId<GuideEntryPrototype>? forcedRoot = null)
  128. {
  129. Tree.Clear();
  130. HashSet<ProtoId<GuideEntryPrototype>> addedEntries = new();
  131. var parent = forcedRoot == null ? null : AddEntry(forcedRoot.Value, null, addedEntries);
  132. foreach (var entry in GetSortedEntries(roots))
  133. {
  134. AddEntry(entry.Id, parent, addedEntries);
  135. }
  136. Tree.SetAllExpanded(true);
  137. }
  138. private TreeItem? AddEntry(ProtoId<GuideEntryPrototype> id,
  139. TreeItem? parent,
  140. HashSet<ProtoId<GuideEntryPrototype>> addedEntries)
  141. {
  142. if (!_entries.TryGetValue(id, out var entry))
  143. return null;
  144. if (!addedEntries.Add(id))
  145. {
  146. // TODO GUIDEBOOK Maybe allow duplicate entries?
  147. // E.g., for adding medicine under both chemicals & the chemist job
  148. Logger.Error($"Adding duplicate guide entry: {id}");
  149. return null;
  150. }
  151. var rulesProto = UserInterfaceManager.GetUIController<InfoUIController>().GetCoreRuleEntry();
  152. if (entry.RuleEntry && entry.Id != rulesProto.Id)
  153. return null;
  154. var item = Tree.AddItem(parent);
  155. item.Metadata = entry;
  156. var name = Loc.GetString(entry.Name);
  157. item.Label.Text = name;
  158. foreach (var child in entry.Children)
  159. {
  160. AddEntry(child, item, addedEntries);
  161. }
  162. return item;
  163. }
  164. private void HandleFilter()
  165. {
  166. var emptySearch = SearchBar.Text.Trim().Length == 0;
  167. if (Tree.SelectedItem != null && Tree.SelectedItem.Metadata is GuideEntry entry && entry.FilterEnabled)
  168. {
  169. var foundElements = EntryContainer.GetSearchableControls();
  170. foreach (var element in foundElements)
  171. {
  172. element.SetHiddenState(true, SearchBar.Text.Trim());
  173. }
  174. }
  175. }
  176. }