You're breathtaking!
This commit is contained in:
495
Source/Editor/Windows/VisualScriptDebuggerWindow.cs
Normal file
495
Source/Editor/Windows/VisualScriptDebuggerWindow.cs
Normal file
@@ -0,0 +1,495 @@
|
||||
// Copyright (c) 2012-2020 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 Vector2 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, Vector2 location)
|
||||
{
|
||||
var menu = new ContextMenu();
|
||||
menu.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;
|
||||
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
|
||||
{
|
||||
Offsets = new Margin(0, 0, 0, 32),
|
||||
Parent = this
|
||||
};
|
||||
toolstrip.AddButton(editor.Icons.Docs32, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/visual/index.html")).LinkTooltip("See documentation to learn more");
|
||||
_debugToolstripControls = new[]
|
||||
{
|
||||
toolstrip.AddSeparator(),
|
||||
toolstrip.AddButton(editor.Icons.Play32, OnDebuggerContinue).LinkTooltip("Continue (F5)"),
|
||||
toolstrip.AddButton(editor.Icons.Find32, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"),
|
||||
toolstrip.AddButton(editor.Icons.Stop32, 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, 32, 0),
|
||||
TabsSize = new Vector2(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user