Files
MovementTests/addons/forge/editor/statescript/StatescriptAddNodeDialog.cs
Minimata 1d856fd937
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 26s
Create tag and build when new code gets to main / Export (push) Successful in 5m42s
Replicated the weapon flying tick setup using resources
2026-04-07 16:32:26 +02:00

426 lines
9.5 KiB
C#

// 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;
/// <summary>
/// A popup dialog for adding Statescript nodes to a graph. Features a search bar, categorized tree view, description
/// panel, and Create/Cancel buttons.
/// </summary>
[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;
/// <summary>
/// Raised when the user confirms node creation. The first argument is the selected
/// <see cref="StatescriptNodeDiscovery.NodeTypeInfo"/> (null for Exit node), the second is the
/// <see cref="StatescriptNodeType"/>, and the third is the graph-local position to place the node.
/// </summary>
public event Action<StatescriptNodeDiscovery.NodeTypeInfo?, StatescriptNodeType, Vector2>? NodeCreationRequested;
/// <summary>
/// Gets or sets the graph-local position where the new node should be placed.
/// </summary>
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();
}
/// <summary>
/// Shows the dialog at the specified screen position, resets search and selection state.
/// </summary>
/// <param name="spawnPosition">The graph-local position where the node should be created.</param>
/// <param name="screenPosition">The screen position to show the dialog at.</param>
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<StatescriptNodeDiscovery.NodeTypeInfo> 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