// Copyright © Gamesmiths Guild. #if TOOLS using System; using System.Collections.Generic; using Gamesmiths.Forge.Godot.Resources.Statescript; using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers; using Godot; namespace Gamesmiths.Forge.Godot.Editor.Statescript; /// /// Right-side panel for editing graph variables. Variables are created with a name and type via a creation dialog. /// Once created, only the initial value can be edited. To change name or type, delete and recreate the variable. /// [Tool] internal sealed partial class StatescriptVariablePanel : VBoxContainer, ISerializationListener { private const string ExpandedArraysMetaKey = "_expanded_arrays"; private static readonly Color _variableColor = new(0xe5c07bff); private static readonly Color _highlightColor = new(0x56b6c2ff); private readonly HashSet _expandedArrays = []; private StatescriptGraph? _graph; private VBoxContainer? _variableList; private Button? _addButton; private Window? _creationDialog; private LineEdit? _newNameEdit; private OptionButton? _newTypeDropdown; private CheckBox? _newArrayToggle; private Texture2D? _addIcon; private Texture2D? _removeIcon; private EditorUndoRedoManager? _undoRedo; private string? _selectedVariableName; /// /// Raised when any variable is added, removed, or its value changes. /// public event Action? VariablesChanged; /// /// Raised when an undo/redo action modifies the variable panel, so the dock can auto-expand it. /// public event Action? VariableUndoRedoPerformed; /// /// Raised when the user selects or deselects a variable for highlighting. /// public event Action? VariableHighlightChanged; public override void _Ready() { base._Ready(); _addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons"); _removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons"); SizeFlagsVertical = SizeFlags.ExpandFill; CustomMinimumSize = new Vector2(360, 0); var headerHBox = new HBoxContainer(); AddChild(headerHBox); var titleLabel = new Label { Text = "Graph Variables", SizeFlagsHorizontal = SizeFlags.ExpandFill, }; headerHBox.AddChild(titleLabel); _addButton = new Button { Icon = _addIcon, Flat = true, TooltipText = "Add Variable", CustomMinimumSize = new Vector2(28, 28), }; _addButton.Pressed += OnAddPressed; headerHBox.AddChild(_addButton); var separator = new HSeparator(); AddChild(separator); var scrollContainer = new ScrollContainer { SizeFlagsVertical = SizeFlags.ExpandFill, SizeFlagsHorizontal = SizeFlags.ExpandFill, }; AddChild(scrollContainer); _variableList = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill, }; scrollContainer.AddChild(_variableList); } public override void _ExitTree() { base._ExitTree(); if (_addButton is not null) { _addButton.Pressed -= OnAddPressed; } _creationDialog?.QueueFree(); _creationDialog = null; _newNameEdit = null; _newTypeDropdown = null; _newArrayToggle = null; } public void OnBeforeSerialize() { if (_addButton is not null) { _addButton.Pressed -= OnAddPressed; } if (_variableList is not null) { foreach (Node child in _variableList.GetChildren()) { _variableList.RemoveChild(child); child.Free(); } } _creationDialog?.Free(); _creationDialog = null; _newNameEdit = null; _newTypeDropdown = null; _newArrayToggle = null; } public void OnAfterDeserialize() { if (_addButton is not null) { _addButton.Pressed += OnAddPressed; } RebuildList(); } /// /// Sets the graph to display variables for. /// /// The graph resource, or null to clear. public void SetGraph(StatescriptGraph? graph) { _graph = graph; LoadExpandedArrayState(); RebuildList(); } /// /// Sets the used for undo/redo support. /// /// The undo/redo manager from the editor plugin. public void SetUndoRedo(EditorUndoRedoManager undoRedo) { _undoRedo = undoRedo; } /// /// Rebuilds the variable list UI from the current graph. /// public void RebuildList() { if (_variableList is null) { return; } foreach (Node child in _variableList.GetChildren()) { _variableList.RemoveChild(child); child.Free(); } if (_graph is null) { return; } for (var i = 0; i < _graph.Variables.Count; i++) { AddVariableRow(_graph.Variables[i], i); } } private void SaveExpandedArrayState() { if (_graph is null) { return; } var packed = new string[_expandedArrays.Count]; _expandedArrays.CopyTo(packed); _graph.SetMeta(ExpandedArraysMetaKey, Variant.From(packed)); } private void LoadExpandedArrayState() { _expandedArrays.Clear(); if (_graph?.HasMeta(ExpandedArraysMetaKey) != true) { return; } Variant meta = _graph.GetMeta(ExpandedArraysMetaKey); if (meta.VariantType == Variant.Type.PackedStringArray) { foreach (var name in meta.AsStringArray()) { _expandedArrays.Add(name); } } } private void AddVariableRow(StatescriptGraphVariable variable, int index) { if (_variableList is null) { return; } var rowContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill, }; _variableList.AddChild(rowContainer); var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill, }; rowContainer.AddChild(headerRow); var isSelected = _selectedVariableName == variable.VariableName; var nameButton = new Button { Text = variable.VariableName, SizeFlagsHorizontal = SizeFlags.ExpandFill, Flat = true, ToggleMode = true, ButtonPressed = isSelected, Alignment = HorizontalAlignment.Left, }; Color buttonColor = isSelected ? _highlightColor : _variableColor; nameButton.AddThemeColorOverride("font_color", buttonColor); nameButton.AddThemeColorOverride("font_pressed_color", _highlightColor); nameButton.AddThemeColorOverride("font_hover_color", buttonColor.Lightened(0.2f)); nameButton.AddThemeColorOverride("font_hover_pressed_color", _highlightColor.Lightened(0.2f)); nameButton.AddThemeFontOverride( "font", EditorInterface.Singleton.GetEditorTheme().GetFont("bold", "EditorFonts")); nameButton.Toggled += pressed => { if (pressed) { _selectedVariableName = variable.VariableName; } else if (_selectedVariableName == variable.VariableName) { _selectedVariableName = null; } RebuildList(); VariableHighlightChanged?.Invoke(_selectedVariableName); }; headerRow.AddChild(nameButton); var typeLabel = new Label { Text = $"({StatescriptVariableTypeConverter.GetDisplayName(variable.VariableType)}" + (variable.IsArray ? "[])" : ")"), }; typeLabel.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.6f)); headerRow.AddChild(typeLabel); var capturedIndex = index; var deleteButton = new Button { Icon = _removeIcon, Flat = true, TooltipText = "Remove Variable", CustomMinimumSize = new Vector2(28, 28), }; deleteButton.Pressed += () => OnDeletePressed(capturedIndex); headerRow.AddChild(deleteButton); if (!variable.IsArray) { Control valueEditor = CreateScalarValueEditor(variable); rowContainer.AddChild(valueEditor); } else { VBoxContainer arrayEditor = CreateArrayValueEditor(variable); rowContainer.AddChild(arrayEditor); } rowContainer.AddChild(new HSeparator()); } private void OnAddPressed() { if (_graph is null) { return; } ShowCreationDialog(); } private void ShowCreationDialog() { _creationDialog?.QueueFree(); _creationDialog = new AcceptDialog { Title = "Add Variable", Size = new Vector2I(300, 160), Exclusive = true, }; var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; var nameRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; vBox.AddChild(nameRow); nameRow.AddChild(new Label { Text = "Name:", CustomMinimumSize = new Vector2(60, 0) }); _newNameEdit = new LineEdit { Text = GenerateUniqueName(), SizeFlagsHorizontal = SizeFlags.ExpandFill, }; nameRow.AddChild(_newNameEdit); var typeRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; vBox.AddChild(typeRow); typeRow.AddChild(new Label { Text = "Type:", CustomMinimumSize = new Vector2(60, 0) }); _newTypeDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill }; StatescriptVariableType[] allTypes = StatescriptVariableTypeConverter.GetAllTypes(); for (var t = 0; t < allTypes.Length; t++) { _newTypeDropdown.AddItem(StatescriptVariableTypeConverter.GetDisplayName(allTypes[t]), t); } _newTypeDropdown.Selected = (int)StatescriptVariableType.Int; typeRow.AddChild(_newTypeDropdown); var arrayRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; vBox.AddChild(arrayRow); arrayRow.AddChild(new Label { Text = "Array:", CustomMinimumSize = new Vector2(60, 0) }); _newArrayToggle = new CheckBox(); arrayRow.AddChild(_newArrayToggle); _creationDialog.AddChild(vBox); ((AcceptDialog)_creationDialog).Confirmed += OnCreationConfirmed; AddChild(_creationDialog); _creationDialog.PopupCentered(); } private void OnCreationConfirmed() { if (_graph is null || _newNameEdit is null || _newTypeDropdown is null || _newArrayToggle is null) { return; } var name = _newNameEdit.Text.Trim(); if (string.IsNullOrEmpty(name) || HasVariableNamed(name)) { return; } var selectedIndex = _newTypeDropdown.Selected; if (selectedIndex < 0) { return; } var selectedId = _newTypeDropdown.GetItemId(selectedIndex); var varType = (StatescriptVariableType)selectedId; var newVariable = new StatescriptGraphVariable { VariableName = name, VariableType = varType, IsArray = _newArrayToggle.ButtonPressed, InitialValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(varType), }; if (_undoRedo is not null) { _undoRedo.CreateAction("Add Graph Variable", customContext: _graph); _undoRedo.AddDoMethod(this, MethodName.DoAddVariable, _graph!, newVariable); _undoRedo.AddUndoMethod(this, MethodName.UndoAddVariable, _graph!, newVariable); _undoRedo.CommitAction(); } else { DoAddVariable(_graph, newVariable); } _creationDialog?.QueueFree(); _creationDialog = null; _newNameEdit = null; _newTypeDropdown = null; _newArrayToggle = null; } private void OnDeletePressed(int index) { if (_graph is null || index < 0 || index >= _graph.Variables.Count) { return; } StatescriptGraphVariable variable = _graph.Variables[index]; if (_undoRedo is not null) { _undoRedo.CreateAction("Remove Graph Variable", customContext: _graph); _undoRedo.AddDoMethod(this, MethodName.DoRemoveVariable, _graph!, variable, index); _undoRedo.AddUndoMethod(this, MethodName.UndoRemoveVariable, _graph!, variable, index); _undoRedo.CommitAction(); } else { DoRemoveVariable(_graph, variable, index); } } private void ClearReferencesToVariable(string variableName) { if (_graph is null) { return; } foreach (StatescriptNode node in _graph.Nodes) { foreach (StatescriptNodeProperty binding in node.PropertyBindings) { if (binding.Resolver is VariableResolverResource varRes && varRes.VariableName == variableName) { varRes.VariableName = string.Empty; } } } } private string GenerateUniqueName() { if (_graph is null) { return "variable"; } const string baseName = "variable"; var counter = 1; var name = baseName; while (HasVariableNamed(name)) { name = $"{baseName}_{counter++}"; } return name; } private bool HasVariableNamed(string name) { if (_graph is null) { return false; } foreach (StatescriptGraphVariable variable in _graph.Variables) { if (variable.VariableName == name) { return true; } } return false; } } #endif