// 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; using FlaxEngine.Utilities; namespace FlaxEditor.Windows { /// /// Visual Scripting debugger utility window. /// /// 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; } /// 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 before, List 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) { } /// 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; /// 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(); } /// public override void OnDestroy() { _debugToolstripControls = null; _tabs = null; Editor.Simulation.BreakpointHangBegin -= OnBreakpointHangBegin; Editor.Simulation.BreakpointHangEnd -= OnBreakpointHangEnd; base.OnDestroy(); } } }