Files
FlaxEngine/Source/Editor/Windows/VisualScriptDebuggerWindow.cs
2023-01-10 15:29:37 +01:00

501 lines
19 KiB
C#

// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Tabs;
using FlaxEditor.GUI.Tree;
using FlaxEditor.Scripting;
using FlaxEditor.Surface;
using FlaxEditor.Windows.Assets;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Windows
{
/// <summary>
/// Visual Scripting debugger utility window.
/// </summary>
/// <seealso cref="FlaxEditor.Windows.EditorWindow" />
public sealed class VisualScriptDebuggerWindow : EditorWindow
{
private abstract class Tab : GUI.Tabs.Tab
{
protected class Node : TreeNode
{
private struct NodeInfo
{
public VisualScript Script;
public uint NodeId;
public NodeInfo(VisualScript script, uint nodeId)
{
Script = script;
NodeId = nodeId;
}
}
public Node(VisualScript script, uint nodeId)
: this(script, nodeId, SpriteHandle.Invalid, SpriteHandle.Invalid)
{
}
public Node(VisualScript script, uint nodeId, SpriteHandle iconCollapsed, SpriteHandle iconOpened)
: base(false, iconCollapsed, iconOpened)
{
Tag = new NodeInfo(script, nodeId);
}
public static SurfaceNode GetNode(object tag)
{
if (tag is NodeInfo nodeInfo)
{
VisualScriptWindow vsWindow = null;
foreach (var window in Editor.Instance.Windows.Windows)
{
if (window is VisualScriptWindow w && w.Asset == nodeInfo.Script)
{
vsWindow = (VisualScriptWindow)window;
break;
}
}
if (vsWindow == null)
{
var item = Editor.Instance.ContentDatabase.FindAsset(nodeInfo.Script.ID);
if (item != null)
vsWindow = Editor.Instance.ContentEditing.Open(item) as VisualScriptWindow;
}
return vsWindow?.Surface.FindNode(nodeInfo.NodeId);
}
return null;
}
/// <inheritdoc />
protected override bool OnMouseDoubleClickHeader(ref Float2 location, MouseButton button)
{
var node = GetNode(Tag);
((VisualScriptWindow)node?.Surface.Owner)?.ShowNode(node);
return true;
}
}
private Tree _tree;
protected TreeNode _rootNode;
protected Tab(string text)
: base(text)
{
var scrollPanel = new Panel(ScrollBars.Vertical)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
_tree = new Tree(false)
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
IsScrollable = true,
Parent = scrollPanel
};
_rootNode = new TreeNode(false);
_rootNode.Expand();
_rootNode.ChildrenIndent = 0;
_rootNode.Parent = _tree;
_tree.Margin = new Margin(0.0f, 0.0f, -14.0f, 2.0f); // Hide root node
_tree.SelectedChanged += OnTreeSelectedChanged;
_tree.RightClick += OnTreeRightClick;
}
private void OnTreeSelectedChanged(List<TreeNode> before, List<TreeNode> after)
{
if (after != null && after.Count == 1 && after[0] is Node treeNode)
{
var node = Node.GetNode(treeNode.Tag);
((VisualScriptWindow)node?.Surface.Owner)?.ShowNode(node);
}
}
private void OnTreeRightClick(TreeNode treeNode, Float2 location)
{
var menu = new ContextMenu
{
Tag = treeNode.Tag
};
menu.AddButton("Show node", button =>
{
var node = Node.GetNode(button.ParentContextMenu.Tag);
((VisualScriptWindow)node?.Surface.Owner)?.ShowNode(node);
}).Icon = Editor.Instance.Icons.Search12;
OnTreeNodeRightClick(menu);
menu.Show(treeNode, location);
}
protected virtual void OnTreeNodeRightClick(ContextMenu menu)
{
}
/// <inheritdoc />
public override void OnDestroy()
{
_tree = null;
base.OnDestroy();
}
}
private class CallStackTab : Tab
{
public CallStackTab()
: base("Call Stack")
{
Editor.Instance.Simulation.BreakpointHangBegin += OnBreakpointHangBegin;
Editor.Instance.Simulation.BreakpointHangEnd += OnBreakpointHangEnd;
}
private void OnBreakpointHangBegin()
{
var state = VisualScriptWindow.GetStackFrames();
if (state.StackFrames != null)
{
for (int i = 0; i < state.StackFrames.Length; i++)
{
ref var stackFrame = ref state.StackFrames[i];
VisualScriptWindow vsWindow = null;
foreach (var window in Editor.Instance.Windows.Windows)
{
if (window is VisualScriptWindow w && w.Asset == stackFrame.Script)
{
vsWindow = (VisualScriptWindow)window;
break;
}
}
var node = vsWindow?.Surface.FindNode(stackFrame.NodeId);
string text;
if (node != null)
text = $"{Path.GetFileNameWithoutExtension(stackFrame.Script.Path)} in node {node.Title}";
else
text = $"{Path.GetFileNameWithoutExtension(stackFrame.Script.Path)} in nodeId {stackFrame.NodeId}";
var icon = Editor.Instance.Icons.ArrowRight12;
new Node(stackFrame.Script, stackFrame.NodeId, icon, icon)
{
Text = text,
Parent = _rootNode,
IconColor = i == 0 ? Color.Yellow : Color.Transparent,
};
}
}
}
private void OnBreakpointHangEnd()
{
_rootNode.DisposeChildren();
}
public override void OnDestroy()
{
Editor.Instance.Simulation.BreakpointHangBegin -= OnBreakpointHangBegin;
Editor.Instance.Simulation.BreakpointHangEnd -= OnBreakpointHangEnd;
base.OnDestroy();
}
}
private class LocalsTab : Tab
{
public LocalsTab()
: base("Locals")
{
Editor.Instance.Simulation.BreakpointHangBegin += OnBreakpointHangBegin;
Editor.Instance.Simulation.BreakpointHangEnd += OnBreakpointHangEnd;
}
protected override void OnTreeNodeRightClick(ContextMenu menu)
{
menu.AddSeparator();
menu.AddButton("Copy value", button =>
{
var node = (SurfaceNode)button.ParentContextMenu.Tag;
var state = VisualScriptWindow.GetLocals();
var local = state.Locals.First(x => x.NodeId == node.ID);
Clipboard.Text = local.Value ?? string.Empty;
});
}
private void OnBreakpointHangBegin()
{
var state = VisualScriptWindow.GetLocals();
if (state.Locals != null)
{
for (int i = 0; i < state.Locals.Length; i++)
{
ref var local = ref state.Locals[i];
var node = state.Window.Surface.FindNode(local.NodeId);
var box = node?.GetBox(local.BoxId);
if (box != null)
{
var name = string.IsNullOrEmpty(box.Text) ? node.Title : node.Title + " : " + box.Text;
var value = local.Value ?? string.Empty;
var type = TypeUtils.GetType(local.ValueTypeName);
node.Surface.Style.GetConnectionColor(type, node.Archetype.ConnectionsHints, out var typeColor);
var icon = node.Surface.Style.Icons.BoxClose;
new Node(state.Window.Asset, local.NodeId, icon, icon)
{
Text = name + " = " + value,
Parent = _rootNode,
IconColor = typeColor,
TooltipText = type.TypeName,
};
}
}
}
}
private void OnBreakpointHangEnd()
{
_rootNode.DisposeChildren();
}
public override void OnDestroy()
{
Editor.Instance.Simulation.BreakpointHangBegin -= OnBreakpointHangBegin;
Editor.Instance.Simulation.BreakpointHangEnd -= OnBreakpointHangEnd;
base.OnDestroy();
}
}
private class BreakpointsTab : Tab
{
public BreakpointsTab()
: base("Breakpoints")
{
foreach (var window in Editor.Instance.Windows.Windows)
OnWindowAdded(window);
Editor.Instance.Windows.WindowAdded += OnWindowAdded;
Editor.Instance.Windows.WindowRemoved += OnWindowRemoved;
}
protected override void OnTreeNodeRightClick(ContextMenu menu)
{
menu.AddSeparator();
menu.AddButton("Remove breakpoint", button =>
{
var node = (SurfaceNode)button.ParentContextMenu.Tag;
node.Breakpoint.Set = !node.Breakpoint.Set;
node.Breakpoint.Enabled = true;
node.Surface.OnNodeBreakpointEdited(node);
}).Icon = Editor.Instance.Icons.Cross12;
menu.AddButton("Toggle breakpoint", button =>
{
var node = (SurfaceNode)button.ParentContextMenu.Tag;
node.Breakpoint.Enabled = !node.Breakpoint.Enabled;
node.Surface.OnNodeBreakpointEdited(node);
});
menu.AddSeparator();
menu.AddButton("Delete all breakpoints", () =>
{
foreach (var child in _rootNode.Children.ToArray())
{
if (child is TreeNode n && n.Tag is SurfaceNode node)
{
node.Breakpoint.Set = false;
node.Surface.OnNodeBreakpointEdited(node);
}
}
});
menu.AddButton("Enable all breakpoints", () =>
{
foreach (var child in _rootNode.Children)
{
if (child is TreeNode n && n.Tag is SurfaceNode node && !node.Breakpoint.Enabled)
{
node.Breakpoint.Enabled = true;
node.Surface.OnNodeBreakpointEdited(node);
}
}
});
menu.AddButton("Disable all breakpoints", () =>
{
foreach (var child in _rootNode.Children.ToArray())
{
if (child is TreeNode n && n.Tag is SurfaceNode node && node.Breakpoint.Enabled)
{
node.Breakpoint.Enabled = false;
node.Surface.OnNodeBreakpointEdited(node);
}
}
});
}
private void OnWindowAdded(EditorWindow window)
{
if (window is VisualScriptWindow vsWindow)
{
FlaxEngine.Scripting.RunOnUpdate(() =>
{
vsWindow.Surface.NodeBreakpointEdited += OnSurfaceNodeBreakpointEdited;
if (vsWindow.Surface.Nodes != null)
{
foreach (var node in vsWindow.Surface.Nodes)
if (node.Breakpoint.Set)
OnSurfaceNodeBreakpointEdited(node);
}
});
}
}
private void OnSurfaceNodeBreakpointEdited(SurfaceNode node)
{
var vsWindow = (VisualScriptWindow)node.Surface.Owner;
var bpNode = _rootNode.Children.FirstOrDefault(x => x.Tag == node) as TreeNode;
if (node.Breakpoint.Set)
{
if (bpNode == null)
{
bpNode = new Node(vsWindow.Asset, node.ID, node.Surface.Style.Icons.BoxOpen, node.Surface.Style.Icons.BoxClose)
{
Text = vsWindow.Item.ShortName + " at " + node.Title,
Parent = _rootNode,
IconColor = new Color(0.894117647f, 0.0784313725f, 0.0f),
};
}
bpNode.IsExpanded = node.Breakpoint.Enabled;
}
else
bpNode?.Dispose();
}
private void OnWindowRemoved(EditorWindow window)
{
if (window is VisualScriptWindow vsWindow)
{
vsWindow.Surface.NodeBreakpointEdited -= OnSurfaceNodeBreakpointEdited;
for (var i = _rootNode.Children.Count - 1; i >= 0; i--)
{
var bpNode = _rootNode.Children[i];
if (bpNode.Tag is SurfaceNode node && node.Surface.Owner == vsWindow)
{
_rootNode.RemoveChild(bpNode);
}
}
}
}
public override void OnDestroy()
{
Editor.Instance.Windows.WindowAdded -= OnWindowAdded;
Editor.Instance.Windows.WindowRemoved -= OnWindowRemoved;
base.OnDestroy();
}
}
private Control[] _debugToolstripControls;
private Tab[] _tabs;
/// <inheritdoc />
public VisualScriptDebuggerWindow(Editor editor)
: base(editor, true, ScrollBars.None)
{
Title = "Visual Script Debugger";
var toolstrip = new ToolStrip
{
Parent = this
};
toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/visual/index.html")).LinkTooltip("See documentation to learn more");
_debugToolstripControls = new[]
{
toolstrip.AddSeparator(),
toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip("Continue (F5)"),
toolstrip.AddButton(editor.Icons.Search64, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"),
toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"),
};
foreach (var control in _debugToolstripControls)
control.Visible = Editor.Simulation.IsDuringBreakpointHang;
var tabs = new Tabs
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = new Margin(0, 0, toolstrip.Bottom, 0),
TabsSize = new Float2(80, 20),
TabsTextHorizontalAlignment = TextAlignment.Center,
UseScroll = true,
Parent = this
};
_tabs = new Tab[]
{
new CallStackTab(),
new LocalsTab(),
new BreakpointsTab(),
};
foreach (var tab in _tabs)
tabs.AddTab(tab);
InputActions.Add(options => options.DebuggerContinue, OnDebuggerContinue);
Editor.Simulation.BreakpointHangBegin += OnBreakpointHangBegin;
Editor.Simulation.BreakpointHangEnd += OnBreakpointHangEnd;
}
private void OnBreakpointHangBegin()
{
foreach (var control in _debugToolstripControls)
control.Visible = true;
}
private void OnBreakpointHangEnd()
{
foreach (var control in _debugToolstripControls)
control.Visible = false;
}
private void OnDebuggerContinue()
{
if (!Editor.Simulation.IsDuringBreakpointHang)
return;
// Stop hang
Editor.Simulation.StopBreakpointHang();
}
private void OnDebuggerNavigateToCurrentNode()
{
if (!Editor.Simulation.IsDuringBreakpointHang)
return;
// Focus node
var state = (VisualScriptWindow.BreakpointHangState)Editor.Simulation.BreakpointHangTag;
state.Window.SelectTab();
state.Window.RootWindow.Focus();
state.Window.Surface.Focus();
state.Window.Surface.FocusNode(state.Node);
}
private void OnDebuggerStop()
{
if (!Editor.Simulation.IsDuringBreakpointHang)
return;
// Stop play
Editor.Simulation.RequestStopPlay();
}
/// <inheritdoc />
public override void OnDestroy()
{
_debugToolstripControls = null;
_tabs = null;
Editor.Simulation.BreakpointHangBegin -= OnBreakpointHangBegin;
Editor.Simulation.BreakpointHangEnd -= OnBreakpointHangEnd;
base.OnDestroy();
}
}
}