// 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