501 lines
19 KiB
C#
501 lines
19 KiB
C#
// Copyright (c) 2012-2022 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();
|
|
}
|
|
}
|
|
}
|