With the new icons the clickable images for adding functions for the Visject surface got too small and unreadable. Instead using more visible buttons should counteract this.
1164 lines
43 KiB
C#
1164 lines
43 KiB
C#
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Xml;
|
|
using FlaxEditor.Content;
|
|
using FlaxEditor.CustomEditors;
|
|
using FlaxEditor.CustomEditors.Editors;
|
|
using FlaxEditor.GUI;
|
|
using FlaxEditor.GUI.ContextMenu;
|
|
using FlaxEditor.Scripting;
|
|
using FlaxEditor.Surface;
|
|
using FlaxEngine;
|
|
using FlaxEngine.GUI;
|
|
using FlaxEngine.Json;
|
|
|
|
#pragma warning disable 649
|
|
|
|
// ReSharper disable UnusedMember.Local
|
|
// ReSharper disable UnusedMember.Global
|
|
// ReSharper disable MemberCanBePrivate.Local
|
|
|
|
namespace FlaxEditor.Windows.Assets
|
|
{
|
|
/// <summary>
|
|
/// VisualScript window allows to view and edit <see cref="VisualScript"/> asset.
|
|
/// </summary>
|
|
/// <seealso cref="VisualScript" />
|
|
/// <seealso cref="VisualScriptSurface" />
|
|
public sealed class VisualScriptWindow : AssetEditorWindowBase<VisualScript>, IVisjectSurfaceWindow
|
|
{
|
|
private struct BreakpointData
|
|
{
|
|
public uint NodeId;
|
|
public bool Enabled;
|
|
}
|
|
|
|
private sealed class PropertiesProxy
|
|
{
|
|
[EditorOrder(0), EditorDisplay("Options"), CustomEditor(typeof(OptionsEditor)), NoSerialize]
|
|
public VisualScriptWindow Window2;
|
|
|
|
[EditorOrder(10), EditorDisplay("Options"), TypeReference(typeof(FlaxEngine.Object), nameof(IsValid)), NoSerialize]
|
|
[Tooltip("The base class of the new Visual Script to inherit from.")]
|
|
public Type BaseClass;
|
|
|
|
[EditorOrder(20), EditorDisplay("Options")]
|
|
[Tooltip("If checked, the class is abstract and cannot be instantiated directly.")]
|
|
public bool IsAbstract;
|
|
|
|
[EditorOrder(30), EditorDisplay("Options")]
|
|
[Tooltip("If checked, the class is sealed and cannot be inherited by other scripts.")]
|
|
public bool IsSealed;
|
|
|
|
[EditorOrder(900), CustomEditor(typeof(FunctionsEditor)), NoSerialize]
|
|
public VisualScriptWindow Window1;
|
|
|
|
[EditorOrder(1000), EditorDisplay("Parameters"), CustomEditor(typeof(ParametersEditor)), NoSerialize]
|
|
public VisualScriptWindow Window;
|
|
|
|
[HideInEditor]
|
|
public object[] Parameters
|
|
{
|
|
get
|
|
{
|
|
var parameters = Window.Surface.Parameters;
|
|
var result = new object[parameters.Count];
|
|
for (int i = 0; i < result.Length; i++)
|
|
result[i] = parameters[i].Value;
|
|
return result;
|
|
}
|
|
set
|
|
{
|
|
var parameters = Window.Surface.Parameters;
|
|
if (value != null && parameters.Count == value.Length)
|
|
{
|
|
for (int i = 0; i < parameters.Count; i++)
|
|
parameters[i].Value = value[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool IsValid(Type type)
|
|
{
|
|
return type.IsPublic && !type.IsSealed && !type.IsGenericType;
|
|
}
|
|
|
|
public void OnLoad(VisualScriptWindow window)
|
|
{
|
|
var asset = window.Asset;
|
|
var meta = asset.Meta;
|
|
BaseClass = TypeUtils.GetType(meta.BaseTypename).Type;
|
|
IsAbstract = (meta.Flags & VisualScript.Flags.Abstract) != 0;
|
|
IsSealed = (meta.Flags & VisualScript.Flags.Sealed) != 0;
|
|
Window = Window1 = Window2 = window;
|
|
}
|
|
|
|
public void OnClean()
|
|
{
|
|
Window = Window1 = Window2 = null;
|
|
}
|
|
}
|
|
|
|
class OptionsEditor : CustomEditor
|
|
{
|
|
/// <inheritdoc />
|
|
public override DisplayStyle Style => DisplayStyle.InlineIntoParent;
|
|
|
|
/// <inheritdoc />
|
|
public override void Initialize(LayoutElementsContainer layout)
|
|
{
|
|
var group = (CustomEditors.Elements.GroupElement)layout;
|
|
|
|
// Options button
|
|
const float buttonSize = 14;
|
|
var optionsButton = new Image
|
|
{
|
|
TooltipText = "Script options",
|
|
AutoFocus = true,
|
|
AnchorPreset = AnchorPresets.TopRight,
|
|
Parent = group.Panel,
|
|
Bounds = new Rectangle(group.Panel.Width - buttonSize, 0, buttonSize, buttonSize),
|
|
IsScrollable = false,
|
|
Color = FlaxEngine.GUI.Style.Current.ForegroundGrey,
|
|
Margin = new Margin(1),
|
|
Brush = new SpriteBrush(Editor.Instance.Icons.Settings12),
|
|
};
|
|
optionsButton.Clicked += OnOptionsButtonClicked;
|
|
}
|
|
|
|
private void OnOptionsButtonClicked(Image image, MouseButton button)
|
|
{
|
|
if (button != MouseButton.Left)
|
|
return;
|
|
|
|
var cm = new ContextMenu();
|
|
cm.AddButton("Copy typename", () => Clipboard.Text = ((VisualScriptWindow)Values[0]).Asset.ScriptTypeName);
|
|
cm.AddButton("Edit attributes...", OnEditAttributes);
|
|
cm.Show(image, new Vector2(0, image.Height));
|
|
}
|
|
|
|
private void OnEditAttributes(ContextMenuButton button)
|
|
{
|
|
var window = (VisualScriptWindow)Values[0];
|
|
var attributes = window.Surface.Context.Meta.GetAttributes();
|
|
var editor = new AttributesEditor(attributes, NodeFactory.TypeAttributeTypes);
|
|
editor.Edited += newValue =>
|
|
{
|
|
var action = new EditSurfaceAttributesAction
|
|
{
|
|
Window = (IVisjectSurfaceWindow)window.Surface.Owner,
|
|
Before = window.Surface.Context.Meta.GetAttributes(),
|
|
After = newValue,
|
|
};
|
|
action.Window.Undo?.AddAction(action);
|
|
action.Do();
|
|
};
|
|
editor.Show(window, window.Surface.Parent.UpperRight);
|
|
}
|
|
}
|
|
|
|
class FunctionsEditor : CustomEditor
|
|
{
|
|
/// <inheritdoc />
|
|
public override DisplayStyle Style => DisplayStyle.InlineIntoParent;
|
|
|
|
private Control _overrideButton;
|
|
|
|
/// <inheritdoc />
|
|
public override void Initialize(LayoutElementsContainer layout)
|
|
{
|
|
var window = Values[0] as IVisjectSurfaceWindow;
|
|
var asset = window?.VisjectAsset;
|
|
if (asset == null || !asset.IsLoaded)
|
|
return;
|
|
|
|
var group = layout.Group("Functions");
|
|
var nodes = window.VisjectSurface.Nodes;
|
|
|
|
var grid = group.CustomContainer<UniformGridPanel>();
|
|
var gridControl = grid.CustomControl;
|
|
gridControl.ClipChildren = false;
|
|
gridControl.Height = Button.DefaultHeight;
|
|
gridControl.SlotsHorizontally = 2;
|
|
gridControl.SlotsVertically = 1;
|
|
|
|
var addOverride = grid.Button("Add Override");
|
|
addOverride.Button.Clicked += OnOverrideMethodClicked;
|
|
// TODO: Add sender arg to button clicked action?
|
|
_overrideButton = addOverride.Control;
|
|
|
|
var addFuncction = grid.Button("Add Function");
|
|
addFuncction.Button.Clicked += OnAddNewFunctionClicked;
|
|
|
|
// List of functions in the graph
|
|
for (int i = 0; i < nodes.Count; i++)
|
|
{
|
|
var node = nodes[i];
|
|
if (node is Surface.Archetypes.Function.VisualScriptFunctionNode functionNode && !string.IsNullOrEmpty(functionNode._signature.Name))
|
|
{
|
|
var label = group.ClickableLabel(functionNode._signature.Name ?? string.Empty).CustomControl;
|
|
label.TextColorHighlighted = Color.FromBgra(0xFFA0A0A0);
|
|
label.TooltipText = functionNode.TooltipText;
|
|
label.DoubleClick += () => ((VisualScriptWindow)Values[0]).Surface.FocusNode(functionNode);
|
|
label.RightClick += () => ShowContextMenu(functionNode, label);
|
|
}
|
|
else if (node is Surface.Archetypes.Function.MethodOverrideNode overrideNode)
|
|
{
|
|
var label = group.ClickableLabel($"{overrideNode.Title} (override)").CustomControl;
|
|
label.TextColorHighlighted = Color.FromBgra(0xFFA0A0A0);
|
|
label.TooltipText = overrideNode.TooltipText;
|
|
label.DoubleClick += () => ((VisualScriptWindow)Values[0]).Surface.FocusNode(overrideNode);
|
|
label.RightClick += () => ShowContextMenu(overrideNode, label);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ShowContextMenu(SurfaceNode node, ClickableLabel label)
|
|
{
|
|
// TODO: Execute only on "Edit Signature...", this makes the "Show" button useless
|
|
((VisualScriptWindow)Values[0]).Surface.FocusNode(node);
|
|
|
|
var cm = new ContextMenu();
|
|
cm.AddButton("Show", () => ((VisualScriptWindow)Values[0]).Surface.FocusNode(node)).Icon = Editor.Instance.Icons.Search12;
|
|
cm.AddButton("Delete", () => ((VisualScriptWindow)Values[0]).Surface.Delete(node)).Icon = Editor.Instance.Icons.Cross12;
|
|
node.OnShowSecondaryContextMenu(cm, Vector2.Zero);
|
|
cm.Show(label, new Vector2(0, label.Height));
|
|
}
|
|
|
|
private void OnAddNewFunctionClicked()
|
|
{
|
|
var surface = ((VisualScriptWindow)Values[0]).Surface;
|
|
var surfaceBounds = surface.AllNodesBounds;
|
|
surface.ShowArea(new Rectangle(surfaceBounds.BottomLeft, new Vector2(200, 150)).MakeExpanded(400.0f));
|
|
var node = surface.Context.SpawnNode(16, 6, surfaceBounds.BottomLeft + new Vector2(0, 50));
|
|
surface.Select(node);
|
|
}
|
|
|
|
private void OnOverrideMethodClicked()
|
|
{
|
|
var cm = new ContextMenu();
|
|
var window = (VisualScriptWindow)Values[0];
|
|
var scriptMeta = window.Asset.Meta;
|
|
var baseType = TypeUtils.GetType(scriptMeta.BaseTypename);
|
|
var members = baseType.GetMembers(BindingFlags.Public | BindingFlags.Instance);
|
|
for (var i = 0; i < members.Length; i++)
|
|
{
|
|
ref var member = ref members[i];
|
|
if (SurfaceUtils.IsValidVisualScriptOverrideMethod(member, out var parameters))
|
|
{
|
|
var name = member.Name;
|
|
|
|
// Skip already added override method nodes
|
|
bool isAlreadyAdded = false;
|
|
foreach (var n in window.Surface.Nodes)
|
|
{
|
|
if (n.GroupArchetype.GroupID == 16 &&
|
|
n.Archetype.TypeID == 3 &&
|
|
(string)n.Values[0] == name &&
|
|
(int)n.Values[1] == parameters.Length)
|
|
{
|
|
isAlreadyAdded = true;
|
|
break;
|
|
}
|
|
}
|
|
if (isAlreadyAdded)
|
|
continue;
|
|
|
|
var cmButton = cm.AddButton($"{name} (in {member.DeclaringType.Name})");
|
|
var attributes = member.GetAttributes(true);
|
|
var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute);
|
|
if (tooltipAttribute != null)
|
|
cmButton.TooltipText = tooltipAttribute.Text;
|
|
cmButton.Clicked += () =>
|
|
{
|
|
var surface = ((VisualScriptWindow)Values[0]).Surface;
|
|
var surfaceBounds = surface.AllNodesBounds;
|
|
surface.ShowArea(new Rectangle(surfaceBounds.BottomLeft, new Vector2(200, 150)).MakeExpanded(400.0f));
|
|
var node = surface.Context.SpawnNode(16, 3, surfaceBounds.BottomLeft + new Vector2(0, 50), new object[]
|
|
{
|
|
name,
|
|
parameters.Length,
|
|
Utils.GetEmptyArray<byte>()
|
|
});
|
|
surface.Select(node);
|
|
};
|
|
}
|
|
}
|
|
if (!cm.Items.Any())
|
|
{
|
|
cm.AddButton("Nothing to override");
|
|
}
|
|
cm.Show(_overrideButton, new Vector2(0, _overrideButton.Height));
|
|
}
|
|
}
|
|
|
|
private readonly PropertiesProxy _properties;
|
|
private readonly SplitPanel _split;
|
|
private CustomEditorPresenter _propertiesEditor;
|
|
private VisualScriptSurface _surface;
|
|
private FlaxObjectRefPickerControl _debugObjectPicker;
|
|
private Undo _undo;
|
|
private readonly ToolStripButton _saveButton;
|
|
private readonly ToolStripButton _undoButton;
|
|
private readonly ToolStripButton _redoButton;
|
|
private Control[] _debugToolstripControls;
|
|
private bool _showWholeGraphOnLoad = true;
|
|
private bool _tmpAssetIsDirty;
|
|
private bool _isWaitingForSurfaceLoad;
|
|
private bool _refreshPropertiesOnLoad;
|
|
private bool _paramValueChange;
|
|
private bool _canEdit = true;
|
|
private List<uint> _debugStepOverNodesIds;
|
|
private List<KeyValuePair<VisualScript, uint>> _debugStepOutNodesIds;
|
|
private bool _debugBreakNext;
|
|
private ulong _debugBreakNextThreadId;
|
|
private readonly List<Editor.VisualScriptingDebugFlowInfo> _debugFlows = new List<Editor.VisualScriptingDebugFlowInfo>();
|
|
|
|
/// <summary>
|
|
/// Gets the Visject Surface.
|
|
/// </summary>
|
|
public VisualScriptSurface Surface => _surface;
|
|
|
|
/// <summary>
|
|
/// Gets the undo history context for this window.
|
|
/// </summary>
|
|
public Undo Undo => _undo;
|
|
|
|
/// <inheritdoc />
|
|
public VisualScriptWindow(Editor editor, VisualScriptItem item)
|
|
: base(editor, item)
|
|
{
|
|
var isPlayMode = Editor.IsPlayMode;
|
|
|
|
// Undo
|
|
_undo = new Undo();
|
|
_undo.UndoDone += OnUndoRedo;
|
|
_undo.RedoDone += OnUndoRedo;
|
|
_undo.ActionDone += OnUndoRedo;
|
|
|
|
// Split Panel
|
|
_split = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
|
|
{
|
|
AnchorPreset = AnchorPresets.StretchAll,
|
|
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
|
|
SplitterValue = 0.7f,
|
|
Parent = this
|
|
};
|
|
|
|
// Surface
|
|
_surface = new VisualScriptSurface(this, Save, _undo)
|
|
{
|
|
Parent = _split.Panel1,
|
|
Enabled = false
|
|
};
|
|
_surface.NodeSpawned += OnNodeValuesChange;
|
|
_surface.NodeValuesEdited += OnNodeValuesChange;
|
|
_surface.NodeDeleted += OnNodeValuesChange;
|
|
|
|
// Properties editor
|
|
_propertiesEditor = new CustomEditorPresenter(_undo);
|
|
_propertiesEditor.Panel.Parent = _split.Panel2;
|
|
_propertiesEditor.Modified += OnPropertyEdited;
|
|
_properties = new PropertiesProxy();
|
|
_propertiesEditor.Select(_properties);
|
|
|
|
// Toolstrip
|
|
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
|
|
_toolstrip.AddSeparator();
|
|
_undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
|
|
_redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
|
|
_toolstrip.AddSeparator();
|
|
_toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph");
|
|
_toolstrip.AddSeparator();
|
|
_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.Right64, OnDebuggerStepOver).LinkTooltip("Step Over (F10)"),
|
|
_toolstrip.AddButton(editor.Icons.Down64, OnDebuggerStepInto).LinkTooltip("Step Into (F11)"),
|
|
_toolstrip.AddButton(editor.Icons.Up64, OnDebuggerStepOut).LinkTooltip("Step Out (Shift+F11)"),
|
|
_toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"),
|
|
};
|
|
foreach (var control in _debugToolstripControls)
|
|
control.Visible = false;
|
|
|
|
// Debug object picker
|
|
var debugObjectPickerContainer = new ContainerControl();
|
|
var debugObjectPickerLabel = new Label
|
|
{
|
|
AnchorPreset = AnchorPresets.VerticalStretchLeft,
|
|
VerticalAlignment = TextAlignment.Center,
|
|
HorizontalAlignment = TextAlignment.Far,
|
|
Parent = debugObjectPickerContainer,
|
|
Size = new Vector2(60.0f, _toolstrip.Height),
|
|
Text = "Debug:",
|
|
TooltipText = "The Visual Script object instance to debug. Can be used to filter the logic to debug a specific instance.",
|
|
};
|
|
_debugObjectPicker = new FlaxObjectRefPickerControl
|
|
{
|
|
Location = new Vector2(debugObjectPickerLabel.Right + 4.0f, 8.0f),
|
|
Width = 140.0f,
|
|
Type = item.ScriptType,
|
|
Parent = debugObjectPickerContainer,
|
|
};
|
|
debugObjectPickerContainer.Enabled = debugObjectPickerContainer.Visible = isPlayMode;
|
|
debugObjectPickerContainer.Size = new Vector2(_debugObjectPicker.Right + 2.0f, _toolstrip.Height);
|
|
debugObjectPickerContainer.Parent = _toolstrip;
|
|
|
|
// Setup input actions
|
|
InputActions.Add(options => options.Undo, _undo.PerformUndo);
|
|
InputActions.Add(options => options.Redo, _undo.PerformRedo);
|
|
InputActions.Add(options => options.DebuggerContinue, OnDebuggerContinue);
|
|
InputActions.Add(options => options.DebuggerStepOver, OnDebuggerStepOver);
|
|
InputActions.Add(options => options.DebuggerStepOut, OnDebuggerStepOut);
|
|
InputActions.Add(options => options.DebuggerStepInto, OnDebuggerStepInto);
|
|
|
|
SetCanEdit(!isPlayMode);
|
|
}
|
|
|
|
private void OnNodeValuesChange(SurfaceNode node)
|
|
{
|
|
if (node is Surface.Archetypes.Function.VisualScriptFunctionNode || node is Surface.Archetypes.Function.MethodOverrideNode)
|
|
{
|
|
_propertiesEditor.BuildLayoutOnUpdate();
|
|
}
|
|
}
|
|
|
|
private void OnUndoRedo(IUndoAction action)
|
|
{
|
|
_paramValueChange = false;
|
|
MarkAsEdited();
|
|
UpdateToolstrip();
|
|
_propertiesEditor.BuildLayoutOnUpdate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the asset properties proxy object gets edited.
|
|
/// </summary>
|
|
private void OnPropertyEdited()
|
|
{
|
|
_surface.MarkAsEdited(!_paramValueChange);
|
|
_paramValueChange = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the whole surface graph.
|
|
/// </summary>
|
|
public void ShowWholeGraph()
|
|
{
|
|
_surface.ShowWholeGraph();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes temporary asset to see changes live when editing the surface.
|
|
/// </summary>
|
|
/// <returns>True if cannot refresh it, otherwise false.</returns>
|
|
public bool RefreshTempAsset()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Save()
|
|
{
|
|
// Early check
|
|
if (!IsEdited || _asset == null || _isWaitingForSurfaceLoad)
|
|
return;
|
|
|
|
// Check if surface has been edited
|
|
if (_surface.IsEdited)
|
|
{
|
|
if (SaveSurface())
|
|
return;
|
|
}
|
|
|
|
ClearEditedFlag();
|
|
OnSurfaceEditedChanged();
|
|
_item.RefreshThumbnail();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void UpdateToolstrip()
|
|
{
|
|
_saveButton.Enabled = _canEdit && IsEdited;
|
|
_undoButton.Enabled = _canEdit && _undo.CanUndo;
|
|
_redoButton.Enabled = _canEdit && _undo.CanRedo;
|
|
|
|
base.UpdateToolstrip();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnAssetLinked()
|
|
{
|
|
_surface.Script = Asset;
|
|
_isWaitingForSurfaceLoad = true;
|
|
_refreshPropertiesOnLoad = false;
|
|
|
|
base.OnAssetLinked();
|
|
}
|
|
|
|
private void OnDebugFlow(Editor.VisualScriptingDebugFlowInfo flowInfo)
|
|
{
|
|
// Skip any debug flows during hang
|
|
if (Editor.Simulation.IsDuringBreakpointHang)
|
|
return;
|
|
|
|
// Check if moving into next flow
|
|
if (_debugBreakNext && _debugBreakNextThreadId == Platform.CurrentThreadID)
|
|
{
|
|
_debugBreakNext = false;
|
|
var item = Editor.ContentDatabase.Find(flowInfo.Script.ID);
|
|
if (item != null)
|
|
{
|
|
var editor = Editor.ContentEditing.Open(item);
|
|
if (editor is VisualScriptWindow vsWindow)
|
|
{
|
|
lock (vsWindow._debugFlows)
|
|
vsWindow._debugFlows.Add(flowInfo);
|
|
vsWindow.OnDebugBreakpointHit(ref flowInfo, vsWindow.Surface.FindNode(flowInfo.NodeId));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check if moving outside the current scope
|
|
if (_debugStepOutNodesIds != null && _debugStepOutNodesIds.Contains(new KeyValuePair<VisualScript, uint>(flowInfo.Script, flowInfo.NodeId)))
|
|
{
|
|
_debugStepOutNodesIds.Clear();
|
|
var item = Editor.ContentDatabase.Find(flowInfo.Script.ID);
|
|
if (item != null)
|
|
{
|
|
var editor = Editor.ContentEditing.Open(item);
|
|
if (editor is VisualScriptWindow vsWindow)
|
|
{
|
|
lock (vsWindow._debugFlows)
|
|
vsWindow._debugFlows.Add(flowInfo);
|
|
vsWindow.OnDebugBreakpointHit(ref flowInfo, vsWindow.Surface.FindNode(flowInfo.NodeId));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Filter the flow
|
|
if (_debugObjectPicker.Value != null && flowInfo.ScriptInstance != null && _debugObjectPicker.Value != flowInfo.ScriptInstance)
|
|
return;
|
|
if (flowInfo.Script != Asset)
|
|
return;
|
|
|
|
// Register flow to show it in UI on a surface
|
|
lock (_debugFlows)
|
|
{
|
|
_debugFlows.Add(flowInfo);
|
|
}
|
|
|
|
// Check if moving over the flow with debugger
|
|
if (_debugStepOverNodesIds != null && _debugStepOverNodesIds.Contains(flowInfo.NodeId))
|
|
{
|
|
OnDebugBreakpointHit(ref flowInfo, Surface.FindNode(flowInfo.NodeId));
|
|
return;
|
|
}
|
|
|
|
// Check if any breakpoint was hit
|
|
for (int i = 0; i < Surface.Breakpoints.Count; i++)
|
|
{
|
|
if (Surface.Breakpoints[i].ID == flowInfo.NodeId)
|
|
{
|
|
OnDebugBreakpointHit(ref flowInfo, Surface.Breakpoints[i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal struct BreakpointHangState
|
|
{
|
|
public SurfaceNode Node;
|
|
public VisualScriptWindow Window;
|
|
public Editor.VisualScriptLocal[] Locals;
|
|
public Editor.VisualScriptStackFrame[] StackFrames;
|
|
}
|
|
|
|
internal static BreakpointHangState GetLocals()
|
|
{
|
|
var state = (BreakpointHangState)Editor.Instance.Simulation.BreakpointHangTag;
|
|
if (state.Locals == null)
|
|
{
|
|
state.Locals = Editor.Internal_GetVisualScriptLocals();
|
|
Editor.Instance.Simulation.BreakpointHangTag = state;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
internal static BreakpointHangState GetStackFrames()
|
|
{
|
|
var state = (BreakpointHangState)Editor.Instance.Simulation.BreakpointHangTag;
|
|
if (state.StackFrames == null)
|
|
{
|
|
state.StackFrames = Editor.Internal_GetVisualScriptStackFrames();
|
|
Editor.Instance.Simulation.BreakpointHangTag = state;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
private void OnDebugBreakpointHit(ref Editor.VisualScriptingDebugFlowInfo flowInfo, SurfaceNode node)
|
|
{
|
|
if (!Platform.IsInMainThread || GPUDevice.Instance.IsRendering)
|
|
return;
|
|
_debugStepOverNodesIds?.Clear();
|
|
_debugStepOutNodesIds?.Clear();
|
|
_debugBreakNext = false;
|
|
|
|
Editor.Log(string.Format("Visual Script breakpoint hit at node '{0}' in script '{1}'{2} (thread: 0x{3:x})", node.Title, Item.ShortName, flowInfo.ScriptInstance ? $" (instance {flowInfo.ScriptInstance})" : "", Platform.CurrentThreadID));
|
|
|
|
try
|
|
{
|
|
Editor.Simulation.BreakpointHangTag = new BreakpointHangState
|
|
{
|
|
Node = node,
|
|
Window = this,
|
|
};
|
|
Editor.Simulation.OnBreakpointHangBegin();
|
|
|
|
// Disable all UI that is not supported during breakpoint hang
|
|
_debugObjectPicker.Enabled = false;
|
|
foreach (var window in Editor.Windows.Windows)
|
|
{
|
|
if (!(window is VisualScriptWindow) && !(window is VisualScriptDebuggerWindow))
|
|
window.Enabled = false;
|
|
}
|
|
foreach (var control in _debugToolstripControls)
|
|
control.Visible = true;
|
|
Editor.UI.UpdateToolstrip();
|
|
Editor.UI.UpdateMainMenu();
|
|
UpdateToolstrip();
|
|
|
|
// Focus stack node
|
|
node.Breakpoint.Hit = true;
|
|
OnDebuggerNavigateToCurrentNode();
|
|
|
|
// Handle breakpoint by hanging the main thread and manually processing input events with rendering of the current Visual Script window
|
|
var lastTick = DateTime.Now;
|
|
var targetTickTime = TimeSpan.FromSeconds(1.0 / 60.0);
|
|
var tickMargin = TimeSpan.FromMilliseconds(1);
|
|
var deltaTime = 1.0f / 60.0f;
|
|
while (Editor.Simulation.ContinueBreakpointHang())
|
|
{
|
|
// Handle input and drawing (UI only)
|
|
Editor.Internal_RunVisualScriptBreakpointLoopTick(deltaTime);
|
|
|
|
// Keep stable update rate
|
|
var now = DateTime.Now;
|
|
var tickTime = now - lastTick;
|
|
deltaTime = (float)tickTime.TotalMilliseconds;
|
|
var tickTimeLeft = targetTickTime - tickTime - tickMargin;
|
|
if (tickTimeLeft > TimeSpan.Zero)
|
|
Thread.Sleep(tickTimeLeft);
|
|
lastTick = now;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Editor.LogError("Error while Visual Script breakpoint handling.");
|
|
Editor.LogWarning(ex);
|
|
}
|
|
finally
|
|
{
|
|
node.Breakpoint.Hit = false;
|
|
Editor.Simulation.OnBreakpointHangEnd();
|
|
|
|
// Restore all UI
|
|
_debugObjectPicker.Enabled = true;
|
|
foreach (var window in Editor.Windows.Windows)
|
|
window.Enabled = true;
|
|
foreach (var control in _debugToolstripControls)
|
|
control.Visible = false;
|
|
Editor.UI.UpdateToolstrip();
|
|
Editor.UI.UpdateMainMenu();
|
|
UpdateToolstrip();
|
|
}
|
|
}
|
|
|
|
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 = (BreakpointHangState)Editor.Simulation.BreakpointHangTag;
|
|
ShowNode(state.Node);
|
|
}
|
|
|
|
private void OnDebuggerStepOver()
|
|
{
|
|
if (!Editor.Simulation.IsDuringBreakpointHang)
|
|
return;
|
|
|
|
// Break on any of the output connections from the current node
|
|
var state = (BreakpointHangState)Editor.Simulation.BreakpointHangTag;
|
|
if (_debugStepOverNodesIds == null)
|
|
_debugStepOverNodesIds = new List<uint>();
|
|
else
|
|
_debugStepOverNodesIds.Clear();
|
|
foreach (var element in state.Node.Elements)
|
|
{
|
|
if (element is Surface.Elements.OutputBox outputBox)
|
|
{
|
|
foreach (var box in outputBox.Connections)
|
|
_debugStepOverNodesIds.Add(box.ParentNode.ID);
|
|
}
|
|
}
|
|
if (_debugStepOverNodesIds.Count == 0)
|
|
OnDebuggerStepOut();
|
|
else
|
|
Editor.Simulation.StopBreakpointHang();
|
|
}
|
|
|
|
private void OnDebuggerStepInto()
|
|
{
|
|
if (!Editor.Simulation.IsDuringBreakpointHang)
|
|
return;
|
|
|
|
// Break on any next flow on this thread
|
|
_debugBreakNext = true;
|
|
_debugBreakNextThreadId = Platform.CurrentThreadID;
|
|
Editor.Simulation.StopBreakpointHang();
|
|
}
|
|
|
|
private void OnDebuggerStepOut()
|
|
{
|
|
if (!Editor.Simulation.IsDuringBreakpointHang)
|
|
return;
|
|
|
|
// Break on any of the output connects from the previous scope node
|
|
var frame = Editor.Internal_GetVisualScriptPreviousScopeFrame();
|
|
if (frame.Script != null)
|
|
{
|
|
if (_debugStepOutNodesIds == null)
|
|
_debugStepOutNodesIds = new List<KeyValuePair<VisualScript, uint>>();
|
|
else
|
|
_debugStepOutNodesIds.Clear();
|
|
|
|
VisualScriptWindow vsWindow = null;
|
|
foreach (var window in Editor.Instance.Windows.Windows)
|
|
{
|
|
if (window is VisualScriptWindow w && w.Asset == frame.Script)
|
|
{
|
|
vsWindow = (VisualScriptWindow)window;
|
|
break;
|
|
}
|
|
}
|
|
if (vsWindow == null)
|
|
{
|
|
var item = Editor.Instance.ContentDatabase.FindAsset(frame.Script.ID);
|
|
if (item != null)
|
|
vsWindow = Editor.Instance.ContentEditing.Open(item) as VisualScriptWindow;
|
|
}
|
|
var node = vsWindow?.Surface.FindNode(frame.NodeId);
|
|
_debugStepOutNodesIds.Add(new KeyValuePair<VisualScript, uint>(frame.Script, frame.NodeId));
|
|
if (node != null)
|
|
{
|
|
foreach (var element in node.Elements)
|
|
{
|
|
if (element is Surface.Elements.OutputBox outputBox)
|
|
{
|
|
foreach (var box in outputBox.Connections)
|
|
_debugStepOutNodesIds.Add(new KeyValuePair<VisualScript, uint>(frame.Script, box.ParentNode.ID));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Editor.Simulation.StopBreakpointHang();
|
|
}
|
|
|
|
private void OnDebuggerStop()
|
|
{
|
|
if (!Editor.Simulation.IsDuringBreakpointHang)
|
|
return;
|
|
|
|
// Stop play
|
|
Editor.Simulation.RequestStopPlay();
|
|
}
|
|
|
|
private void LoadBreakpoints()
|
|
{
|
|
try
|
|
{
|
|
// Try to restore the cached breakpoints from the last session
|
|
if (Editor.ProjectCache.TryGetCustomData(_asset.ScriptTypeName + ".Breakpoints", out var breakpointsData))
|
|
{
|
|
var data = JsonSerializer.Deserialize<BreakpointData[]>(breakpointsData);
|
|
if (data != null)
|
|
{
|
|
for (int i = 0; i < data.Length; i++)
|
|
{
|
|
ref var e = ref data[i];
|
|
var node = Surface.FindNode(e.NodeId);
|
|
if (node != null)
|
|
{
|
|
node.Breakpoint.Set = true;
|
|
node.Breakpoint.Enabled = e.Enabled;
|
|
Surface.OnNodeBreakpointEdited(node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Editor.LogWarning("Failed to load breakpoints data.");
|
|
Editor.LogWarning(ex);
|
|
}
|
|
}
|
|
|
|
private void SaveBreakpoints()
|
|
{
|
|
try
|
|
{
|
|
var breakpointsCount = 0;
|
|
var nodes = Surface.Nodes;
|
|
for (int i = 0; i < nodes.Count; i++)
|
|
{
|
|
if (nodes[i].Breakpoint.Set)
|
|
breakpointsCount++;
|
|
}
|
|
if (breakpointsCount == 0)
|
|
{
|
|
// No breakpoints in use
|
|
Editor.ProjectCache.RemoveCustomData(_asset.ScriptTypeName + ".Breakpoints");
|
|
return;
|
|
}
|
|
|
|
var data = new BreakpointData[breakpointsCount];
|
|
for (int i = 0, j = 0; i < nodes.Count; i++)
|
|
{
|
|
var node = nodes[i];
|
|
if (node.Breakpoint.Set)
|
|
{
|
|
data[j++] = new BreakpointData
|
|
{
|
|
NodeId = node.ID,
|
|
Enabled = node.Breakpoint.Enabled,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Set the cached breakpoints
|
|
Editor.ProjectCache.SetCustomData(_asset.ScriptTypeName + ".Breakpoints", JsonSerializer.Serialize(data));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Editor.LogWarning("Failed to save breakpoints data.");
|
|
Editor.LogWarning(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Focuses the node.
|
|
/// </summary>
|
|
/// <param name="node">The node.</param>
|
|
public void ShowNode(SurfaceNode node)
|
|
{
|
|
SelectTab();
|
|
RootWindow.Focus();
|
|
Surface.Focus();
|
|
Surface.FocusNode(node);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string SurfaceName => "Visual Script";
|
|
|
|
/// <inheritdoc />
|
|
public byte[] SurfaceData
|
|
{
|
|
get => _asset.LoadSurface();
|
|
set
|
|
{
|
|
// Create metadata
|
|
var meta = new VisualScript.Metadata
|
|
{
|
|
BaseTypename = _properties.BaseClass?.FullName,
|
|
Flags = VisualScript.Flags.None,
|
|
};
|
|
if (_properties.IsAbstract)
|
|
meta.Flags |= VisualScript.Flags.Abstract;
|
|
if (_properties.IsSealed)
|
|
meta.Flags |= VisualScript.Flags.Sealed;
|
|
|
|
// Save data to the asset
|
|
if (_asset.SaveSurface(value, ref meta))
|
|
{
|
|
// Error
|
|
_surface.MarkAsEdited();
|
|
Editor.LogError("Failed to save Visual Script surface data");
|
|
}
|
|
_asset.Reload();
|
|
SaveBreakpoints();
|
|
|
|
// Asset got edited so the methods/properties types info could be changed
|
|
Editor.CodeEditing.ClearTypes();
|
|
Editor.CodeEditing.OnTypesChanged();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnContextCreated(VisjectSurfaceContext context)
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnSurfaceEditedChanged()
|
|
{
|
|
if (_surface.IsEdited)
|
|
MarkAsEdited();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnSurfaceGraphEdited()
|
|
{
|
|
// Mark as dirty
|
|
_tmpAssetIsDirty = true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnSurfaceClose()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void UnlinkItem()
|
|
{
|
|
Editor.VisualScriptingDebugFlow -= OnDebugFlow;
|
|
SaveBreakpoints();
|
|
_surface.Script = null;
|
|
_isWaitingForSurfaceLoad = false;
|
|
_properties.OnClean();
|
|
|
|
base.UnlinkItem();
|
|
}
|
|
|
|
private bool LoadSurface()
|
|
{
|
|
// Init properties and parameters proxy
|
|
_properties.OnLoad(this);
|
|
|
|
// Load surface graph
|
|
if (_surface.Load())
|
|
{
|
|
// Error
|
|
Editor.LogError("Failed to load Visual Script surface.");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool SaveSurface()
|
|
{
|
|
_surface.Save();
|
|
|
|
// Reselect actors to prevent issues after Visual Script properties were modified
|
|
Editor.Windows.PropertiesWin.Presenter.BuildLayoutOnUpdate();
|
|
|
|
return false;
|
|
}
|
|
|
|
private void SetCanEdit(bool canEdit)
|
|
{
|
|
if (_canEdit == canEdit)
|
|
return;
|
|
_canEdit = canEdit;
|
|
_undo.Enabled = canEdit;
|
|
_surface.CanEdit = canEdit;
|
|
foreach (var child in _propertiesEditor.Panel.Children)
|
|
{
|
|
if (!(child is ScrollBar))
|
|
child.Enabled = canEdit;
|
|
}
|
|
UpdateToolstrip();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnPlayBeginning()
|
|
{
|
|
base.OnPlayBeginning();
|
|
|
|
if (IsEdited && Editor.Options.Options.General.AutoSaveVisualScriptOnPlayStart)
|
|
{
|
|
Save();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnPlayBegin()
|
|
{
|
|
base.OnPlayBegin();
|
|
|
|
SetCanEdit(false);
|
|
_debugObjectPicker.Parent.Visible = _debugObjectPicker.Parent.Enabled = true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnPlayEnd()
|
|
{
|
|
_debugObjectPicker.Parent.Visible = _debugObjectPicker.Parent.Enabled = false;
|
|
_debugObjectPicker.Value = null;
|
|
SetCanEdit(true);
|
|
|
|
base.OnPlayEnd();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Update(float deltaTime)
|
|
{
|
|
base.Update(deltaTime);
|
|
|
|
if (_tmpAssetIsDirty)
|
|
{
|
|
_tmpAssetIsDirty = false;
|
|
|
|
RefreshTempAsset();
|
|
}
|
|
|
|
if (_isWaitingForSurfaceLoad && _asset.IsLoaded)
|
|
{
|
|
_isWaitingForSurfaceLoad = false;
|
|
|
|
if (LoadSurface())
|
|
{
|
|
Close();
|
|
return;
|
|
}
|
|
|
|
// Setup
|
|
_undo.Clear();
|
|
_surface.Enabled = true;
|
|
_propertiesEditor.BuildLayout();
|
|
ClearEditedFlag();
|
|
if (_showWholeGraphOnLoad)
|
|
{
|
|
_showWholeGraphOnLoad = false;
|
|
_surface.ShowWholeGraph();
|
|
}
|
|
LoadBreakpoints();
|
|
Editor.VisualScriptingDebugFlow += OnDebugFlow;
|
|
}
|
|
else if (_refreshPropertiesOnLoad && _asset.IsLoaded)
|
|
{
|
|
_refreshPropertiesOnLoad = false;
|
|
|
|
_propertiesEditor.BuildLayout();
|
|
}
|
|
|
|
// Update script execution flow debugging visualization
|
|
lock (_debugFlows)
|
|
{
|
|
foreach (var debugFlow in _debugFlows)
|
|
{
|
|
var node = Surface.Context.FindNode(debugFlow.NodeId);
|
|
var box = node?.GetBox(debugFlow.BoxId);
|
|
box?.HighlightConnections();
|
|
}
|
|
_debugFlows.Clear();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool UseLayoutData => true;
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLayoutSerialize(XmlWriter writer)
|
|
{
|
|
writer.WriteAttributeString("Split", _split.SplitterValue.ToString());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLayoutDeserialize(XmlElement node)
|
|
{
|
|
if (float.TryParse(node.GetAttribute("Split"), out float value1))
|
|
_split.SplitterValue = value1;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLayoutDeserialize()
|
|
{
|
|
_split.SplitterValue = 0.7f;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnDestroy()
|
|
{
|
|
_undo.Enabled = false;
|
|
_propertiesEditor.Deselect();
|
|
_undo.Clear();
|
|
_debugObjectPicker = null;
|
|
_debugToolstripControls = null;
|
|
|
|
base.OnDestroy();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<ScriptType> NewParameterTypes => Editor.CodeEditing.VisualScriptPropertyTypes.Get();
|
|
|
|
/// <inheritdoc />
|
|
public void OnParamRenameUndo()
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnParamEditAttributesUndo()
|
|
{
|
|
_propertiesEditor.BuildLayout();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnParamAddUndo()
|
|
{
|
|
_refreshPropertiesOnLoad = true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnParamRemoveUndo()
|
|
{
|
|
_refreshPropertiesOnLoad = true;
|
|
_propertiesEditor.BuildLayout();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public object GetParameter(int index)
|
|
{
|
|
var param = Surface.Parameters[index];
|
|
return param.Value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetParameter(int index, object value)
|
|
{
|
|
var param = Surface.Parameters[index];
|
|
param.Value = value;
|
|
_paramValueChange = true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Asset VisjectAsset => Asset;
|
|
|
|
/// <inheritdoc />
|
|
public VisjectSurface VisjectSurface => _surface;
|
|
}
|
|
}
|