// Copyright © Gamesmiths Guild. #if TOOLS using System; using System.Collections.Generic; using Gamesmiths.Forge.Godot.Resources.Statescript; using Gamesmiths.Forge.Statescript.Nodes; using Godot; namespace Gamesmiths.Forge.Godot.Editor.Statescript; /// /// A popup dialog for adding Statescript nodes to a graph. Features a search bar, categorized tree view, description /// panel, and Create/Cancel buttons. /// [Tool] internal sealed partial class StatescriptAddNodeDialog : ConfirmationDialog, ISerializationListener { private const int DialogWidth = 244; private const int DialogHeight = 400; private static readonly string _exitNodeDescription = new ExitNode().Description; private LineEdit? _searchBar; private MenuButton? _expandCollapseButton; private PopupMenu? _expandCollapsePopup; private Tree? _tree; private Label? _descriptionHeader; private RichTextLabel? _descriptionLabel; private bool _isFiltering; /// /// Raised when the user confirms node creation. The first argument is the selected /// (null for Exit node), the second is the /// , and the third is the graph-local position to place the node. /// public event Action? NodeCreationRequested; /// /// Gets or sets the graph-local position where the new node should be placed. /// public Vector2 SpawnPosition { get; set; } public StatescriptAddNodeDialog() { Title = "Add Statescript Node"; Exclusive = true; Unresizable = false; MinSize = new Vector2I(DialogWidth, DialogHeight); Size = new Vector2I(DialogWidth, DialogHeight); OkButtonText = "Create"; } public override void _Ready() { base._Ready(); Transient = true; TransientToFocused = true; BuildUI(); PopulateTree(); GetOkButton().Disabled = true; Confirmed += OnConfirmed; Canceled += OnCanceled; } public override void _ExitTree() { base._ExitTree(); DisconnectSignals(); } public void OnBeforeSerialize() { DisconnectSignals(); NodeCreationRequested = null; } public void OnAfterDeserialize() { ConnectSignals(); } /// /// Shows the dialog at the specified screen position, resets search and selection state. /// /// The graph-local position where the node should be created. /// The screen position to show the dialog at. public void ShowAtPosition(Vector2 spawnPosition, Vector2I screenPosition) { SpawnPosition = spawnPosition; if (_isFiltering) { _searchBar?.Clear(); PopulateTree(); } else { _searchBar?.Clear(); } _tree?.DeselectAll(); GetOkButton().Disabled = true; UpdateDescription(null); Position = screenPosition; Size = new Vector2I(DialogWidth, DialogHeight); Popup(); _searchBar?.GrabFocus(); } private static void SetAllCollapsed(TreeItem root, bool collapsed) { TreeItem? child = root.GetFirstChild(); while (child is not null) { child.Collapsed = collapsed; SetAllCollapsed(child, collapsed); child = child.GetNext(); } } private void BuildUI() { var vBox = new VBoxContainer { SizeFlagsVertical = Control.SizeFlags.ExpandFill, SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, }; AddChild(vBox); var searchHBox = new HBoxContainer(); vBox.AddChild(searchHBox); _searchBar = new LineEdit { PlaceholderText = "Search...", SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, ClearButtonEnabled = true, RightIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Search", "EditorIcons"), }; _searchBar.TextChanged += OnSearchTextChanged; searchHBox.AddChild(_searchBar); _expandCollapseButton = new MenuButton { Flat = true, Icon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Tools", "EditorIcons"), TooltipText = "Options", }; _expandCollapsePopup = _expandCollapseButton.GetPopup(); _expandCollapsePopup.AddItem("Expand All", 0); _expandCollapsePopup.AddItem("Collapse All", 1); _expandCollapsePopup.IdPressed += OnExpandCollapseMenuPressed; searchHBox.AddChild(_expandCollapseButton); _tree = new Tree { SizeFlagsVertical = Control.SizeFlags.ExpandFill, SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, HideRoot = true, SelectMode = Tree.SelectModeEnum.Single, }; _tree.ItemSelected += OnTreeItemSelected; _tree.ItemActivated += OnTreeItemActivated; vBox.AddChild(_tree); _descriptionHeader = new Label { Text = "Description:", }; vBox.AddChild(_descriptionHeader); _descriptionLabel = new RichTextLabel { BbcodeEnabled = true, ScrollActive = true, CustomMinimumSize = new Vector2(0, 70), }; vBox.AddChild(_descriptionLabel); } private void PopulateTree(string filter = "") { if (_tree is null) { return; } _isFiltering = !string.IsNullOrWhiteSpace(filter); _tree.Clear(); TreeItem root = _tree.CreateItem(); IReadOnlyList discoveredTypes = StatescriptNodeDiscovery.GetDiscoveredNodeTypes(); var filterLower = filter.ToLowerInvariant(); TreeItem? actionCategory = null; TreeItem? conditionCategory = null; TreeItem? stateCategory = null; foreach (StatescriptNodeDiscovery.NodeTypeInfo typeInfo in discoveredTypes) { if (_isFiltering && !typeInfo.DisplayName.Contains(filterLower, StringComparison.OrdinalIgnoreCase)) { continue; } TreeItem categoryItem; switch (typeInfo.NodeType) { case StatescriptNodeType.Action: actionCategory ??= CreateCategoryItem(root, "Action"); categoryItem = actionCategory; break; case StatescriptNodeType.Condition: conditionCategory ??= CreateCategoryItem(root, "Condition"); categoryItem = conditionCategory; break; case StatescriptNodeType.State: stateCategory ??= CreateCategoryItem(root, "State"); categoryItem = stateCategory; break; default: continue; } TreeItem item = _tree.CreateItem(categoryItem); item.SetText(0, typeInfo.DisplayName); item.SetMetadata(0, typeInfo.RuntimeTypeName); } if (!_isFiltering || "exit".Contains(filterLower, StringComparison.OrdinalIgnoreCase) || "exit node".Contains(filterLower, StringComparison.OrdinalIgnoreCase)) { TreeItem exitItem = _tree.CreateItem(root); exitItem.SetText(0, "Exit"); exitItem.SetMetadata(0, "__exit__"); } SetAllCollapsed(root, !_isFiltering); UpdateDescription(null); } private TreeItem CreateCategoryItem(TreeItem parent, string name) { TreeItem item = _tree!.CreateItem(parent); item.SetText(0, name); item.SetSelectable(0, false); return item; } private void OnSearchTextChanged(string newText) { PopulateTree(newText); GetOkButton().Disabled = true; } private void OnExpandCollapseMenuPressed(long id) { if (_tree is null) { return; } TreeItem? root = _tree.GetRoot(); if (root is null) { return; } SetAllCollapsed(root, id != 0); } private void OnTreeItemSelected() { if (_tree is null) { return; } TreeItem? selected = _tree.GetSelected(); if (selected?.IsSelectable(0) != true) { GetOkButton().Disabled = true; UpdateDescription(null); return; } GetOkButton().Disabled = false; var metadata = selected.GetMetadata(0).AsString(); UpdateDescription(metadata); } private void OnTreeItemActivated() { if (_tree?.GetSelected() is not null && !GetOkButton().Disabled) { OnConfirmed(); Hide(); } } private void OnConfirmed() { if (_tree is null) { return; } TreeItem? selected = _tree.GetSelected(); if (selected?.IsSelectable(0) != true) { return; } var metadata = selected.GetMetadata(0).AsString(); if (metadata == "__exit__") { NodeCreationRequested?.Invoke(null, StatescriptNodeType.Exit, SpawnPosition); } else { StatescriptNodeDiscovery.NodeTypeInfo? typeInfo = StatescriptNodeDiscovery.FindByRuntimeTypeName(metadata); if (typeInfo is not null) { NodeCreationRequested?.Invoke(typeInfo, typeInfo.NodeType, SpawnPosition); } } } private void OnCanceled() { // Method intentionally left blank, no action needed on cancel. } private void UpdateDescription(string? runtimeTypeName) { if (_descriptionLabel is null) { return; } if (runtimeTypeName is null) { _descriptionLabel.Text = string.Empty; return; } if (runtimeTypeName == "__exit__") { _descriptionLabel.Text = _exitNodeDescription; return; } StatescriptNodeDiscovery.NodeTypeInfo? typeInfo = StatescriptNodeDiscovery.FindByRuntimeTypeName(runtimeTypeName); _descriptionLabel.Text = typeInfo?.Description ?? string.Empty; } private void DisconnectSignals() { Confirmed -= OnConfirmed; Canceled -= OnCanceled; if (_searchBar is not null) { _searchBar.TextChanged -= OnSearchTextChanged; } if (_expandCollapsePopup is not null) { _expandCollapsePopup.IdPressed -= OnExpandCollapseMenuPressed; } if (_tree is not null) { _tree.ItemSelected -= OnTreeItemSelected; _tree.ItemActivated -= OnTreeItemActivated; } } private void ConnectSignals() { Confirmed += OnConfirmed; Canceled += OnCanceled; if (_searchBar is not null) { _searchBar.TextChanged += OnSearchTextChanged; } if (_expandCollapsePopup is not null) { _expandCollapsePopup.IdPressed += OnExpandCollapseMenuPressed; } if (_tree is not null) { _tree.ItemSelected += OnTreeItemSelected; _tree.ItemActivated += OnTreeItemActivated; } } } #endif