using System.Linq; using Content.Client.UserInterface.ControlExtensions; using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls.FancyTree; using Content.Client.UserInterface.RichText; using Content.Client.UserInterface.Systems.Info; using Content.Shared.Guidebook; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; namespace Content.Client.Guidebook.Controls; [GenerateTypedNameReferences] public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IAnchorClickHandler { [Dependency] private DocumentParsingManager _parsingMan = default!; [Dependency] private IResourceManager _resourceManager = default!; private Dictionary, GuideEntry> _entries = []; private readonly ISawmill _sawmill; public ProtoId? Selected { get; private set; } public GuidebookWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _sawmill = Logger.GetSawmill("guidebook"); Tree.OnSelectedItemChanged += OnSelectionChanged; TableOfContents.OnSelectedItemChanged += OnTableOfContentsSelectionChanged; SearchBar.OnTextChanged += _ => { HandleFilter(); }; } public void HandleClick(string link) { if (!_entries.TryGetValue(link, out var entry)) return; if (Tree.TryGetIndexFromMetadata(entry, out var index)) { Tree.ExpandParentEntries(index.Value); Tree.SetSelectedIndex(index); } else ShowGuide(entry); } public void HandleAnchor(IPrototypeLinkControl prototypeLinkControl) { var prototype = prototypeLinkControl.LinkedPrototype; if (prototype == null) return; var (linkableControls, _) = GetLinkableControlsAndLinks(EntryContainer); foreach (var linkableControl in linkableControls) { if (linkableControl.RepresentedPrototype != prototype) continue; if (linkableControl is not Control control) return; // Check if the target item is currently filtered out if (!control.Visible) control.Visible = true; UserInterfaceManager.DeferAction(() => { if (control.GetControlScrollPosition() is not { } position) return; Scroll.HScrollTarget = position.X; Scroll.VScrollTarget = position.Y; }); break; } } private void OnSelectionChanged(TreeItem? item) { if (item != null && item.Metadata is GuideEntry entry) { // do nothing if the guide is the same as the currently selected one if (entry.Id == Selected) return; ShowGuide(entry); var isRulesEntry = entry.RuleEntry; ReturnContainer.Visible = isRulesEntry; HomeButton.OnPressed += _ => ShowGuide(entry); } else ClearSelectedGuide(); } private void OnTableOfContentsSelectionChanged(TreeItem? item) { if (item is null || item.Metadata is not Label entry) return; UserInterfaceManager.DeferAction(() => { if (entry.GetControlScrollPosition() is not { } position) return; Scroll.HScrollTarget = position.X; Scroll.VScrollTarget = position.Y; }); } public void ClearSelectedGuide() { Placeholder.Visible = true; EntryContainer.Visible = false; SearchContainer.Visible = false; EntryContainer.RemoveAllChildren(); Selected = null; } private void ShowGuide(GuideEntry entry) { Scroll.SetScrollValue(default); Placeholder.Visible = false; EntryContainer.Visible = true; SearchBar.Text = ""; EntryContainer.RemoveAllChildren(); using var file = _resourceManager.ContentFileReadText(entry.Text); SearchContainer.Visible = entry.FilterEnabled; if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd())) { // The guidebook will automatically display the in-guidebook error if it fails _sawmill.Error($"Failed to parse contents of guide document {entry.Id}."); } Selected = entry.Id; var (linkableControls, linkControls) = GetLinkableControlsAndLinks(EntryContainer); HashSet availablePrototypeLinks = []; foreach (var linkableControl in linkableControls) { var prototype = linkableControl.RepresentedPrototype; if (prototype != null) availablePrototypeLinks.Add(prototype); } foreach (var linkControl in linkControls) { var prototype = linkControl.LinkedPrototype; if (prototype != null && availablePrototypeLinks.Contains(prototype)) linkControl.EnablePrototypeLink(); } RepopulateTableOfContents(); } private int? HeadingDepth(Label control) { if (control.StyleClasses.Contains("LabelHeadingBigger")) return 1; else if (control.StyleClasses.Contains("LabelHeading")) return 2; else if (control.StyleClasses.Contains("LabelKeyText")) return 3; return null; } private void RepopulateTableOfContents() { TableOfContents.Clear(); var firstEntry = TableOfContents.AddItem(null); firstEntry.Label.Text = Loc.GetString("guidebook-toc-header"); var labels = EntryContainer.GetControlOfType