/// Control for embedding a microwave recipe into a guidebook.
/// </summary>
[UsedImplicitly, GenerateTypedNameReferences]
-public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, ISearchableControl
+public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
+ public IPrototype? RepresentedPrototype { get; private set; }
+
public GuideMicrowaveEmbed()
{
RobustXamlLoader.Load(this);
{
var entity = _prototype.Index<EntityPrototype>(recipe.Result);
+ RepresentedPrototype = entity;
+
IconContainer.AddChild(new GuideEntityEmbed(recipe.Result, false, false));
ResultName.SetMarkup(entity.Name);
ResultDescription.SetMarkup(entity.Description);
solidNameMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-solid-name-display", ("ingredient", ingredient.Name)));
solidNameMsg.Pop();
- var solidNameLabel = new RichTextLabel();
+ var solidNameLabel = new GuidebookRichPrototypeLink();
solidNameLabel.SetMessage(solidNameMsg);
+ solidNameLabel.LinkedPrototype = ingredient;
IngredientsGrid.AddChild(solidNameLabel);
liquidColorMsg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-reagent-color-display", ("color", reagent.SubstanceColor)));
liquidColorMsg.Pop();
- var liquidColorLabel = new RichTextLabel();
+ var liquidColorLabel = new GuidebookRichPrototypeLink();
liquidColorLabel.SetMessage(liquidColorMsg);
liquidColorLabel.HorizontalAlignment = Control.HAlignment.Center;
+ liquidColorLabel.LinkedPrototype = reagent;
IngredientsGrid.AddChild(liquidColorLabel);
/// Control for embedding a reagent into a guidebook.
/// </summary>
[UsedImplicitly, GenerateTypedNameReferences]
-public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISearchableControl
+public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl
{
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private readonly ChemistryGuideDataSystem _chemistryGuideData;
+ public IPrototype? RepresentedPrototype { get; private set; }
+
public GuideReagentEmbed()
{
RobustXamlLoader.Load(this);
private void GenerateControl(ReagentPrototype reagent)
{
+ RepresentedPrototype = reagent;
+
NameBackground.PanelOverride = new StyleBoxFlat
{
BackgroundColor = reagent.SubstanceColor
HorizontalExpand="True"
Margin="0 0 0 5">
<BoxContainer Orientation="Horizontal">
- <BoxContainer Name="ReactantsContainer" Orientation="Vertical" HorizontalExpand="True"
+ <BoxContainer Orientation="Vertical" HorizontalExpand="True"
VerticalAlignment="Center">
- <RichTextLabel Name="ReactantsLabel"
- HorizontalAlignment="Center"
- VerticalAlignment="Center"
- Access="Public"
- Visible="False" />
+ <BoxContainer Name="ReactantsContainer" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Vertical">
+ <!-- Reactants will be added as children here -->
+ </BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextureRect TexturePath="/Textures/Interface/Misc/beakerlarge.png"
Margin="2 0 0 0" />
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalAlignment="Center">
- <RichTextLabel Name="ProductsLabel"
- HorizontalAlignment="Center"
- VerticalAlignment="Center"
- Access="Public"
- Visible="False" />
+ <BoxContainer Name="ProductsContainer" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Vertical">
+ <!-- Products will be added as children here -->
+ </BoxContainer>
</BoxContainer>
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
public GuideReagentReaction(ReactionPrototype prototype, IPrototypeManager protoMan, IEntitySystemManager sysMan) : this(protoMan)
{
- var reactantsLabel = ReactantsLabel;
- SetReagents(prototype.Reactants, ref reactantsLabel, protoMan);
- var productLabel = ProductsLabel;
+ Container container = ReactantsContainer;
+ SetReagents(prototype.Reactants, ref container, protoMan);
+ Container productContainer = ProductsContainer;
var products = new Dictionary<string, FixedPoint2>(prototype.Products);
foreach (var (reagent, reactantProto) in prototype.Reactants)
{
if (reactantProto.Catalyst)
products.Add(reagent, reactantProto.Amount);
}
- SetReagents(products, ref productLabel, protoMan);
+ SetReagents(products, ref productContainer, protoMan, false);
var mixingCategories = new List<MixingCategoryPrototype>();
if (prototype.MixingCategories != null)
entContainer.AddChild(nameLabel);
ReactantsContainer.AddChild(entContainer);
- var productLabel = ProductsLabel;
- SetReagents(solution.Contents, ref productLabel, protoMan);
+ Container productContainer = ProductsContainer;
+ SetReagents(solution.Contents, ref productContainer, protoMan, false);
SetMixingCategory(categories, null, sysMan);
}
IPrototypeManager protoMan,
IEntitySystemManager sysMan) : this(protoMan)
{
- ReactantsLabel.Visible = true;
- ReactantsLabel.SetMarkup(Loc.GetString("guidebook-reagent-sources-gas-wrapper",
+ var label = new RichTextLabel();
+ label.SetMarkup(Loc.GetString("guidebook-reagent-sources-gas-wrapper",
("name", Loc.GetString(prototype.Name).ToLower())));
+ ReactantsContainer.Visible = true;
+ ReactantsContainer.AddChild(label);
+
if (prototype.Reagent != null)
{
var quantity = new Dictionary<string, FixedPoint2>
{
{ prototype.Reagent, FixedPoint2.New(0.21f) }
};
- var productLabel = ProductsLabel;
- SetReagents(quantity, ref productLabel, protoMan);
+ Container productContainer = ProductsContainer;
+ SetReagents(quantity, ref productContainer, protoMan, false);
}
SetMixingCategory(categories, null, sysMan);
}
- private void SetReagents(List<ReagentQuantity> reagents, ref RichTextLabel label, IPrototypeManager protoMan)
+ private void SetReagents(List<ReagentQuantity> reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true)
{
var amounts = new Dictionary<string, FixedPoint2>();
foreach (var (reagent, quantity) in reagents)
{
amounts.Add(reagent.Prototype, quantity);
}
- SetReagents(amounts, ref label, protoMan);
+ SetReagents(amounts, ref container, protoMan, addLinks);
}
private void SetReagents(
Dictionary<string, ReactantPrototype> reactants,
- ref RichTextLabel label,
- IPrototypeManager protoMan)
+ ref Container container,
+ IPrototypeManager protoMan,
+ bool addLinks = true)
{
var amounts = new Dictionary<string, FixedPoint2>();
foreach (var (reagent, reactantPrototype) in reactants)
{
amounts.Add(reagent, reactantPrototype.Amount);
}
- SetReagents(amounts, ref label, protoMan);
+ SetReagents(amounts, ref container, protoMan, addLinks);
}
[PublicAPI]
private void SetReagents(
Dictionary<ProtoId<MixingCategoryPrototype>, ReactantPrototype> reactants,
- ref RichTextLabel label,
- IPrototypeManager protoMan)
+ ref Container container,
+ IPrototypeManager protoMan,
+ bool addLinks = true)
{
var amounts = new Dictionary<string, FixedPoint2>();
foreach (var (reagent, reactantPrototype) in reactants)
{
amounts.Add(reagent, reactantPrototype.Amount);
}
- SetReagents(amounts, ref label, protoMan);
+ SetReagents(amounts, ref container, protoMan, addLinks);
}
- private void SetReagents(Dictionary<string, FixedPoint2> reagents, ref RichTextLabel label, IPrototypeManager protoMan)
+ private void SetReagents(Dictionary<string, FixedPoint2> reagents, ref Container container, IPrototypeManager protoMan, bool addLinks = true)
{
- var msg = new FormattedMessage();
- var reagentCount = reagents.Count;
- var i = 0;
foreach (var (product, amount) in reagents.OrderByDescending(p => p.Value))
{
+ var productProto = protoMan.Index<ReagentPrototype>(product);
+ var msg = new FormattedMessage();
msg.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-recipes-reagent-display",
- ("reagent", protoMan.Index<ReagentPrototype>(product).LocalizedName), ("ratio", amount)));
- i++;
- if (i < reagentCount)
- msg.PushNewline();
+ ("reagent", productProto.LocalizedName), ("ratio", amount)));
+
+ var label = new GuidebookRichPrototypeLink();
+ if (addLinks)
+ label.LinkedPrototype = productProto;
+ label.SetMessage(msg);
+ container.AddChild(label);
}
- msg.Pop();
- label.SetMessage(msg);
- label.Visible = true;
+ container.Visible = true;
}
private void SetMixingCategory(IReadOnlyList<ProtoId<MixingCategoryPrototype>> mixingCategories, ReactionPrototype? prototype, IEntitySystemManager sysMan)
--- /dev/null
+using Content.Client.Guidebook.RichText;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Input;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using Content.Client.UserInterface.ControlExtensions;
+
+namespace Content.Client.Guidebook.Controls;
+
+/// <summary>
+/// A RichTextLabel which is a link to a specified IPrototype.
+/// The link is activated by the owner if the prototype is represented
+/// somewhere in the same document.
+/// </summary>
+public sealed class GuidebookRichPrototypeLink : Control, IPrototypeLinkControl
+{
+ private bool _linkActive = false;
+ private FormattedMessage? _message;
+ private readonly RichTextLabel _richTextLabel;
+
+ public void EnablePrototypeLink()
+ {
+ if (_message == null)
+ return;
+
+ _linkActive = true;
+
+ DefaultCursorShape = CursorShape.Hand;
+
+ _richTextLabel.SetMessage(_message, null, TextLinkTag.LinkColor);
+ }
+
+ public GuidebookRichPrototypeLink() : base()
+ {
+ MouseFilter = MouseFilterMode.Pass;
+ OnKeyBindDown += HandleClick;
+ _richTextLabel = new RichTextLabel();
+ AddChild(_richTextLabel);
+ }
+
+ public void SetMessage(FormattedMessage message)
+ {
+ _message = message;
+ _richTextLabel.SetMessage(_message);
+ }
+
+ public IPrototype? LinkedPrototype { get; set; }
+
+ private void HandleClick(GUIBoundKeyEventArgs args)
+ {
+ if (!_linkActive)
+ return;
+
+ if (args.Function != EngineKeyFunctions.UIClick)
+ return;
+
+ if (this.TryGetParentHandler<IAnchorClickHandler>(out var handler))
+ {
+ handler.HandleAnchor(this);
+ args.Handle();
+ }
+ else
+ Logger.Warning("Warning! No valid IAnchorClickHandler found.");
+ }
+}
+
+public interface IAnchorClickHandler
+{
+ public void HandleAnchor(IPrototypeLinkControl prototypeLinkControl);
+}
+using System.Diagnostics;
using System.Linq;
using Content.Client.Guidebook.RichText;
using Content.Client.UserInterface.ControlExtensions;
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;
namespace Content.Client.Guidebook.Controls;
[GenerateTypedNameReferences]
-public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
+public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IAnchorClickHandler
{
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
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)
}
LastEntry = entry.Id;
+
+ var (linkableControls, linkControls) = GetLinkableControlsAndLinks(EntryContainer);
+
+ HashSet<IPrototype> availablePrototypeLinks = new();
+ 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();
+ }
}
public void UpdateGuides(
}
}
}
+
+ private static (List<IPrototypeRepresentationControl>, List<IPrototypeLinkControl>) GetLinkableControlsAndLinks(Control parent)
+ {
+ List<IPrototypeRepresentationControl> linkableList = new();
+ List<IPrototypeLinkControl> linkList = new();
+
+ foreach (var child in parent.Children)
+ {
+ var hasChildren = child.ChildCount > 0;
+
+ if (child is IPrototypeLinkControl linkChild)
+ linkList.Add(linkChild);
+ else if (child is IPrototypeRepresentationControl linkableChild)
+ linkableList.Add(linkableChild);
+
+ if (!hasChildren)
+ continue;
+
+ var (childLinkableList, childLinkList) = GetLinkableControlsAndLinks(child);
+
+ linkableList.AddRange(childLinkableList);
+ linkList.AddRange(childLinkList);
+ }
+
+ return (linkableList, linkList);
+ }
}
--- /dev/null
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Guidebook.Controls;
+
+/// <summary>
+/// Interface for controls which represent a Prototype
+/// These can be linked to from a IPrototypeLinkControl
+/// </summary>
+public interface IPrototypeRepresentationControl
+{
+ // The prototype that this control represents
+ public IPrototype? RepresentedPrototype { get; }
+}
+
+/// <summary>
+/// Interface for controls which can be clicked to navigate
+/// to a specified prototype representation on the same page.
+/// </summary>
+public interface IPrototypeLinkControl
+{
+ // This control is a link to the specified prototype
+ public IPrototype? LinkedPrototype { get; }
+
+ // Initially the link will not be enabled,
+ // the owner can enable the link once there is a valid target
+ // for the Prototype link.
+ public void EnablePrototypeLink();
+}
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Input;
using Robust.Shared.Utility;
+using Content.Client.UserInterface.ControlExtensions;
namespace Content.Client.Guidebook.RichText;
[UsedImplicitly]
public sealed class TextLinkTag : IMarkupTag
{
+ public static Color LinkColor => Color.CornflowerBlue;
+
public string Name => "textlink";
public Control? Control;
label.Text = text;
label.MouseFilter = Control.MouseFilterMode.Stop;
- label.FontColorOverride = Color.CornflowerBlue;
+ label.FontColorOverride = LinkColor;
label.DefaultCursorShape = Control.CursorShape.Hand;
label.OnMouseEntered += _ => label.FontColorOverride = Color.LightSkyBlue;
if (Control == null)
return;
- var current = Control;
- while (current != null)
- {
- current = current.Parent;
-
- if (current is not ILinkClickHandler handler)
- continue;
+ if (Control.TryGetParentHandler<ILinkClickHandler>(out var handler))
handler.HandleClick(link);
- return;
- }
- Logger.Warning($"Warning! No valid ILinkClickHandler found.");
+ else
+ Logger.Warning("Warning! No valid ILinkClickHandler found.");
}
}
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
using Content.Client.Guidebook.Controls;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
return controlList;
}
+ /// <summary>
+ /// Search the control’s tree for a parent node of type T
+ /// E.g. to find the control implementing some event handling interface.
+ /// </summary>
+ public static bool TryGetParentHandler<T>(this Control child, [NotNullWhen(true)] out T? result)
+ {
+ for (var control = child; control is not null; control = control.Parent)
+ {
+ if (control is not T handler)
+ continue;
+
+ result = handler;
+ return true;
+ }
+
+ result = default;
+ return false;
+ }
+
+ /// <summary>
+ /// Find the control’s offset relative to its closest ScrollContainer
+ /// Returns null if the control is not in the tree or not visible.
+ /// </summary>
+ public static Vector2? GetControlScrollPosition(this Control child)
+ {
+ if (!child.VisibleInTree)
+ return null;
+
+ var position = new Vector2();
+ var control = child;
+
+ while (control is not null)
+ {
+ // The scroll container's direct child is re-positioned while scrolling,
+ // so we need to ignore its position.
+ if (control.Parent is ScrollContainer)
+ break;
+
+ position += control.Position;
+
+ control = control.Parent;
+ }
+
+ return position;
+ }
+
public static bool ChildrenContainText(this Control parent, string search)
{
var labels = parent.GetControlOfType<Label>();