Files
FlaxEngine/Source/Editor/Surface/VisjectSurface.cs
2023-09-27 17:32:47 +02:00

972 lines
34 KiB
C#

// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Drag;
using FlaxEditor.Options;
using FlaxEditor.Scripting;
using FlaxEditor.Surface.Archetypes;
using FlaxEditor.Surface.ContextMenu;
using FlaxEditor.Surface.GUI;
using FlaxEditor.Surface.Undo;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Surface
{
/// <summary>
/// Visject Surface control for editing Nodes Graph.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
/// <seealso cref="IParametersDependantNode" />
[HideInEditor]
public partial class VisjectSurface : ContainerControl, IParametersDependantNode
{
private static readonly List<VisjectSurfaceContext> NavUpdateCache = new List<VisjectSurfaceContext>(8);
/// <summary>
/// The surface control.
/// </summary>
protected SurfaceRootControl _rootControl;
private float _targetScale = 1.0f;
private float _moveViewWithMouseDragSpeed = 1.0f;
private bool _canEdit = true;
private readonly bool _supportsDebugging;
private bool _isReleasing;
private VisjectCM _activeVisjectCM;
private GroupArchetype _customNodesGroup;
private List<NodeArchetype> _customNodes;
private List<IUndoAction> _batchedUndoActions;
private Action _onSave;
private int _selectedConnectionIndex;
internal int _isUpdatingBoxTypes;
/// <summary>
/// True if surface supports implicit casting of the FlaxEngine.Object types into Boolean value (as simple validate check).
/// </summary>
protected bool _supportsImplicitCastFromObjectToBoolean = false;
/// <summary>
/// The left mouse down flag.
/// </summary>
protected bool _leftMouseDown;
/// <summary>
/// The right mouse down flag.
/// </summary>
protected bool _rightMouseDown;
/// <summary>
/// The middle mouse down flag.
/// </summary>
protected bool _middleMouseDown;
/// <summary>
/// The left mouse down position.
/// </summary>
protected Float2 _leftMouseDownPos = Float2.Minimum;
/// <summary>
/// The right mouse down position.
/// </summary>
protected Float2 _rightMouseDownPos = Float2.Minimum;
/// <summary>
/// The middle mouse down position.
/// </summary>
protected Float2 _middleMouseDownPos = Float2.Minimum;
/// <summary>
/// The mouse position.
/// </summary>
protected Float2 _mousePos = Float2.Minimum;
/// <summary>
/// The mouse movement amount.
/// </summary>
protected float _mouseMoveAmount;
/// <summary>
/// The is moving selection flag.
/// </summary>
protected bool _isMovingSelection;
/// <summary>
/// The moving selection view position.
/// </summary>
protected Float2 _movingSelectionViewPos;
/// <summary>
/// The connection start.
/// </summary>
protected IConnectionInstigator _connectionInstigator;
/// <summary>
/// The last connection instigator under mouse.
/// </summary>
protected IConnectionInstigator _lastInstigatorUnderMouse;
/// <summary>
/// The primary context menu.
/// </summary>
protected VisjectCM _cmPrimaryMenu;
/// <summary>
/// The context menu start position.
/// </summary>
protected Float2 _cmStartPos = Float2.Minimum;
/// <summary>
/// Occurs when selection gets changed.
/// </summary>
public event Action SelectionChanged;
/// <summary>
/// The surface owner.
/// </summary>
public readonly IVisjectSurfaceOwner Owner;
/// <summary>
/// The style used by the surface.
/// </summary>
public readonly SurfaceStyle Style;
/// <summary>
/// The undo system to use for the history actions recording (optional, can be null).
/// </summary>
public readonly FlaxEditor.Undo Undo;
/// <summary>
/// If false, the surface editing is blocked (UI display in read-only mode).
/// </summary>
public bool CanEdit
{
get => _canEdit;
set
{
if (_canEdit == value)
return;
_canEdit = value;
for (int i = 0; i < _rootControl.Children.Count; i++)
{
if (_rootControl.Children[i] is SurfaceControl control)
{
control.OnSurfaceCanEditChanged(value);
}
}
}
}
/// <summary>
/// Gets a value indicating whether surface is edited.
/// </summary>
public bool IsEdited => RootContext.IsModified;
/// <summary>
/// Gets the current context surface root control (nodes and all other surface elements container).
/// </summary>
public SurfaceRootControl SurfaceRoot => _rootControl;
/// <summary>
/// Gets or sets the view position (upper left corner of the view) in the surface space.
/// </summary>
public Float2 ViewPosition
{
get => _rootControl.Location / -ViewScale;
set => _rootControl.Location = value * -ViewScale;
}
/// <summary>
/// Gets or sets the view center position (middle point of the view) in the surface space.
/// </summary>
public Float2 ViewCenterPosition
{
get => (_rootControl.Location - Size * 0.5f) / -ViewScale;
set => _rootControl.Location = Size * 0.5f + value * -ViewScale;
}
/// <summary>
/// Gets or sets the view scale.
/// </summary>
public float ViewScale
{
get => _targetScale;
set
{
// Clamp
value = Mathf.Clamp(value, 0.05f, 1.6f);
// Check if value will change
if (Mathf.Abs(value - _targetScale) > 0.0001f)
{
// Set new target scale
_targetScale = value;
}
// disable view scale animation
_rootControl.Scale = new Float2(_targetScale);
}
}
/// <summary>
/// Gets a value indicating whether user is selecting nodes.
/// </summary>
public bool IsSelecting => _leftMouseDown && !_isMovingSelection && _connectionInstigator == null;
/// <summary>
/// Gets a value indicating whether user is moving selected nodes.
/// </summary>
public bool IsMovingSelection => _leftMouseDown && _isMovingSelection && _connectionInstigator == null;
/// <summary>
/// Gets a value indicating whether user is connecting nodes.
/// </summary>
public bool IsConnecting => _connectionInstigator != null;
/// <summary>
/// Gets a value indicating whether the left mouse button is down.
/// </summary>
public bool IsLeftMouseButtonDown => _leftMouseDown;
/// <summary>
/// Gets a value indicating whether the right mouse button is down.
/// </summary>
public bool IsRightMouseButtonDown => _rightMouseDown;
/// <summary>
/// Gets a value indicating whether this Surface supports debugging.
/// </summary>
public bool SupportsDebugging => _supportsDebugging;
/// <summary>
/// Returns true if any node is selected by the user (one or more).
/// </summary>
public bool HasNodesSelection
{
get
{
for (int i = 0; i < Nodes.Count; i++)
{
if (Nodes[i].IsSelected)
return true;
}
return false;
}
}
/// <summary>
/// Gets the list of the selected nodes.
/// </summary>
/// <remarks>
/// Don't call it too often. It does memory allocation and iterates over the surface controls to find selected nodes in the graph.
/// </remarks>
public List<SurfaceNode> SelectedNodes
{
get
{
var selection = new List<SurfaceNode>();
for (int i = 0; i < Nodes.Count; i++)
{
if (Nodes[i].IsSelected)
selection.Add(Nodes[i]);
}
return selection;
}
}
/// <summary>
/// Gets the list of the selected controls (comments and nodes).
/// </summary>
/// <remarks>
/// Don't call it too often. It does memory allocation and iterates over the surface controls to find selected nodes in the graph.
/// </remarks>
public List<SurfaceControl> SelectedControls
{
get
{
var selection = new List<SurfaceControl>();
for (int i = 0; i < _rootControl.Children.Count; i++)
{
if (_rootControl.Children[i] is SurfaceControl control && control.IsSelected)
selection.Add(control);
}
return selection;
}
}
/// <summary>
/// Gets the list of the surface comments.
/// </summary>
/// <remarks>
/// Don't call it too often. It does memory allocation and iterates over the surface controls to find comments in the graph.
/// </remarks>
public List<SurfaceComment> Comments => _context?.Comments;
/// <summary>
/// The current surface context nodes collection. Read-only.
/// </summary>
public List<SurfaceNode> Nodes => _context?.Nodes;
/// <summary>
/// The surface node descriptors collection.
/// </summary>
public readonly List<GroupArchetype> NodeArchetypes;
/// <summary>
/// Occurs when node gets spawned in the surface (via UI).
/// </summary>
public event Action<SurfaceNode> NodeSpawned;
/// <summary>
/// Occurs when node gets removed from the surface (via UI).
/// </summary>
public event Action<SurfaceNode> NodeDeleted;
/// <summary>
/// Occurs when node values gets modified in the surface (via UI).
/// </summary>
public event Action<SurfaceNode> NodeValuesEdited;
/// <summary>
/// Occurs when node breakpoint state gets modified in the surface (via UI).
/// </summary>
public event Action<SurfaceNode> NodeBreakpointEdited;
/// <summary>
/// Occurs when two nodes gets connected (via UI).
/// </summary>
public event Action<IConnectionInstigator, IConnectionInstigator> NodesConnected;
/// <summary>
/// Occurs when two nodes gets disconnected (via UI).
/// </summary>
public event Action<IConnectionInstigator, IConnectionInstigator> NodesDisconnected;
/// <summary>
/// Initializes a new instance of the <see cref="VisjectSurface"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="onSave">The save action called when user wants to save the surface.</param>
/// <param name="undo">The undo/redo to use for the history actions recording. Optional, can be null to disable undo support.</param>
/// <param name="style">The custom surface style. Use null to create the default style.</param>
/// <param name="groups">The custom surface node types. Pass null to use the default nodes set.</param>
/// <param name="supportsDebugging">True if surface supports debugging features (breakpoints, etc.).</param>
public VisjectSurface(IVisjectSurfaceOwner owner, Action onSave = null, FlaxEditor.Undo undo = null, SurfaceStyle style = null, List<GroupArchetype> groups = null, bool supportsDebugging = false)
{
AnchorPreset = AnchorPresets.StretchAll;
Offsets = Margin.Zero;
AutoFocus = false; // Disable to prevent autofocus and event handling on OnMouseDown event
CullChildren = false;
_supportsDebugging = supportsDebugging;
Owner = owner;
Style = style ?? SurfaceStyle.CreateStyleHandler(Editor.Instance);
if (Style == null)
throw new InvalidOperationException("Missing visject surface style.");
NodeArchetypes = groups ?? NodeFactory.DefaultGroups;
Undo = undo;
_onSave = onSave;
// Initialize with the root context
OpenContext(owner);
RootContext.Modified += OnRootContextModified;
// Setup input actions
InputActions = new InputActionsContainer(new[]
{
new InputActionsContainer.Binding(options => options.Delete, Delete),
new InputActionsContainer.Binding(options => options.SelectAll, SelectAll),
new InputActionsContainer.Binding(options => options.Copy, Copy),
new InputActionsContainer.Binding(options => options.Paste, Paste),
new InputActionsContainer.Binding(options => options.Cut, Cut),
new InputActionsContainer.Binding(options => options.Duplicate, Duplicate),
});
Context.ControlSpawned += OnSurfaceControlSpawned;
Context.ControlDeleted += OnSurfaceControlDeleted;
SelectionChanged += () => { _selectedConnectionIndex = 0; };
// Init drag handlers
DragHandlers.Add(_dragAssets = new DragAssets<DragDropEventArgs>(ValidateDragItem));
DragHandlers.Add(_dragParameters = new DragNames<DragDropEventArgs>(SurfaceParameter.DragPrefix, ValidateDragParameter));
}
/// <summary>
/// Gets the display name of the connection type used in the surface.
/// </summary>
/// <param name="type">The graph connection type.</param>
/// <returns>The display name (for UI).</returns>
public virtual string GetTypeName(ScriptType type)
{
return type == ScriptType.Null ? null : type.Name;
}
/// <summary>
/// Gets the debugger tooltip for the given box.
/// </summary>
/// <param name="box">The box.</param>
/// <param name="text">The result tooltip text.</param>
/// <returns>True if processed output text should be used to display it on a box tooltip for debugger UI, otherwise false.</returns>
public virtual bool GetBoxDebuggerTooltip(Elements.Box box, out string text)
{
text = null;
return false;
}
private void OnRootContextModified(VisjectSurfaceContext context, bool graphEdited)
{
Owner.OnSurfaceEditedChanged();
if (graphEdited)
{
Owner.OnSurfaceGraphEdited();
}
}
/// <summary>
/// Gets the custom nodes group archetype with custom nodes archetypes. May be null if no custom nodes in use.
/// </summary>
/// <returns>The custom nodes or null if no used.</returns>
public GroupArchetype GetCustomNodes()
{
return _customNodesGroup;
}
/// <summary>
/// Adds the custom nodes archetypes to the surface (user can spawn them and surface can deserialize).
/// </summary>
/// <remarks>Custom nodes has to have a node logic typename in DefaultValues[0] and group name in DefaultValues[1].</remarks>
/// <param name="archetypes">The archetypes.</param>
public void AddCustomNodes(IEnumerable<NodeArchetype> archetypes)
{
if (_customNodes == null)
{
// First time setup
_customNodes = new List<NodeArchetype>(archetypes);
_customNodesGroup = new GroupArchetype
{
GroupID = Custom.GroupID,
Name = "Custom",
Color = Color.Wheat
};
}
else
{
// Add more nodes
_customNodes.AddRange(archetypes);
}
// Update collection
_customNodesGroup.Archetypes = _customNodes;
}
/// <summary>
/// Updates the navigation bar of the toolstrip from window that uses this surface. Updates the navigation bar panel buttons to match the current view path.
/// </summary>
/// <param name="navigationBar">The navigation bar to update.</param>
/// <param name="toolStrip">The toolstrip to use as layout reference.</param>
/// <param name="hideIfRoot">True if skip showing nav button if the current context is the root location (user has no option to change context).</param>
public void UpdateNavigationBar(NavigationBar navigationBar, ToolStrip toolStrip, bool hideIfRoot = true)
{
if (navigationBar == null || toolStrip == null)
return;
bool wasLayoutLocked = navigationBar.IsLayoutLocked;
navigationBar.IsLayoutLocked = true;
// Remove previous buttons
navigationBar.DisposeChildren();
// Spawn buttons
var nodes = NavUpdateCache;
nodes.Clear();
var context = Context;
if (hideIfRoot && context == RootContext)
context = null;
while (context != null)
{
nodes.Add(context);
context = context.Parent;
}
float x = NavigationBar.DefaultButtonsMargin;
float h = toolStrip.ItemsHeight - 2 * ToolStrip.DefaultMarginV;
for (int i = nodes.Count - 1; i >= 0; i--)
{
var button = new VisjectContextNavigationButton(this, nodes[i].Context, x, ToolStrip.DefaultMarginV, h);
button.PerformLayout();
x += button.Width + NavigationBar.DefaultButtonsMargin;
navigationBar.AddChild(button);
}
nodes.Clear();
// Update
navigationBar.IsLayoutLocked = wasLayoutLocked;
navigationBar.PerformLayout();
}
/// <summary>
/// Gets all nodes bounds or empty if surface is empty.
/// </summary>
public Rectangle AllNodesBounds
{
get
{
var area = Rectangle.Empty;
if (Nodes.Count != 0)
{
area = Nodes[0].Bounds;
for (int i = 1; i < Nodes.Count; i++)
area = Rectangle.Union(area, Nodes[i].Bounds);
}
return area;
}
}
/// <summary>
/// Gets a value indicating whether surface parameters are not read-only and can be modified in a surface graph.
/// </summary>
public virtual bool CanSetParameters => false;
/// <summary>
/// Determines whether the specified node archetype can be used in the surface.
/// </summary>
/// <param name="groupID">The nodes group archetype identifier.</param>
/// <param name="typeID">The node archetype identifier.</param>
/// <returns>True if can use this node archetype, otherwise false.</returns>
public bool CanUseNodeType(ushort groupID, ushort typeID)
{
var result = false;
var nodeArchetypes = NodeArchetypes ?? NodeFactory.DefaultGroups;
if (NodeFactory.GetArchetype(nodeArchetypes, groupID, typeID, out var groupArchetype, out var nodeArchetype))
{
var flags = nodeArchetype.Flags;
nodeArchetype.Flags &= ~NodeFlags.NoSpawnViaGUI;
nodeArchetype.Flags &= ~NodeFlags.NoSpawnViaPaste;
result = CanUseNodeType(groupArchetype, nodeArchetype);
nodeArchetype.Flags = flags;
}
return result;
}
/// <summary>
/// Determines whether the specified node archetype can be used in the surface.
/// </summary>
/// <param name="groupArchetype">The nodes group archetype.</param>
/// <param name="nodeArchetype">The node archetype.</param>
/// <returns>True if can use this node archetype, otherwise false.</returns>
public virtual bool CanUseNodeType(GroupArchetype groupArchetype, NodeArchetype nodeArchetype)
{
return (nodeArchetype.Flags & NodeFlags.NoSpawnViaPaste) == 0;
}
/// <summary>
/// Shows the whole graph by changing the view scale and the position.
/// </summary>
public void ShowWholeGraph()
{
ShowArea(AllNodesBounds.MakeExpanded(100.0f));
}
/// <summary>
/// Shows the given surface area by changing the view scale and the position.
/// </summary>
/// <param name="areaRect">The area rectangle.</param>
public void ShowArea(Rectangle areaRect)
{
ViewScale = (Size / areaRect.Size).MinValue * 0.95f;
ViewCenterPosition = areaRect.Center;
}
/// <summary>
/// Shows the given surface node by changing the view scale and the position and focuses the node.
/// </summary>
/// <param name="node">The node to show.</param>
public void FocusNode(SurfaceNode node)
{
if (node != null)
{
ShowArea(node.Bounds.MakeExpanded(500.0f));
Select(node);
}
}
/// <summary>
/// Mark surface as edited.
/// </summary>
/// <param name="graphEdited">True if graph has been edited (nodes structure or parameter value).</param>
public void MarkAsEdited(bool graphEdited = true)
{
_context.MarkAsModified(graphEdited);
}
/// <summary>
/// Selects all the nodes.
/// </summary>
public void SelectAll()
{
bool selectionChanged = false;
for (int i = 0; i < _rootControl.Children.Count; i++)
{
if (_rootControl.Children[i] is SurfaceControl control && !control.IsSelected)
{
control.IsSelected = true;
selectionChanged = true;
}
}
if (selectionChanged)
SelectionChanged?.Invoke();
}
/// <summary>
/// Clears the selection.
/// </summary>
public void ClearSelection()
{
bool selectionChanged = false;
for (int i = 0; i < _rootControl.Children.Count; i++)
{
if (_rootControl.Children[i] is SurfaceControl control && control.IsSelected)
{
control.IsSelected = false;
selectionChanged = true;
}
}
if (selectionChanged)
SelectionChanged?.Invoke();
}
/// <summary>
/// Adds the specified control to the selection.
/// </summary>
/// <param name="control">The control.</param>
public void AddToSelection(SurfaceControl control)
{
if (control.IsSelected)
return;
control.IsSelected = true;
SelectionChanged?.Invoke();
}
/// <summary>
/// Selects the specified control.
/// </summary>
/// <param name="control">The control.</param>
public void Select(SurfaceControl control)
{
bool selectionChanged = false;
for (int i = 0; i < _rootControl.Children.Count; i++)
{
if (_rootControl.Children[i] is SurfaceControl c && c != control && c.IsSelected)
{
c.IsSelected = false;
selectionChanged = true;
}
}
if (!control.IsSelected)
{
control.IsSelected = true;
selectionChanged = true;
}
if (selectionChanged)
SelectionChanged?.Invoke();
}
/// <summary>
/// Selects the specified controls collection.
/// </summary>
/// <param name="controls">The controls.</param>
public void Select(IEnumerable<SurfaceControl> controls)
{
var newSelection = controls.ToList();
var prevSelection = SelectedControls;
if (Utils.ArraysEqual(newSelection, prevSelection))
return;
ClearSelection();
foreach (var control in newSelection)
control.IsSelected = true;
SelectionChanged?.Invoke();
}
/// <summary>
/// Deselects the specified control.
/// </summary>
/// <param name="control">The control.</param>
public void Deselect(SurfaceControl control)
{
if (!control.IsSelected)
return;
control.IsSelected = false;
SelectionChanged?.Invoke();
}
/// <summary>
/// Creates the comment around the selected nodes.
/// </summary>
public SurfaceComment CommentSelection(string text = "")
{
var selection = SelectedNodes;
if (selection.Count == 0)
return null;
Rectangle surfaceArea = GetNodesBounds(selection).MakeExpanded(80.0f);
return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f));
}
private static Rectangle GetNodesBounds(List<SurfaceNode> nodes)
{
if (nodes.Count == 0)
return Rectangle.Empty;
Rectangle surfaceArea = nodes[0].Bounds;
for (int i = 1; i < nodes.Count; i++)
{
surfaceArea = Rectangle.Union(surfaceArea, nodes[i].Bounds);
}
return surfaceArea;
}
/// <summary>
/// Deletes the specified collection of the controls.
/// </summary>
/// <param name="controls">The controls.</param>
/// <param name="withUndo">True if use undo/redo action for node removing.</param>
public void Delete(IEnumerable<SurfaceControl> controls, bool withUndo = true)
{
if (!CanEdit || controls == null || !controls.Any())
return;
var selectionChanged = false;
List<SurfaceNode> nodes = null;
foreach (var control in controls)
{
if (control.IsSelected)
{
selectionChanged = true;
control.IsSelected = false;
}
if (control is SurfaceNode node)
{
if ((node.Archetype.Flags & NodeFlags.NoRemove) == 0)
{
if (nodes == null)
nodes = new List<SurfaceNode>();
var sealedNodes = node.SealedNodes;
if (sealedNodes != null)
{
foreach (var sealedNode in sealedNodes)
{
if (sealedNode != null)
{
if (sealedNode.IsSelected)
{
selectionChanged = true;
sealedNode.IsSelected = false;
}
if (!nodes.Contains(sealedNode))
nodes.Add(sealedNode);
}
}
}
if (!nodes.Contains(node))
nodes.Add(node);
}
}
else
{
Context.OnControlDeleted(control, SurfaceNodeActions.User);
}
}
if (selectionChanged)
SelectionChanged?.Invoke();
if (nodes != null)
{
if (Undo == null || !withUndo)
{
// Remove all nodes
foreach (var node in nodes)
{
node.RemoveConnections();
Nodes.Remove(node);
Context.OnControlDeleted(node, SurfaceNodeActions.User);
}
}
else
{
var actions = new List<IUndoAction>();
// Break connections for all nodes
foreach (var node in nodes)
{
var action = new EditNodeConnections(Context, node);
node.RemoveConnections();
action.End();
actions.Add(action);
}
// Remove all nodes
foreach (var node in nodes)
{
var action = new AddRemoveNodeAction(node, false);
action.Do();
actions.Add(action);
}
Undo.AddAction(new MultiUndoAction(actions, nodes.Count == 1 ? "Remove node" : "Remove nodes"));
}
}
MarkAsEdited();
}
/// <summary>
/// Deletes the specified control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="withUndo">True if use undo/redo action for node removing.</param>
public void Delete(SurfaceControl control, bool withUndo = true)
{
if (!CanEdit)
return;
Delete(new[] { control }, withUndo);
}
/// <summary>
/// Deletes the selected controls.
/// </summary>
public void Delete()
{
if (!CanEdit)
return;
Delete(SelectedControls, true);
}
/// <summary>
/// Finds the node of the given type.
/// </summary>
/// <param name="groupId">The group identifier.</param>
/// <param name="typeId">The type identifier.</param>
/// <returns>Found node or null if cannot.</returns>
public SurfaceNode FindNode(ushort groupId, ushort typeId)
{
return _context.FindNode(groupId, typeId);
}
/// <summary>
/// Finds the node with the given ID.
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>Found node or null if cannot.</returns>
public SurfaceNode FindNode(int id)
{
return _context.FindNode(id);
}
/// <summary>
/// Finds the node with the given ID.
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>Found node or null if cannot.</returns>
public SurfaceNode FindNode(uint id)
{
return _context.FindNode(id);
}
/// <summary>
/// Adds the undo action to be batched (eg. if multiple undo actions is performed in a sequence during single update).
/// </summary>
/// <param name="action">The action.</param>
public void AddBatchedUndoAction(IUndoAction action)
{
if (Undo == null || !Undo.Enabled)
return;
if (_batchedUndoActions == null)
_batchedUndoActions = new List<IUndoAction>();
_batchedUndoActions.Add(action);
}
/// <summary>
/// Called when node gets spawned in the surface (via UI).
/// </summary>
public virtual void OnNodeSpawned(SurfaceNode node)
{
NodeSpawned?.Invoke(node);
}
/// <summary>
/// Called when node values gets modified in the surface (via UI).
/// </summary>
public virtual void OnNodeValuesEdited(SurfaceNode node)
{
NodeValuesEdited?.Invoke(node);
}
/// <summary>
/// Called when node breakpoint gets modified.
/// </summary>
public virtual void OnNodeBreakpointEdited(SurfaceNode node)
{
NodeBreakpointEdited?.Invoke(node);
}
/// <summary>
/// Called when node gets removed from the surface (via UI).
/// </summary>
public virtual void OnNodeDeleted(SurfaceNode node)
{
NodeDeleted?.Invoke(node);
}
/// <summary>
/// Called when two nodes gets connected (via UI).
/// </summary>
public virtual void OnNodesConnected(IConnectionInstigator a, IConnectionInstigator b)
{
NodesConnected?.Invoke(a, b);
MarkAsEdited();
}
/// <summary>
/// Called when two nodes gets disconnected (via UI).
/// </summary>
public virtual void OnNodesDisconnected(IConnectionInstigator a, IConnectionInstigator b)
{
NodesDisconnected?.Invoke(a, b);
}
/// <inheritdoc />
public override void OnDestroy()
{
if (IsDisposing)
return;
_isReleasing = true;
// Cleanup context cache
_root = null;
_context = null;
_onSave = null;
ContextStack.Clear();
foreach (var context in _contextCache.Values)
{
context.Clear();
}
_contextCache.Clear();
// Cleanup
_activeVisjectCM = null;
_cmPrimaryMenu?.Dispose();
base.OnDestroy();
}
}
}