// Copyright © Gamesmiths Guild. #if TOOLS using System.Collections.Generic; using Gamesmiths.Forge.Godot.Resources; using Gamesmiths.Forge.Godot.Resources.Statescript; using Godot; using Godot.Collections; namespace Gamesmiths.Forge.Godot.Editor.Statescript; /// /// Custom that renders the array using the /// same polished value-editor controls as the graph variable panel. /// [Tool] internal sealed partial class SharedVariableSetEditorProperty : EditorProperty, ISerializationListener { private static readonly Color _variableColor = new(0xe5c07bff); private readonly HashSet _expandedArrays = []; private EditorUndoRedoManager? _undoRedo; private VBoxContainer? _root; private VBoxContainer? _variableList; private Button? _addButton; private AcceptDialog? _creationDialog; private LineEdit? _newNameEdit; private OptionButton? _newTypeDropdown; private CheckBox? _newArrayToggle; private Texture2D? _addIcon; private Texture2D? _removeIcon; /// /// Sets the used for undo/redo support. /// /// The undo/redo manager from the editor plugin. public void SetUndoRedo(EditorUndoRedoManager? undoRedo) { _undoRedo = undoRedo; } public override void _Ready() { _addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons"); _removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons"); var backgroundPanel = new PanelContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill, }; var panelStyle = new StyleBoxFlat { BgColor = EditorInterface.Singleton.GetEditorTheme().GetColor("base_color", "Editor"), ContentMarginLeft = 6, ContentMarginRight = 6, ContentMarginTop = 4, ContentMarginBottom = 4, CornerRadiusTopLeft = 3, CornerRadiusTopRight = 3, CornerRadiusBottomLeft = 3, CornerRadiusBottomRight = 3, }; backgroundPanel.AddThemeStyleboxOverride("panel", panelStyle); AddChild(backgroundPanel); SetBottomEditor(backgroundPanel); _root = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; backgroundPanel.AddChild(_root); var headerHBox = new HBoxContainer(); _root.AddChild(headerHBox); _addButton = new Button { Text = "Add Variable", SizeFlagsHorizontal = SizeFlags.ExpandFill, }; _addButton.Pressed += OnAddPressed; headerHBox.AddChild(_addButton); _root.AddChild(new HSeparator()); _variableList = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; _root.AddChild(_variableList); } public override void _UpdateProperty() { RebuildList(); } public void OnBeforeSerialize() { if (_addButton is not null) { _addButton.Pressed -= OnAddPressed; } ClearVariableList(); _creationDialog?.Free(); _creationDialog = null; _newNameEdit = null; _newTypeDropdown = null; _newArrayToggle = null; } public void OnAfterDeserialize() { if (_addButton is not null) { _addButton.Pressed += OnAddPressed; } RebuildList(); } private Array GetDefinitions() { GodotObject obj = GetEditedObject(); string propertyName = GetEditedProperty(); Variant value = obj.Get(propertyName); return value.AsGodotArray() ?? []; } private void NotifyChanged() { if (GetEditedObject() is Resource resource) { resource.EmitChanged(); } } private void RebuildList() { if (_variableList is null) { return; } // Defer the actual rebuild so that any in-progress signal emission (e.g. a button Pressed handler that // triggered an add/remove) finishes before we free the emitting nodes. CallDeferred(MethodName.RebuildListDeferred); } private void RebuildListDeferred() { if (_variableList is null) { return; } ClearVariableList(); Array definitions = GetDefinitions(); for (var i = 0; i < definitions.Count; i++) { AddVariableRow(definitions, i); } } private void ClearVariableList() { if (_variableList is null) { return; } foreach (Node child in _variableList.GetChildren()) { _variableList.RemoveChild(child); child.Free(); } } private void AddVariableRow(Array definitions, int index) { if (_variableList is null || index < 0 || index >= definitions.Count) { return; } ForgeSharedVariableDefinition def = definitions[index]; var rowContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; _variableList.AddChild(rowContainer); var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; rowContainer.AddChild(headerRow); var nameLabel = new Label { Text = def.VariableName, SizeFlagsHorizontal = SizeFlags.ExpandFill, }; nameLabel.AddThemeColorOverride("font_color", _variableColor); nameLabel.AddThemeFontOverride( "font", EditorInterface.Singleton.GetEditorTheme().GetFont("bold", "EditorFonts")); headerRow.AddChild(nameLabel); var typeLabel = new Label { Text = $"({StatescriptVariableTypeConverter.GetDisplayName(def.VariableType)}" + (def.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 (!def.IsArray) { Control valueEditor = CreateValueEditor(def); rowContainer.AddChild(valueEditor); } else { VBoxContainer arrayEditor = CreateArrayValueEditor(def); rowContainer.AddChild(arrayEditor); } rowContainer.AddChild(new HSeparator()); } private Control CreateValueEditor(ForgeSharedVariableDefinition def) { if (def.VariableType == StatescriptVariableType.Bool) { var hBox = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; hBox.AddChild(StatescriptEditorControls.CreateBoolEditor( def.InitialValue.AsBool(), x => SetVariableValue(def, Variant.From(x)))); return hBox; } if (StatescriptEditorControls.IsIntegerType(def.VariableType) || StatescriptEditorControls.IsFloatType(def.VariableType)) { var hBox = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; EditorSpinSlider spin = StatescriptEditorControls.CreateNumericSpinSlider( def.VariableType, def.InitialValue.AsDouble(), onChanged: x => { Variant newValue = StatescriptEditorControls.IsIntegerType(def.VariableType) ? Variant.From((long)x) : Variant.From(x); SetVariableValue(def, newValue); }); hBox.AddChild(spin); return hBox; } if (StatescriptEditorControls.IsVectorType(def.VariableType)) { return StatescriptEditorControls.CreateVectorEditor( def.VariableType, x => StatescriptEditorControls.GetVectorComponent( def.InitialValue, def.VariableType, x), onChanged: x => { Variant newValue = StatescriptEditorControls.BuildVectorVariant( def.VariableType, x); SetVariableValue(def, newValue); }); } var fallback = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; fallback.AddChild(new Label { Text = def.VariableType.ToString() }); return fallback; } private VBoxContainer CreateArrayValueEditor(ForgeSharedVariableDefinition def) { var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill, }; var headerRow = new HBoxContainer(); vBox.AddChild(headerRow); var isExpanded = _expandedArrays.Contains(def.VariableName); var elementsContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill, Visible = isExpanded, }; var toggleButton = new Button { Text = $"Array (size {def.InitialArrayValues.Count})", SizeFlagsHorizontal = SizeFlags.ExpandFill, ToggleMode = true, ButtonPressed = isExpanded, }; toggleButton.Toggled += x => { elementsContainer.Visible = x; var wasExpanded = !x; if (x) { _expandedArrays.Add(def.VariableName); } else { _expandedArrays.Remove(def.VariableName); } if (_undoRedo is not null) { _undoRedo.CreateAction("Toggle Array Expand"); _undoRedo.AddDoMethod( this, MethodName.DoSetArrayExpanded, def.VariableName, x); _undoRedo.AddUndoMethod( this, MethodName.DoSetArrayExpanded, def.VariableName, wasExpanded); _undoRedo.CommitAction(false); } }; headerRow.AddChild(toggleButton); var addElementButton = new Button { Icon = _addIcon, Flat = true, TooltipText = "Add Element", CustomMinimumSize = new Vector2(24, 24), }; addElementButton.Pressed += () => { Variant defaultValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(def.VariableType); AddArrayElement(def, defaultValue); }; headerRow.AddChild(addElementButton); vBox.AddChild(elementsContainer); for (var i = 0; i < def.InitialArrayValues.Count; i++) { var capturedIndex = i; if (def.VariableType == StatescriptVariableType.Bool) { var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; elementsContainer.AddChild(elementRow); elementRow.AddChild(new Label { Text = $"[{i}]" }); elementRow.AddChild(StatescriptEditorControls.CreateBoolEditor( def.InitialArrayValues[i].AsBool(), x => SetArrayElementValue(def, capturedIndex, Variant.From(x)))); AddArrayElementRemoveButton(elementRow, def, capturedIndex); } else if (StatescriptEditorControls.IsVectorType(def.VariableType)) { var elementVBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; elementsContainer.AddChild(elementVBox); var labelRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; elementVBox.AddChild(labelRow); labelRow.AddChild(new Label { Text = $"[{i}]", SizeFlagsHorizontal = SizeFlags.ExpandFill, }); AddArrayElementRemoveButton(labelRow, def, capturedIndex); VBoxContainer vectorEditor = StatescriptEditorControls.CreateVectorEditor( def.VariableType, x => StatescriptEditorControls.GetVectorComponent( def.InitialArrayValues[capturedIndex], def.VariableType, x), x => { Variant newValue = StatescriptEditorControls.BuildVectorVariant( def.VariableType, x); SetArrayElementValue(def, capturedIndex, newValue); }); elementVBox.AddChild(vectorEditor); } else { var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; elementsContainer.AddChild(elementRow); elementRow.AddChild(new Label { Text = $"[{i}]" }); EditorSpinSlider elementSpin = StatescriptEditorControls.CreateNumericSpinSlider( def.VariableType, def.InitialArrayValues[i].AsDouble(), onChanged: x => { Variant newValue = StatescriptEditorControls.IsIntegerType(def.VariableType) ? Variant.From((long)x) : Variant.From(x); SetArrayElementValue(def, capturedIndex, newValue); }); elementRow.AddChild(elementSpin); AddArrayElementRemoveButton(elementRow, def, capturedIndex); } } return vBox; } private void AddArrayElementRemoveButton( HBoxContainer row, ForgeSharedVariableDefinition def, int elementIndex) { var removeElementButton = new Button { Icon = _removeIcon, Flat = true, CustomMinimumSize = new Vector2(24, 24), }; removeElementButton.Pressed += () => RemoveArrayElement(def, elementIndex); row.AddChild(removeElementButton); } private void SetVariableValue(ForgeSharedVariableDefinition def, Variant newValue) { Variant oldValue = def.InitialValue; def.InitialValue = newValue; NotifyChanged(); if (_undoRedo is not null) { _undoRedo.CreateAction($"Change Shared Variable '{def.VariableName}'"); _undoRedo.AddDoMethod(this, MethodName.ApplyVariableValue, def, newValue); _undoRedo.AddUndoMethod(this, MethodName.ApplyVariableValue, def, oldValue); _undoRedo.CommitAction(false); } } private void SetArrayElementValue(ForgeSharedVariableDefinition def, int index, Variant newValue) { Variant oldValue = def.InitialArrayValues[index]; def.InitialArrayValues[index] = newValue; NotifyChanged(); if (_undoRedo is not null) { _undoRedo.CreateAction($"Change Shared Variable '{def.VariableName}' Element [{index}]"); _undoRedo.AddDoMethod(this, MethodName.ApplyArrayElementValue, def, index, newValue); _undoRedo.AddUndoMethod(this, MethodName.ApplyArrayElementValue, def, index, oldValue); _undoRedo.CommitAction(false); } } private void AddArrayElement(ForgeSharedVariableDefinition def, Variant value) { var wasExpanded = _expandedArrays.Contains(def.VariableName); if (_undoRedo is not null) { _undoRedo.CreateAction($"Add Element to '{def.VariableName}'"); _undoRedo.AddDoMethod(this, MethodName.DoAddArrayElement, def, value); _undoRedo.AddUndoMethod(this, MethodName.UndoAddArrayElement, def, wasExpanded); _undoRedo.CommitAction(); } else { DoAddArrayElement(def, value); } } private void RemoveArrayElement(ForgeSharedVariableDefinition def, int index) { if (index < 0 || index >= def.InitialArrayValues.Count) { return; } Variant oldValue = def.InitialArrayValues[index]; if (_undoRedo is not null) { _undoRedo.CreateAction($"Remove Element [{index}] from '{def.VariableName}'"); _undoRedo.AddDoMethod(this, MethodName.DoRemoveArrayElement, def, index); _undoRedo.AddUndoMethod(this, MethodName.UndoRemoveArrayElement, def, index, oldValue); _undoRedo.CommitAction(); } else { DoRemoveArrayElement(def, index); } } private void OnAddPressed() { ShowCreationDialog(); } private void ShowCreationDialog() { _creationDialog?.QueueFree(); _creationDialog = new AcceptDialog { Title = "Add Shared Variable", Size = new Vector2I(300, 160), Exclusive = true, }; var vBox = new VBoxContainer(); _creationDialog.AddChild(vBox); var nameRow = new HBoxContainer(); vBox.AddChild(nameRow); nameRow.AddChild(new Label { Text = "Name:", CustomMinimumSize = new Vector2(60, 0) }); _newNameEdit = new LineEdit { SizeFlagsHorizontal = SizeFlags.ExpandFill, PlaceholderText = "variable name", }; nameRow.AddChild(_newNameEdit); var typeRow = new HBoxContainer(); vBox.AddChild(typeRow); typeRow.AddChild(new Label { Text = "Type:", CustomMinimumSize = new Vector2(60, 0) }); _newTypeDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill }; foreach (StatescriptVariableType variableType in StatescriptVariableTypeConverter.GetAllTypes()) { _newTypeDropdown.AddItem( StatescriptVariableTypeConverter.GetDisplayName(variableType), (int)variableType); } typeRow.AddChild(_newTypeDropdown); var arrayRow = new HBoxContainer(); vBox.AddChild(arrayRow); arrayRow.AddChild(new Label { Text = "Array:", CustomMinimumSize = new Vector2(60, 0) }); _newArrayToggle = new CheckBox(); arrayRow.AddChild(_newArrayToggle); _creationDialog.Confirmed += OnCreationConfirmed; _creationDialog.Canceled += OnCreationCanceled; EditorInterface.Singleton.PopupDialogCentered(_creationDialog); } private void OnCreationConfirmed() { if (_newNameEdit is null || _newTypeDropdown is null || _newArrayToggle is null) { return; } var name = _newNameEdit.Text.Trim(); if (string.IsNullOrEmpty(name)) { return; } var variableType = (StatescriptVariableType)_newTypeDropdown.GetItemId(_newTypeDropdown.Selected); var newDef = new ForgeSharedVariableDefinition { VariableName = name, VariableType = variableType, IsArray = _newArrayToggle.ButtonPressed, InitialValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(variableType), }; Array definitions = GetDefinitions(); if (_undoRedo is not null) { _undoRedo.CreateAction("Add Shared Variable"); _undoRedo.AddDoMethod(this, MethodName.DoAddVariable, definitions, newDef); _undoRedo.AddUndoMethod(this, MethodName.UndoAddVariable, definitions, newDef); _undoRedo.CommitAction(); } else { DoAddVariable(definitions, newDef); } CleanupCreationDialog(); } private void OnCreationCanceled() { CleanupCreationDialog(); } private void CleanupCreationDialog() { _creationDialog?.QueueFree(); _creationDialog = null; _newNameEdit = null; _newTypeDropdown = null; _newArrayToggle = null; } private void OnDeletePressed(int index) { Array definitions = GetDefinitions(); if (index < 0 || index >= definitions.Count) { return; } ForgeSharedVariableDefinition variable = definitions[index]; if (_undoRedo is not null) { _undoRedo.CreateAction("Remove Shared Variable"); _undoRedo.AddDoMethod(this, MethodName.DoRemoveVariable, definitions, variable, index); _undoRedo.AddUndoMethod(this, MethodName.UndoRemoveVariable, definitions, variable, index); _undoRedo.CommitAction(); } else { DoRemoveVariable(definitions, index); } } private void ApplyVariableValue(ForgeSharedVariableDefinition def, Variant value) { def.InitialValue = value; NotifyChanged(); RebuildList(); } private void ApplyArrayElementValue(ForgeSharedVariableDefinition def, int index, Variant value) { def.InitialArrayValues[index] = value; NotifyChanged(); RebuildList(); } private void DoAddVariable(Array definitions, ForgeSharedVariableDefinition def) { definitions.Add(def); NotifyChanged(); RebuildList(); } private void UndoAddVariable(Array definitions, ForgeSharedVariableDefinition def) { definitions.Remove(def); NotifyChanged(); RebuildList(); } private void DoRemoveVariable( Array definitions, int index) { definitions.RemoveAt(index); NotifyChanged(); RebuildList(); } private void UndoRemoveVariable( Array definitions, ForgeSharedVariableDefinition sharedVariableDefinition, int index) { if (index >= definitions.Count) { definitions.Add(sharedVariableDefinition); } else { definitions.Insert(index, sharedVariableDefinition); } NotifyChanged(); RebuildList(); } private void DoAddArrayElement(ForgeSharedVariableDefinition sharedVariableDefinition, Variant value) { sharedVariableDefinition.InitialArrayValues.Add(value); _expandedArrays.Add(sharedVariableDefinition.VariableName); NotifyChanged(); RebuildList(); } private void UndoAddArrayElement(ForgeSharedVariableDefinition sharedVariableDefinition, bool wasExpanded) { if (sharedVariableDefinition.InitialArrayValues.Count > 0) { sharedVariableDefinition.InitialArrayValues.RemoveAt(sharedVariableDefinition.InitialArrayValues.Count - 1); } if (!wasExpanded) { _expandedArrays.Remove(sharedVariableDefinition.VariableName); } NotifyChanged(); RebuildList(); } private void DoRemoveArrayElement(ForgeSharedVariableDefinition sharedVariableDefinition, int index) { sharedVariableDefinition.InitialArrayValues.RemoveAt(index); NotifyChanged(); RebuildList(); } private void UndoRemoveArrayElement( ForgeSharedVariableDefinition sharedVariableDefinition, int index, Variant value) { if (index >= sharedVariableDefinition.InitialArrayValues.Count) { sharedVariableDefinition.InitialArrayValues.Add(value); } else { sharedVariableDefinition.InitialArrayValues.Insert(index, value); } NotifyChanged(); RebuildList(); } private void DoSetArrayExpanded(string variableName, bool expanded) { if (expanded) { _expandedArrays.Add(variableName); } else { _expandedArrays.Remove(variableName); } RebuildList(); } } #endif