1421 lines
33 KiB
C#
1421 lines
33 KiB
C#
// Copyright © Gamesmiths Guild.
|
|
|
|
#if TOOLS
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
|
using Godot;
|
|
using GodotCollections = Godot.Collections;
|
|
|
|
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
|
|
|
|
/// <summary>
|
|
/// Main editor panel for Statescript graphs. Supports editing multiple graphs via tabs.
|
|
/// Designed to be shown in the bottom panel area of the Godot editor.
|
|
/// </summary>
|
|
[Tool]
|
|
public partial class StatescriptGraphEditorDock : EditorDock, ISerializationListener
|
|
{
|
|
private readonly List<GraphTab> _openTabs = [];
|
|
private readonly Dictionary<StringName, Vector2> _preMovePositions = [];
|
|
|
|
private PanelContainer? _tabBarBackground;
|
|
private TabBar? _tabBar;
|
|
private PanelContainer? _contentPanel;
|
|
private GraphEdit? _graphEdit;
|
|
private Label? _emptyLabel;
|
|
private Button? _addNodeButton;
|
|
private StatescriptAddNodeDialog? _addNodeDialog;
|
|
private StatescriptVariablePanel? _variablePanel;
|
|
private HSplitContainer? _splitContainer;
|
|
|
|
private MenuButton? _fileMenuButton;
|
|
private PopupMenu? _fileMenuPopup;
|
|
private Button? _variablesToggleButton;
|
|
private Button? _onlineDocsButton;
|
|
|
|
private AcceptDialog? _newStatescriptDialog;
|
|
private LineEdit? _newStatescriptPathEdit;
|
|
|
|
private EditorUndoRedoManager? _undoRedo;
|
|
|
|
private int _nextNodeId;
|
|
private bool _isLoadingGraph;
|
|
|
|
private string? _pendingConnectionNode;
|
|
private int _pendingConnectionPort;
|
|
private bool _pendingConnectionIsOutput;
|
|
|
|
private EditorFileSystem? _fileSystem;
|
|
private Callable _filesystemChangedCallable;
|
|
|
|
private string[]? _serializedTabPaths;
|
|
private int _serializedActiveTab = -1;
|
|
private bool[]? _serializedVariablesStates;
|
|
private string[]? _serializedConnections;
|
|
private int[]? _serializedConnectionCounts;
|
|
|
|
/// <summary>
|
|
/// Gets the currently active graph resource, if any.
|
|
/// </summary>
|
|
public StatescriptGraph? CurrentGraph =>
|
|
_openTabs.Count > 0 && _tabBar is not null && _tabBar.CurrentTab < _openTabs.Count
|
|
? _openTabs[_tabBar.CurrentTab].GraphResource
|
|
: null;
|
|
|
|
public StatescriptGraphEditorDock()
|
|
{
|
|
Title = "Statescript";
|
|
DefaultSlot = DockSlot.Bottom;
|
|
DockIcon = GD.Load<Texture2D>("uid://b6yrjb46fluw3");
|
|
|
|
AvailableLayouts = DockLayout.Horizontal | DockLayout.Floating;
|
|
}
|
|
|
|
public override void _Ready()
|
|
{
|
|
base._Ready();
|
|
|
|
StyleBox bottomPanelStyleBox = EditorInterface.Singleton.GetBaseControl()
|
|
.GetThemeStylebox("BottomPanel", "EditorStyles");
|
|
|
|
AddThemeConstantOverride("margin_top", -(int)bottomPanelStyleBox.ContentMarginTop);
|
|
AddThemeConstantOverride("margin_left", -(int)bottomPanelStyleBox.ContentMarginLeft);
|
|
AddThemeConstantOverride("margin_right", -(int)bottomPanelStyleBox.ContentMarginRight);
|
|
|
|
BuildUI();
|
|
UpdateVisibility();
|
|
|
|
_fileSystem = EditorInterface.Singleton.GetResourceFilesystem();
|
|
_filesystemChangedCallable = new Callable(this, nameof(OnFilesystemChanged));
|
|
|
|
_fileSystem.Connect(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable);
|
|
}
|
|
|
|
public override void _ExitTree()
|
|
{
|
|
base._ExitTree();
|
|
|
|
ClearGraphEditor();
|
|
_openTabs.Clear();
|
|
|
|
if (_fileSystem?.IsConnected(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable)
|
|
== true)
|
|
{
|
|
_fileSystem.Disconnect(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable);
|
|
}
|
|
|
|
DisconnectUISignals();
|
|
}
|
|
|
|
public void OnBeforeSerialize()
|
|
{
|
|
if (_fileSystem?.IsConnected(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable)
|
|
== true)
|
|
{
|
|
_fileSystem.Disconnect(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable);
|
|
}
|
|
|
|
_serializedTabPaths = GetOpenResourcePaths();
|
|
_serializedActiveTab = GetActiveTabIndex();
|
|
_serializedVariablesStates = GetVariablesPanelStates();
|
|
|
|
SyncVisualNodePositionsToGraph();
|
|
SyncConnectionsToCurrentGraph();
|
|
|
|
if (CurrentGraph is not null && _graphEdit is not null)
|
|
{
|
|
CurrentGraph.ScrollOffset = _graphEdit.ScrollOffset;
|
|
CurrentGraph.Zoom = _graphEdit.Zoom;
|
|
}
|
|
|
|
var allConnections = new List<string>();
|
|
_serializedConnectionCounts = new int[_openTabs.Count];
|
|
for (var i = 0; i < _openTabs.Count; i++)
|
|
{
|
|
StatescriptGraph graph = _openTabs[i].GraphResource;
|
|
var count = 0;
|
|
foreach (StatescriptConnection c in graph.Connections)
|
|
{
|
|
allConnections.Add($"{c.FromNode},{c.OutputPort},{c.ToNode},{c.InputPort}");
|
|
count++;
|
|
}
|
|
|
|
_serializedConnectionCounts[i] = count;
|
|
}
|
|
|
|
_serializedConnections = [.. allConnections];
|
|
|
|
DisconnectUISignals();
|
|
ClearGraphEditor();
|
|
|
|
if (_tabBar is not null)
|
|
{
|
|
while (_tabBar.GetTabCount() > 0)
|
|
{
|
|
_tabBar.RemoveTab(0);
|
|
}
|
|
}
|
|
|
|
_openTabs.Clear();
|
|
}
|
|
|
|
public void OnAfterDeserialize()
|
|
{
|
|
_filesystemChangedCallable = new Callable(this, nameof(OnFilesystemChanged));
|
|
|
|
if (_fileSystem?.
|
|
IsConnected(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable) == false)
|
|
{
|
|
_fileSystem.Connect(EditorFileSystem.SignalName.ResourcesReimported, _filesystemChangedCallable);
|
|
}
|
|
|
|
ConnectUISignals();
|
|
|
|
if (_serializedTabPaths?.Length > 0)
|
|
{
|
|
_ = RestoreTabsDeferred();
|
|
}
|
|
}
|
|
|
|
public override void _Notification(int what)
|
|
{
|
|
base._Notification(what);
|
|
|
|
if (what == NotificationThemeChanged)
|
|
{
|
|
UpdateTheme();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the <see cref="EditorUndoRedoManager"/> used for undo/redo support.
|
|
/// </summary>
|
|
/// <param name="undoRedo">The undo/redo manager from the editor plugin.</param>
|
|
public void SetUndoRedo(EditorUndoRedoManager undoRedo)
|
|
{
|
|
_undoRedo = undoRedo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens a graph resource for editing. If already open, switches to its tab.
|
|
/// </summary>
|
|
/// <param name="graph">The graph resource to edit.</param>
|
|
public void OpenGraph(StatescriptGraph graph)
|
|
{
|
|
if (_tabBar is null || _graphEdit is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < _openTabs.Count; i++)
|
|
{
|
|
if (_openTabs[i].GraphResource == graph || (!string.IsNullOrEmpty(graph.ResourcePath)
|
|
&& _openTabs[i].ResourcePath == graph.ResourcePath))
|
|
{
|
|
_tabBar.CurrentTab = i;
|
|
return;
|
|
}
|
|
}
|
|
|
|
graph.EnsureEntryNode();
|
|
|
|
var tab = new GraphTab(graph);
|
|
_openTabs.Add(tab);
|
|
|
|
_tabBar.AddTab(graph.StatescriptName);
|
|
_tabBar.CurrentTab = _openTabs.Count - 1;
|
|
|
|
LoadGraphIntoEditor(graph);
|
|
UpdateVisibility();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the currently active graph tab.
|
|
/// </summary>
|
|
public void CloseCurrentTab()
|
|
{
|
|
if (_tabBar is null || _openTabs.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var currentTab = _tabBar.CurrentTab;
|
|
if (currentTab < 0 || currentTab >= _openTabs.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CloseTabByIndex(currentTab);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the resource paths of all open tabs for state persistence.
|
|
/// </summary>
|
|
/// <returns>An array of resource paths.</returns>
|
|
public string[] GetOpenResourcePaths()
|
|
{
|
|
return [.. _openTabs.Select(x => x.ResourcePath)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the currently active tab index.
|
|
/// </summary>
|
|
/// <returns>The active tab index, or -1 if no tabs are open.</returns>
|
|
public int GetActiveTabIndex()
|
|
{
|
|
return _tabBar?.CurrentTab ?? -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns per-tab variables panel visibility states.
|
|
/// </summary>
|
|
/// <returns><see langword="true"/> for tabs with the variables panel open, <see langword="false"/> otherwise.
|
|
/// </returns>
|
|
public bool[] GetVariablesPanelStates()
|
|
{
|
|
return [.. _openTabs.Select(x => x.VariablesPanelOpen)];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves all open graphs that have a resource path. Called by the plugin's _SaveExternalData
|
|
/// so that Ctrl+S persists statescript graphs alongside scenes.
|
|
/// </summary>
|
|
public void SaveAllOpenGraphs()
|
|
{
|
|
if (_graphEdit is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SyncVisualNodePositionsToGraph();
|
|
SyncConnectionsToCurrentGraph();
|
|
|
|
if (CurrentGraph is not null)
|
|
{
|
|
CurrentGraph.ScrollOffset = _graphEdit.ScrollOffset;
|
|
CurrentGraph.Zoom = _graphEdit.Zoom;
|
|
}
|
|
|
|
foreach (StatescriptGraph graph in _openTabs.Select(x => x.GraphResource))
|
|
{
|
|
if (string.IsNullOrEmpty(graph.ResourcePath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SaveGraphResource(graph);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores tabs from paths and active index, used by EditorPlugin _SetWindowLayout.
|
|
/// </summary>
|
|
/// <param name="paths">The resource paths of the tabs to restore.</param>
|
|
/// <param name="activeIndex">The index of the tab to make active.</param>
|
|
/// <param name="variablesStates">The visibility states of the variables panel for each tab.</param>
|
|
public void RestoreFromPaths(string[] paths, int activeIndex, bool[]? variablesStates = null)
|
|
{
|
|
if (_tabBar is null || _graphEdit is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isLoadingGraph = true;
|
|
|
|
_openTabs.Clear();
|
|
while (_tabBar.GetTabCount() > 0)
|
|
{
|
|
_tabBar.RemoveTab(0);
|
|
}
|
|
|
|
var skippedTabs = 0;
|
|
for (var i = 0; i < paths.Length; i++)
|
|
{
|
|
var path = paths[i];
|
|
|
|
StatescriptGraph? graph = LoadGraphFromPath(path);
|
|
if (graph is null)
|
|
{
|
|
skippedTabs++;
|
|
continue;
|
|
}
|
|
|
|
graph.EnsureEntryNode();
|
|
var tab = new GraphTab(graph);
|
|
|
|
var currentTab = i - skippedTabs;
|
|
if (variablesStates is not null && currentTab < variablesStates.Length)
|
|
{
|
|
tab.VariablesPanelOpen = variablesStates[currentTab];
|
|
}
|
|
|
|
_openTabs.Add(tab);
|
|
_tabBar.AddTab(graph.StatescriptName);
|
|
}
|
|
|
|
_isLoadingGraph = false;
|
|
|
|
if (activeIndex >= 0 && activeIndex < _openTabs.Count)
|
|
{
|
|
_tabBar.CurrentTab = activeIndex;
|
|
LoadGraphIntoEditor(_openTabs[activeIndex].GraphResource);
|
|
ApplyVariablesPanelState(activeIndex);
|
|
}
|
|
|
|
UpdateVisibility();
|
|
}
|
|
|
|
private static void SyncNodePositionsToResource(
|
|
StatescriptGraph graph,
|
|
GodotCollections.Dictionary<StringName, Vector2> positions)
|
|
{
|
|
foreach (StatescriptNode node in graph.Nodes)
|
|
{
|
|
if (positions.TryGetValue(node.NodeId, out Vector2 pos))
|
|
{
|
|
node.PositionOffset = pos;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void OnOnlineDocsPressed()
|
|
{
|
|
OS.ShellOpen("https://github.com/gamesmiths-guild/forge-godot/tree/main/docs");
|
|
}
|
|
|
|
private static string GetBaseFilePath(string resourcePath)
|
|
{
|
|
var separatorIndex = resourcePath.IndexOf("::", StringComparison.Ordinal);
|
|
return separatorIndex >= 0 ? resourcePath[..separatorIndex] : resourcePath;
|
|
}
|
|
|
|
private static bool IsSubResourcePath(string resourcePath)
|
|
{
|
|
return resourcePath.Contains("::", StringComparison.Ordinal);
|
|
}
|
|
|
|
private static StatescriptGraph? LoadGraphFromPath(string path)
|
|
{
|
|
if (IsSubResourcePath(path))
|
|
{
|
|
var basePath = GetBaseFilePath(path);
|
|
if (!ResourceLoader.Exists(basePath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Resource? parentResource = ResourceLoader.Load(basePath);
|
|
if (parentResource is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return FindSubResourceGraph(parentResource, path);
|
|
}
|
|
|
|
if (!ResourceLoader.Exists(path))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return ResourceLoader.Load<StatescriptGraph>(path);
|
|
}
|
|
|
|
private static StatescriptGraph? FindSubResourceGraph(Resource parentResource, string subResourcePath)
|
|
{
|
|
foreach (var propertyName in parentResource.GetPropertyList()
|
|
.Select(p => p["name"].AsString()))
|
|
{
|
|
Variant value = parentResource.Get(propertyName);
|
|
if (value.Obj is StatescriptGraph graph && graph.ResourcePath == subResourcePath)
|
|
{
|
|
return graph;
|
|
}
|
|
|
|
if (value.Obj is Resource nestedResource)
|
|
{
|
|
StatescriptGraph? found = FindSubResourceInNested(nestedResource, subResourcePath);
|
|
if (found is not null)
|
|
{
|
|
return found;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static StatescriptGraph? FindSubResourceInNested(Resource resource, string subResourcePath)
|
|
{
|
|
if (resource is StatescriptGraph graph && graph.ResourcePath == subResourcePath)
|
|
{
|
|
return graph;
|
|
}
|
|
|
|
foreach (var propertyName in resource.GetPropertyList()
|
|
.Select(p => p["name"].AsString()))
|
|
{
|
|
Variant value = resource.Get(propertyName);
|
|
if (value.Obj is StatescriptGraph nestedGraph && nestedGraph.ResourcePath == subResourcePath)
|
|
{
|
|
return nestedGraph;
|
|
}
|
|
|
|
if (value.Obj is Resource nestedResource && nestedResource != resource)
|
|
{
|
|
StatescriptGraph? found = FindSubResourceInNested(nestedResource, subResourcePath);
|
|
if (found is not null)
|
|
{
|
|
return found;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void SaveGraphResource(StatescriptGraph graph)
|
|
{
|
|
var path = graph.ResourcePath;
|
|
|
|
if (IsSubResourcePath(path))
|
|
{
|
|
var basePath = GetBaseFilePath(path);
|
|
Resource? parentResource = ResourceLoader.Load(basePath);
|
|
if (parentResource is not null)
|
|
{
|
|
ResourceSaver.Save(parentResource);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ResourceSaver.Save(graph);
|
|
}
|
|
}
|
|
|
|
private async Task RestoreTabsDeferred()
|
|
{
|
|
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
|
|
|
if (_serializedTabPaths is null || _serializedTabPaths.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var paths = _serializedTabPaths;
|
|
var activeTab = _serializedActiveTab;
|
|
var varStates = _serializedVariablesStates;
|
|
var savedConnections = _serializedConnections;
|
|
var connectionCounts = _serializedConnectionCounts;
|
|
|
|
_serializedTabPaths = null;
|
|
_serializedActiveTab = -1;
|
|
_serializedVariablesStates = null;
|
|
_serializedConnections = null;
|
|
_serializedConnectionCounts = null;
|
|
|
|
if (_tabBar is null || _graphEdit is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isLoadingGraph = true;
|
|
|
|
_openTabs.Clear();
|
|
while (_tabBar.GetTabCount() > 0)
|
|
{
|
|
_tabBar.RemoveTab(0);
|
|
}
|
|
|
|
var skippedTabs = 0;
|
|
for (var i = 0; i < paths.Length; i++)
|
|
{
|
|
if (!ResourceLoader.Exists(paths[i]))
|
|
{
|
|
skippedTabs++;
|
|
continue;
|
|
}
|
|
|
|
StatescriptGraph? graph = ResourceLoader.Load<StatescriptGraph>(paths[i]);
|
|
if (graph is null)
|
|
{
|
|
skippedTabs++;
|
|
continue;
|
|
}
|
|
|
|
graph.EnsureEntryNode();
|
|
var tab = new GraphTab(graph);
|
|
|
|
var currentTab = i - skippedTabs;
|
|
if (varStates is not null && currentTab < varStates.Length)
|
|
{
|
|
tab.VariablesPanelOpen = varStates[currentTab];
|
|
}
|
|
|
|
_openTabs.Add(tab);
|
|
_tabBar.AddTab(graph.StatescriptName);
|
|
}
|
|
|
|
_isLoadingGraph = false;
|
|
|
|
if (savedConnections is not null && connectionCounts is not null)
|
|
{
|
|
var offset = 0;
|
|
for (var i = 0; i < _openTabs.Count && i < connectionCounts.Length; i++)
|
|
{
|
|
StatescriptGraph graph = _openTabs[i].GraphResource;
|
|
graph.Connections.Clear();
|
|
|
|
for (var j = 0; j < connectionCounts[i] && offset < savedConnections.Length; j++, offset++)
|
|
{
|
|
var parts = savedConnections[offset].Split(',');
|
|
if (parts.Length != 4
|
|
|| !int.TryParse(parts[1], out var outPort)
|
|
|| !int.TryParse(parts[3], out var inPort))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
graph.Connections.Add(new StatescriptConnection
|
|
{
|
|
FromNode = parts[0],
|
|
OutputPort = outPort,
|
|
ToNode = parts[2],
|
|
InputPort = inPort,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (activeTab >= 0 && activeTab < _openTabs.Count)
|
|
{
|
|
_tabBar.CurrentTab = activeTab;
|
|
LoadGraphIntoEditor(_openTabs[activeTab].GraphResource);
|
|
ApplyVariablesPanelState(activeTab);
|
|
}
|
|
|
|
UpdateVisibility();
|
|
}
|
|
|
|
private void CloseTabByIndex(int tabIndex)
|
|
{
|
|
if (_tabBar is null || tabIndex < 0 || tabIndex >= _openTabs.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_openTabs.RemoveAt(tabIndex);
|
|
_tabBar.RemoveTab(tabIndex);
|
|
|
|
if (_openTabs.Count > 0)
|
|
{
|
|
var newTab = Mathf.Min(tabIndex, _openTabs.Count - 1);
|
|
_tabBar.CurrentTab = newTab;
|
|
LoadGraphIntoEditor(_openTabs[newTab].GraphResource);
|
|
ApplyVariablesPanelState(newTab);
|
|
}
|
|
else
|
|
{
|
|
ClearGraphEditor();
|
|
}
|
|
|
|
UpdateVisibility();
|
|
}
|
|
|
|
private void BuildUI()
|
|
{
|
|
var vBox = new VBoxContainer
|
|
{
|
|
SizeFlagsVertical = SizeFlags.ExpandFill,
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
|
};
|
|
|
|
AddChild(vBox);
|
|
|
|
_tabBarBackground = new PanelContainer();
|
|
vBox.AddChild(_tabBarBackground);
|
|
|
|
var tabBarHBox = new HBoxContainer
|
|
{
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
|
};
|
|
|
|
_tabBarBackground.AddChild(tabBarHBox);
|
|
|
|
_tabBar = new TabBar
|
|
{
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
|
TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowActiveOnly,
|
|
DragToRearrangeEnabled = true,
|
|
};
|
|
|
|
_tabBar.TabChanged += OnTabChanged;
|
|
_tabBar.TabClosePressed += OnTabClosePressed;
|
|
tabBarHBox.AddChild(_tabBar);
|
|
|
|
_contentPanel = new PanelContainer
|
|
{
|
|
SizeFlagsVertical = SizeFlags.ExpandFill,
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
|
};
|
|
|
|
vBox.AddChild(_contentPanel);
|
|
|
|
_splitContainer = new HSplitContainer
|
|
{
|
|
SizeFlagsVertical = SizeFlags.ExpandFill,
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
|
};
|
|
|
|
_contentPanel.AddChild(_splitContainer);
|
|
|
|
_graphEdit = new GraphEdit
|
|
{
|
|
SizeFlagsVertical = SizeFlags.ExpandFill,
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
|
ShowZoomLabel = true,
|
|
RightDisconnects = true,
|
|
GridPattern = GraphEdit.GridPatternEnum.Dots,
|
|
};
|
|
|
|
_graphEdit.ConnectionRequest += OnConnectionRequest;
|
|
_graphEdit.DisconnectionRequest += OnDisconnectionRequest;
|
|
_graphEdit.DeleteNodesRequest += OnDeleteNodesRequest;
|
|
_graphEdit.BeginNodeMove += OnBeginNodeMove;
|
|
_graphEdit.EndNodeMove += OnEndNodeMove;
|
|
_graphEdit.PopupRequest += OnGraphEditPopupRequest;
|
|
_graphEdit.ConnectionToEmpty += OnConnectionToEmpty;
|
|
_graphEdit.ConnectionFromEmpty += OnConnectionFromEmpty;
|
|
_graphEdit.GuiInput += OnGraphEditGuiInput;
|
|
_splitContainer.AddChild(_graphEdit);
|
|
|
|
_variablePanel = new StatescriptVariablePanel
|
|
{
|
|
Visible = true,
|
|
};
|
|
|
|
_variablePanel.VariablesChanged += OnGraphVariablesChanged;
|
|
_variablePanel.VariableUndoRedoPerformed += OnVariableUndoRedoPerformed;
|
|
_variablePanel.VariableHighlightChanged += OnVariableHighlightChanged;
|
|
_splitContainer.AddChild(_variablePanel);
|
|
|
|
if (_undoRedo is not null)
|
|
{
|
|
_variablePanel.SetUndoRedo(_undoRedo);
|
|
}
|
|
|
|
HBoxContainer menuHBox = _graphEdit.GetMenuHBox();
|
|
|
|
menuHBox.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
|
var parent = (PanelContainer)menuHBox.GetParent();
|
|
parent.SetAnchorsAndOffsetsPreset(LayoutPreset.TopWide, LayoutPresetMode.Minsize, 10);
|
|
|
|
_fileMenuButton = new MenuButton
|
|
{
|
|
Text = "File",
|
|
Flat = false,
|
|
SwitchOnHover = true,
|
|
ThemeTypeVariation = "FlatMenuButton",
|
|
};
|
|
|
|
_fileMenuPopup = _fileMenuButton.GetPopup();
|
|
#pragma warning disable RCS1130, S3265 // Bitwise operation on enum without Flags attribute
|
|
_fileMenuPopup.AddItem("New Statescript...", 0, Key.N | (Key)KeyModifierMask.MaskCtrl);
|
|
_fileMenuPopup.AddItem("Load Statescript File...", 1, Key.O | (Key)KeyModifierMask.MaskCtrl);
|
|
_fileMenuPopup.AddSeparator();
|
|
_fileMenuPopup.AddItem("Save", 2, Key.S | (Key)KeyModifierMask.MaskCtrl | (Key)KeyModifierMask.MaskAlt);
|
|
_fileMenuPopup.AddItem("Save As...", 3);
|
|
_fileMenuPopup.AddSeparator();
|
|
_fileMenuPopup.AddItem("Close", 4, Key.W | (Key)KeyModifierMask.MaskCtrl);
|
|
#pragma warning restore RCS1130, S3265 // Bitwise operation on enum without Flags attribute
|
|
_fileMenuPopup.IdPressed += OnFileMenuIdPressed;
|
|
|
|
menuHBox.AddChild(_fileMenuButton);
|
|
menuHBox.MoveChild(_fileMenuButton, 0);
|
|
|
|
var separator1 = new VSeparator();
|
|
menuHBox.AddChild(separator1);
|
|
menuHBox.MoveChild(separator1, 1);
|
|
|
|
_addNodeButton = new Button
|
|
{
|
|
Text = "Add Node...",
|
|
ThemeTypeVariation = "FlatButton",
|
|
};
|
|
|
|
_addNodeButton.Pressed += OnAddNodeButtonPressed;
|
|
|
|
menuHBox.AddChild(_addNodeButton);
|
|
menuHBox.MoveChild(_addNodeButton, 2);
|
|
|
|
var separator2 = new VSeparator();
|
|
menuHBox.AddChild(separator2);
|
|
|
|
_variablesToggleButton = new Button
|
|
{
|
|
Text = "Variables",
|
|
ToggleMode = true,
|
|
ThemeTypeVariation = "FlatButton",
|
|
Icon = EditorInterface.Singleton.GetEditorTheme().GetIcon("SubViewport", "EditorIcons"),
|
|
};
|
|
|
|
_variablesToggleButton.Toggled += OnVariablesToggled;
|
|
menuHBox.AddChild(_variablesToggleButton);
|
|
|
|
var spacer = new Control
|
|
{
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
|
};
|
|
|
|
menuHBox.AddChild(spacer);
|
|
|
|
_onlineDocsButton = new Button
|
|
{
|
|
Text = "Online Docs",
|
|
ThemeTypeVariation = "FlatButton",
|
|
Icon = EditorInterface.Singleton.GetEditorTheme().GetIcon("ExternalLink", "EditorIcons"),
|
|
};
|
|
|
|
_onlineDocsButton.Pressed += OnOnlineDocsPressed;
|
|
menuHBox.AddChild(_onlineDocsButton);
|
|
|
|
_emptyLabel = new Label
|
|
{
|
|
Text = "Select a Statescript resource to begin editing.",
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
SizeFlagsVertical = SizeFlags.ExpandFill,
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
|
};
|
|
|
|
_emptyLabel.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.6f));
|
|
_contentPanel.AddChild(_emptyLabel);
|
|
|
|
_addNodeDialog = new StatescriptAddNodeDialog();
|
|
_addNodeDialog.NodeCreationRequested += OnDialogNodeCreationRequested;
|
|
_addNodeDialog.Canceled += OnDialogCanceled;
|
|
AddChild(_addNodeDialog);
|
|
|
|
UpdateTheme();
|
|
}
|
|
|
|
private void UpdateTheme()
|
|
{
|
|
if (_tabBarBackground is null || _contentPanel is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Control baseControl = EditorInterface.Singleton.GetBaseControl();
|
|
|
|
StyleBox tabBarStyle = baseControl.GetThemeStylebox("tabbar_background", "TabContainer");
|
|
_tabBarBackground.AddThemeStyleboxOverride("panel", tabBarStyle);
|
|
|
|
StyleBox panelStyle = baseControl.GetThemeStylebox("panel", "TabContainer");
|
|
_contentPanel.AddThemeStyleboxOverride("panel", panelStyle);
|
|
}
|
|
|
|
private void UpdateVisibility()
|
|
{
|
|
var hasOpenGraph = _openTabs.Count > 0;
|
|
|
|
if (_splitContainer is not null)
|
|
{
|
|
_splitContainer.Visible = hasOpenGraph;
|
|
}
|
|
|
|
if (_tabBarBackground is not null)
|
|
{
|
|
_tabBarBackground.Visible = hasOpenGraph;
|
|
}
|
|
|
|
if (_emptyLabel is not null)
|
|
{
|
|
_emptyLabel.Visible = !hasOpenGraph;
|
|
}
|
|
|
|
if (!hasOpenGraph)
|
|
{
|
|
if (_variablePanel is not null)
|
|
{
|
|
_variablePanel.Visible = false;
|
|
}
|
|
|
|
_variablesToggleButton?.SetPressedNoSignal(false);
|
|
}
|
|
}
|
|
|
|
private void LoadGraphIntoEditor(StatescriptGraph graph)
|
|
{
|
|
if (_graphEdit is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var wasLoading = _isLoadingGraph;
|
|
_isLoadingGraph = true;
|
|
|
|
ClearGraphEditor();
|
|
|
|
_graphEdit.Zoom = graph.Zoom;
|
|
|
|
UpdateNextNodeId(graph);
|
|
|
|
foreach (StatescriptNode nodeResource in graph.Nodes)
|
|
{
|
|
var graphNode = new StatescriptGraphNode();
|
|
_graphEdit.AddChild(graphNode);
|
|
graphNode.Initialize(nodeResource, graph);
|
|
graphNode.SetUndoRedo(_undoRedo);
|
|
}
|
|
|
|
foreach (StatescriptConnection connection in graph.Connections)
|
|
{
|
|
_graphEdit.ConnectNode(
|
|
connection.FromNode,
|
|
connection.OutputPort,
|
|
connection.ToNode,
|
|
connection.InputPort);
|
|
}
|
|
|
|
_isLoadingGraph = wasLoading;
|
|
|
|
_ = ApplyScrollNextFrame(graph.ScrollOffset);
|
|
}
|
|
|
|
private async Task ApplyScrollNextFrame(Vector2 offset)
|
|
{
|
|
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
|
|
|
if (_graphEdit is not null)
|
|
{
|
|
_graphEdit.ScrollOffset = offset;
|
|
}
|
|
}
|
|
|
|
private void ClearGraphEditor()
|
|
{
|
|
if (_graphEdit is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_graphEdit.ClearConnections();
|
|
|
|
var toRemove = new List<Node>();
|
|
toRemove.AddRange(_graphEdit.GetChildren().Where(x => x is GraphNode));
|
|
|
|
foreach (Node node in toRemove)
|
|
{
|
|
if (node is StatescriptGraphNode graphNode)
|
|
{
|
|
graphNode.OnBeforeSerialize();
|
|
}
|
|
|
|
_graphEdit.RemoveChild(node);
|
|
node.Free();
|
|
}
|
|
}
|
|
|
|
private void UpdateNextNodeId(StatescriptGraph graph)
|
|
{
|
|
var maxId = 0;
|
|
foreach (var nodeId in graph.Nodes.Select(x => x.NodeId))
|
|
{
|
|
if (nodeId.StartsWith("node_", StringComparison.InvariantCultureIgnoreCase)
|
|
&& int.TryParse(nodeId["node_".Length..], out var id)
|
|
&& id >= maxId)
|
|
{
|
|
maxId = id + 1;
|
|
}
|
|
}
|
|
|
|
if (maxId > _nextNodeId)
|
|
{
|
|
_nextNodeId = maxId;
|
|
}
|
|
}
|
|
|
|
private void RefreshTabTitles()
|
|
{
|
|
if (_tabBar is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < _openTabs.Count; i++)
|
|
{
|
|
_tabBar.SetTabTitle(i, _openTabs[i].GraphResource.StatescriptName);
|
|
}
|
|
}
|
|
|
|
private void SaveGraphStateByIndex(int tabIndex)
|
|
{
|
|
if (tabIndex < 0 || tabIndex >= _openTabs.Count || _graphEdit is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StatescriptGraph graph = _openTabs[tabIndex].GraphResource;
|
|
|
|
graph.ScrollOffset = _graphEdit.ScrollOffset;
|
|
graph.Zoom = _graphEdit.Zoom;
|
|
|
|
SyncVisualNodePositionsToGraph();
|
|
SyncConnectionsToGraph(graph);
|
|
}
|
|
|
|
private void SaveOutgoingTabState(int newTabIndex)
|
|
{
|
|
if (_graphEdit is null || _openTabs.Count <= 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StatescriptGraphNode? firstNode = null;
|
|
foreach (Node child in _graphEdit.GetChildren())
|
|
{
|
|
if (child is StatescriptGraphNode statescriptNode)
|
|
{
|
|
firstNode = statescriptNode;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (firstNode?.NodeResource is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < _openTabs.Count; i++)
|
|
{
|
|
if (i == newTabIndex)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
StatescriptGraph graph = _openTabs[i].GraphResource;
|
|
foreach (StatescriptNode node in graph.Nodes)
|
|
{
|
|
if (node == firstNode.NodeResource)
|
|
{
|
|
SaveGraphStateByIndex(i);
|
|
|
|
if (_variablePanel is not null)
|
|
{
|
|
_openTabs[i].VariablesPanelOpen = _variablePanel.Visible;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnTabChanged(long tab)
|
|
{
|
|
if (_isLoadingGraph)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (tab >= 0 && tab < _openTabs.Count)
|
|
{
|
|
SaveOutgoingTabState((int)tab);
|
|
LoadGraphIntoEditor(_openTabs[(int)tab].GraphResource);
|
|
|
|
ApplyVariablesPanelState((int)tab);
|
|
}
|
|
}
|
|
|
|
private void OnTabClosePressed(long tab)
|
|
{
|
|
if (tab >= 0 && tab < _openTabs.Count)
|
|
{
|
|
SaveOutgoingTabState(-1);
|
|
CloseTabByIndex((int)tab);
|
|
}
|
|
}
|
|
|
|
private void SyncConnectionsToGraph(StatescriptGraph graph)
|
|
{
|
|
if (_graphEdit is null || _isLoadingGraph)
|
|
{
|
|
return;
|
|
}
|
|
|
|
graph.Connections.Clear();
|
|
foreach (GodotCollections.Dictionary connection in _graphEdit.GetConnectionList())
|
|
{
|
|
var connectionResource = new StatescriptConnection
|
|
{
|
|
FromNode = connection["from_node"].AsString(),
|
|
OutputPort = connection["from_port"].AsInt32(),
|
|
ToNode = connection["to_node"].AsString(),
|
|
InputPort = connection["to_port"].AsInt32(),
|
|
};
|
|
|
|
graph.Connections.Add(connectionResource);
|
|
}
|
|
}
|
|
|
|
private void SyncConnectionsToCurrentGraph()
|
|
{
|
|
StatescriptGraph? graph = CurrentGraph;
|
|
if (graph is not null)
|
|
{
|
|
SyncConnectionsToGraph(graph);
|
|
}
|
|
}
|
|
|
|
private void SyncVisualNodePositionsToGraph()
|
|
{
|
|
if (_graphEdit is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (Node child in _graphEdit.GetChildren())
|
|
{
|
|
if (child is not StatescriptGraphNode sgn || sgn.NodeResource is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
sgn.NodeResource.PositionOffset = sgn.PositionOffset;
|
|
}
|
|
}
|
|
|
|
private void OnVariablesToggled(bool pressed)
|
|
{
|
|
if (_variablePanel is null || _tabBar is null || _openTabs.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_variablePanel.Visible = pressed;
|
|
|
|
var current = _tabBar.CurrentTab;
|
|
if (current >= 0 && current < _openTabs.Count)
|
|
{
|
|
_openTabs[current].VariablesPanelOpen = pressed;
|
|
}
|
|
|
|
if (pressed)
|
|
{
|
|
StatescriptGraph? graph = CurrentGraph;
|
|
if (graph is not null)
|
|
{
|
|
_variablePanel.SetGraph(graph);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnGraphVariablesChanged()
|
|
{
|
|
StatescriptGraph? graph = CurrentGraph;
|
|
if (graph is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LoadGraphIntoEditor(graph);
|
|
}
|
|
|
|
private void OnVariableUndoRedoPerformed()
|
|
{
|
|
EnsureVariablesPanelVisible();
|
|
}
|
|
|
|
private void OnVariableHighlightChanged(string? variableName)
|
|
{
|
|
if (_graphEdit is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (Node child in _graphEdit.GetChildren())
|
|
{
|
|
if (child is StatescriptGraphNode graphNode)
|
|
{
|
|
graphNode.SetHighlightedVariable(variableName);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void EnsureVariablesPanelVisible()
|
|
{
|
|
if (_variablePanel is null || _variablesToggleButton is null || _openTabs.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_variablePanel.Visible)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_variablePanel.Visible = true;
|
|
_variablesToggleButton.SetPressedNoSignal(true);
|
|
|
|
var current = _tabBar?.CurrentTab ?? -1;
|
|
if (current >= 0 && current < _openTabs.Count)
|
|
{
|
|
_openTabs[current].VariablesPanelOpen = true;
|
|
}
|
|
|
|
StatescriptGraph? graph = CurrentGraph;
|
|
if (graph is not null)
|
|
{
|
|
_variablePanel.SetGraph(graph);
|
|
}
|
|
}
|
|
|
|
private void OnFilesystemChanged()
|
|
{
|
|
for (var i = 0; i < _openTabs.Count; i++)
|
|
{
|
|
_openTabs[i].UpdateCachedPathIfMissing();
|
|
}
|
|
|
|
for (var i = _openTabs.Count - 1; i >= 0; i--)
|
|
{
|
|
var path = _openTabs[i].ResourcePath;
|
|
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var filePath = GetBaseFilePath(path);
|
|
if (!FileAccess.FileExists(filePath))
|
|
{
|
|
CloseTabByIndex(i);
|
|
}
|
|
}
|
|
|
|
RefreshTabTitles();
|
|
}
|
|
|
|
private void ApplyVariablesPanelState(int tabIndex)
|
|
{
|
|
if (_variablePanel is null || _variablesToggleButton is null
|
|
|| tabIndex < 0 || tabIndex >= _openTabs.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var shouldShow = _openTabs[tabIndex].VariablesPanelOpen;
|
|
_variablePanel.Visible = shouldShow;
|
|
_variablesToggleButton.SetPressedNoSignal(shouldShow);
|
|
|
|
if (shouldShow)
|
|
{
|
|
_variablePanel.SetGraph(_openTabs[tabIndex].GraphResource);
|
|
}
|
|
}
|
|
|
|
private void OnGraphEditGuiInput(InputEvent @event)
|
|
{
|
|
if (@event is InputEventKey { Pressed: true, Keycode: Key.D, CtrlPressed: true })
|
|
{
|
|
DuplicateSelectedNodes();
|
|
GetViewport().SetInputAsHandled();
|
|
}
|
|
}
|
|
|
|
private void DisconnectUISignals()
|
|
{
|
|
if (_tabBar is not null)
|
|
{
|
|
_tabBar.TabChanged -= OnTabChanged;
|
|
_tabBar.TabClosePressed -= OnTabClosePressed;
|
|
}
|
|
|
|
if (_graphEdit is not null)
|
|
{
|
|
_graphEdit.ConnectionRequest -= OnConnectionRequest;
|
|
_graphEdit.DisconnectionRequest -= OnDisconnectionRequest;
|
|
_graphEdit.DeleteNodesRequest -= OnDeleteNodesRequest;
|
|
_graphEdit.BeginNodeMove -= OnBeginNodeMove;
|
|
_graphEdit.EndNodeMove -= OnEndNodeMove;
|
|
_graphEdit.PopupRequest -= OnGraphEditPopupRequest;
|
|
_graphEdit.ConnectionToEmpty -= OnConnectionToEmpty;
|
|
_graphEdit.ConnectionFromEmpty -= OnConnectionFromEmpty;
|
|
_graphEdit.GuiInput -= OnGraphEditGuiInput;
|
|
}
|
|
|
|
if (_fileMenuPopup is not null)
|
|
{
|
|
_fileMenuPopup.IdPressed -= OnFileMenuIdPressed;
|
|
}
|
|
|
|
if (_addNodeButton is not null)
|
|
{
|
|
_addNodeButton.Pressed -= OnAddNodeButtonPressed;
|
|
}
|
|
|
|
if (_variablesToggleButton is not null)
|
|
{
|
|
_variablesToggleButton.Toggled -= OnVariablesToggled;
|
|
}
|
|
|
|
if (_onlineDocsButton is not null)
|
|
{
|
|
_onlineDocsButton.Pressed -= OnOnlineDocsPressed;
|
|
}
|
|
|
|
if (_addNodeDialog is not null)
|
|
{
|
|
_addNodeDialog.Canceled -= OnDialogCanceled;
|
|
_addNodeDialog.NodeCreationRequested -= OnDialogNodeCreationRequested;
|
|
}
|
|
|
|
if (_variablePanel is not null)
|
|
{
|
|
_variablePanel.VariablesChanged -= OnGraphVariablesChanged;
|
|
_variablePanel.VariableUndoRedoPerformed -= OnVariableUndoRedoPerformed;
|
|
_variablePanel.VariableHighlightChanged -= OnVariableHighlightChanged;
|
|
}
|
|
}
|
|
|
|
private void ConnectUISignals()
|
|
{
|
|
if (_tabBar is not null)
|
|
{
|
|
_tabBar.TabChanged += OnTabChanged;
|
|
_tabBar.TabClosePressed += OnTabClosePressed;
|
|
}
|
|
|
|
if (_graphEdit is not null)
|
|
{
|
|
_graphEdit.ConnectionRequest += OnConnectionRequest;
|
|
_graphEdit.DisconnectionRequest += OnDisconnectionRequest;
|
|
_graphEdit.DeleteNodesRequest += OnDeleteNodesRequest;
|
|
_graphEdit.BeginNodeMove += OnBeginNodeMove;
|
|
_graphEdit.EndNodeMove += OnEndNodeMove;
|
|
_graphEdit.PopupRequest += OnGraphEditPopupRequest;
|
|
_graphEdit.ConnectionToEmpty += OnConnectionToEmpty;
|
|
_graphEdit.ConnectionFromEmpty += OnConnectionFromEmpty;
|
|
_graphEdit.GuiInput += OnGraphEditGuiInput;
|
|
}
|
|
|
|
if (_fileMenuPopup is not null)
|
|
{
|
|
_fileMenuPopup.IdPressed += OnFileMenuIdPressed;
|
|
}
|
|
|
|
if (_addNodeButton is not null)
|
|
{
|
|
_addNodeButton.Pressed += OnAddNodeButtonPressed;
|
|
}
|
|
|
|
if (_variablesToggleButton is not null)
|
|
{
|
|
_variablesToggleButton.Toggled += OnVariablesToggled;
|
|
}
|
|
|
|
if (_onlineDocsButton is not null)
|
|
{
|
|
_onlineDocsButton.Pressed += OnOnlineDocsPressed;
|
|
}
|
|
|
|
if (_addNodeDialog is not null)
|
|
{
|
|
_addNodeDialog.Canceled += OnDialogCanceled;
|
|
_addNodeDialog.NodeCreationRequested += OnDialogNodeCreationRequested;
|
|
}
|
|
|
|
if (_variablePanel is not null)
|
|
{
|
|
_variablePanel.VariablesChanged += OnGraphVariablesChanged;
|
|
_variablePanel.VariableUndoRedoPerformed += OnVariableUndoRedoPerformed;
|
|
_variablePanel.VariableHighlightChanged += OnVariableHighlightChanged;
|
|
}
|
|
}
|
|
|
|
private int FindFirstEnabledInputPort(string nodeId)
|
|
{
|
|
if (_graphEdit is null)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
Node? child = _graphEdit.GetNodeOrNull(nodeId);
|
|
if (child is not GraphNode graphNode)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
for (var i = 0; i < graphNode.GetChildCount(); i++)
|
|
{
|
|
if (graphNode.IsSlotEnabledLeft(i))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private int FindFirstEnabledOutputPort(string nodeId)
|
|
{
|
|
if (_graphEdit is null)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
Node? child = _graphEdit.GetNodeOrNull(nodeId);
|
|
if (child is not GraphNode graphNode)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
for (var i = 0; i < graphNode.GetChildCount(); i++)
|
|
{
|
|
if (graphNode.IsSlotEnabledRight(i))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private sealed class GraphTab
|
|
{
|
|
private string _cachedPath;
|
|
|
|
public StatescriptGraph GraphResource { get; }
|
|
|
|
public string ResourcePath => !string.IsNullOrEmpty(GraphResource?.ResourcePath)
|
|
? GraphResource.ResourcePath
|
|
: _cachedPath;
|
|
|
|
public bool VariablesPanelOpen { get; set; }
|
|
|
|
public GraphTab(StatescriptGraph graphResource)
|
|
{
|
|
GraphResource = graphResource;
|
|
_cachedPath = graphResource?.ResourcePath ?? string.Empty;
|
|
}
|
|
|
|
public void UpdateCachedPathIfMissing()
|
|
{
|
|
if (GraphResource is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(GraphResource.ResourcePath))
|
|
{
|
|
_cachedPath = GraphResource.ResourcePath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|