// Copyright (c) 2012-2024 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
{
///
/// Visject Surface control for editing Nodes Graph.
///
///
///
[HideInEditor]
public partial class VisjectSurface : ContainerControl, IParametersDependantNode
{
private static readonly List NavUpdateCache = new List(8);
///
/// The surface control.
///
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 _customNodes;
private List _batchedUndoActions;
private Action _onSave;
private int _selectedConnectionIndex;
internal int _isUpdatingBoxTypes;
///
/// True if surface supports implicit casting of the FlaxEngine.Object types into Boolean value (as simple validate check).
///
protected bool _supportsImplicitCastFromObjectToBoolean = false;
///
/// The left mouse down flag.
///
protected bool _leftMouseDown;
///
/// The right mouse down flag.
///
protected bool _rightMouseDown;
///
/// The middle mouse down flag.
///
protected bool _middleMouseDown;
///
/// The left mouse down position.
///
protected Float2 _leftMouseDownPos = Float2.Minimum;
///
/// The right mouse down position.
///
protected Float2 _rightMouseDownPos = Float2.Minimum;
///
/// The middle mouse down position.
///
protected Float2 _middleMouseDownPos = Float2.Minimum;
///
/// The mouse position.
///
protected Float2 _mousePos = Float2.Minimum;
///
/// The mouse movement amount.
///
protected float _mouseMoveAmount;
///
/// The is moving selection flag.
///
protected bool _isMovingSelection;
///
/// The moving selection view position.
///
protected Float2 _movingSelectionViewPos;
///
/// The connection start.
///
protected IConnectionInstigator _connectionInstigator;
///
/// The last connection instigator under mouse.
///
protected IConnectionInstigator _lastInstigatorUnderMouse;
///
/// The primary context menu.
///
protected VisjectCM _cmPrimaryMenu;
///
/// The context menu start position.
///
protected Float2 _cmStartPos = Float2.Minimum;
///
/// Occurs when selection gets changed.
///
public event Action SelectionChanged;
///
/// The surface owner.
///
public readonly IVisjectSurfaceOwner Owner;
///
/// The style used by the surface.
///
public readonly SurfaceStyle Style;
///
/// The undo system to use for the history actions recording (optional, can be null).
///
public readonly FlaxEditor.Undo Undo;
///
/// If false, the surface editing is blocked (UI display in read-only mode).
///
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);
}
}
}
}
///
/// Gets a value indicating whether surface is edited.
///
public bool IsEdited => RootContext.IsModified;
///
/// Gets the current context surface root control (nodes and all other surface elements container).
///
public SurfaceRootControl SurfaceRoot => _rootControl;
///
/// Gets or sets the view position (upper left corner of the view) in the surface space.
///
public Float2 ViewPosition
{
get => _rootControl.Location / -ViewScale;
set => _rootControl.Location = value * -ViewScale;
}
///
/// Gets or sets the view center position (middle point of the view) in the surface space.
///
public Float2 ViewCenterPosition
{
get => (_rootControl.Location - Size * 0.5f) / -ViewScale;
set => _rootControl.Location = Size * 0.5f + value * -ViewScale;
}
///
/// Gets or sets the view scale.
///
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);
}
}
///
/// Gets a value indicating whether user is selecting nodes.
///
public bool IsSelecting => _leftMouseDown && !_isMovingSelection && _connectionInstigator == null;
///
/// Gets a value indicating whether user is moving selected nodes.
///
public bool IsMovingSelection => _leftMouseDown && _isMovingSelection && _connectionInstigator == null;
///
/// Gets a value indicating whether user is connecting nodes.
///
public bool IsConnecting => _connectionInstigator != null;
///
/// Gets a value indicating whether the left mouse button is down.
///
public bool IsLeftMouseButtonDown => _leftMouseDown;
///
/// Gets a value indicating whether the right mouse button is down.
///
public bool IsRightMouseButtonDown => _rightMouseDown;
///
/// Gets a value indicating whether this Surface supports debugging.
///
public bool SupportsDebugging => _supportsDebugging;
///
/// Returns true if any node is selected by the user (one or more).
///
public bool HasNodesSelection
{
get
{
for (int i = 0; i < Nodes.Count; i++)
{
if (Nodes[i].IsSelected)
return true;
}
return false;
}
}
///
/// Gets the list of the selected nodes.
///
///
/// Don't call it too often. It does memory allocation and iterates over the surface controls to find selected nodes in the graph.
///
public List SelectedNodes
{
get
{
var selection = new List();
for (int i = 0; i < Nodes.Count; i++)
{
if (Nodes[i].IsSelected)
selection.Add(Nodes[i]);
}
return selection;
}
}
///
/// Gets the list of the selected controls (comments and nodes).
///
///
/// Don't call it too often. It does memory allocation and iterates over the surface controls to find selected nodes in the graph.
///
public List SelectedControls
{
get
{
var selection = new List();
for (int i = 0; i < _rootControl.Children.Count; i++)
{
if (_rootControl.Children[i] is SurfaceControl control && control.IsSelected)
selection.Add(control);
}
return selection;
}
}
///
/// Gets the list of the surface comments.
///
///
/// Don't call it too often. It does memory allocation and iterates over the surface controls to find comments in the graph.
///
public List Comments => _context?.Comments;
///
/// The current surface context nodes collection. Read-only.
///
public List Nodes => _context?.Nodes;
///
/// The surface node descriptors collection.
///
public readonly List NodeArchetypes;
///
/// Occurs when node gets spawned in the surface (via UI).
///
public event Action NodeSpawned;
///
/// Occurs when node gets removed from the surface (via UI).
///
public event Action NodeDeleted;
///
/// Occurs when node values gets modified in the surface (via UI).
///
public event Action NodeValuesEdited;
///
/// Occurs when node breakpoint state gets modified in the surface (via UI).
///
public event Action NodeBreakpointEdited;
///
/// Occurs when two nodes gets connected (via UI).
///
public event Action NodesConnected;
///
/// Occurs when two nodes gets disconnected (via UI).
///
public event Action NodesDisconnected;
///
/// Initializes a new instance of the class.
///
/// The owner.
/// The save action called when user wants to save the surface.
/// The undo/redo to use for the history actions recording. Optional, can be null to disable undo support.
/// The custom surface style. Use null to create the default style.
/// The custom surface node types. Pass null to use the default nodes set.
/// True if surface supports debugging features (breakpoints, etc.).
public VisjectSurface(IVisjectSurfaceOwner owner, Action onSave = null, FlaxEditor.Undo undo = null, SurfaceStyle style = null, List 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.DeselectAll, DeselectAll),
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(ValidateDragItem));
DragHandlers.Add(_dragParameters = new DragNames(SurfaceParameter.DragPrefix, ValidateDragParameter));
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
}
private void OnScriptsReloadBegin()
{
_activeVisjectCM = null;
_cmPrimaryMenu?.Dispose();
_cmPrimaryMenu = null;
}
///
/// Gets the display name of the connection type used in the surface.
///
/// The graph connection type.
/// The display name (for UI).
public virtual string GetTypeName(ScriptType type)
{
return type == ScriptType.Null ? null : type.Name;
}
///
/// Gets the debugger tooltip for the given box.
///
/// The box.
/// The result tooltip text.
/// True if processed output text should be used to display it on a box tooltip for debugger UI, otherwise false.
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();
}
}
///
/// Gets the custom nodes group archetype with custom nodes archetypes. May be null if no custom nodes in use.
///
/// The custom nodes or null if no used.
public GroupArchetype GetCustomNodes()
{
return _customNodesGroup;
}
///
/// Adds the custom nodes archetypes to the surface (user can spawn them and surface can deserialize).
///
/// Custom nodes has to have a node logic typename in DefaultValues[0] and group name in DefaultValues[1].
/// The archetypes.
public void AddCustomNodes(IEnumerable archetypes)
{
if (_customNodes == null)
{
// First time setup
_customNodes = new List(archetypes);
_customNodesGroup = new GroupArchetype
{
GroupID = Custom.GroupID,
Name = "Custom",
Color = Color.Wheat
};
}
else
{
// Add more nodes
_customNodes.AddRange(archetypes);
}
// Update collection
_customNodesGroup.Archetypes = _customNodes;
}
///
/// 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.
///
/// The navigation bar to update.
/// The toolstrip to use as layout reference.
/// True if skip showing nav button if the current context is the root location (user has no option to change context).
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();
}
///
/// Gets all nodes bounds or empty if surface is empty.
///
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;
}
}
///
/// Gets a value indicating whether surface parameters are not read-only and can be modified in a surface graph.
///
public virtual bool CanSetParameters => false;
///
/// Gets a value indicating whether surface supports/allows live previewing graph modifications due to value sliders and color pickers. True by default but disabled for shader surfaces that generate and compile shader source at flight.
///
public virtual bool CanLivePreviewValueChanges => true;
///
/// Determines whether the specified node archetype can be used in the surface.
///
/// The nodes group archetype identifier.
/// The node archetype identifier.
/// True if can use this node archetype, otherwise false.
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;
}
///
/// Determines whether the specified node archetype can be used in the surface.
///
/// The nodes group archetype.
/// The node archetype.
/// True if can use this node archetype, otherwise false.
public virtual bool CanUseNodeType(GroupArchetype groupArchetype, NodeArchetype nodeArchetype)
{
return (nodeArchetype.Flags & NodeFlags.NoSpawnViaPaste) == 0;
}
///
/// Shows the whole graph by changing the view scale and the position.
///
public void ShowWholeGraph()
{
ShowArea(AllNodesBounds.MakeExpanded(100.0f));
}
///
/// Shows the given surface area by changing the view scale and the position.
///
/// The area rectangle.
public void ShowArea(Rectangle areaRect)
{
ViewScale = (Size / areaRect.Size).MinValue * 0.95f;
ViewCenterPosition = areaRect.Center;
}
///
/// Shows the given surface node by changing the view scale and the position and focuses the node.
///
/// The node to show.
public void FocusNode(SurfaceNode node)
{
if (node != null)
{
ShowArea(node.Bounds.MakeExpanded(500.0f));
Select(node);
}
}
///
/// Mark surface as edited.
///
/// True if graph has been edited (nodes structure or parameter value).
public void MarkAsEdited(bool graphEdited = true)
{
_context.MarkAsModified(graphEdited);
}
private void BulkSelectUpdate(bool select = true)
{
bool selectionChanged = false;
for (int i = 0; i < _rootControl.Children.Count; i++)
{
if (_rootControl.Children[i] is SurfaceControl control && control.IsSelected != select)
{
control.IsSelected = select;
selectionChanged = true;
}
}
if (selectionChanged)
SelectionChanged?.Invoke();
}
///
/// Selects all the nodes.
///
public void SelectAll()
{
BulkSelectUpdate(true);
}
///
/// Deelects all the nodes.
///
public void DeselectAll()
{
BulkSelectUpdate(false);
}
///
/// Clears the selection.
///
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();
}
///
/// Adds the specified control to the selection.
///
/// The control.
public void AddToSelection(SurfaceControl control)
{
if (control.IsSelected)
return;
control.IsSelected = true;
SelectionChanged?.Invoke();
}
///
/// Selects the specified control.
///
/// The control.
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();
}
///
/// Selects the specified controls collection.
///
/// The controls.
public void Select(IEnumerable 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();
}
///
/// Deselects the specified control.
///
/// The control.
public void Deselect(SurfaceControl control)
{
if (!control.IsSelected)
return;
control.IsSelected = false;
SelectionChanged?.Invoke();
}
///
/// Creates the comment around the selected nodes.
///
public SurfaceComment CommentSelection(string text = "")
{
var selection = SelectedNodes;
if (selection.Count == 0)
return null;
Rectangle surfaceArea = GetNodesBounds(selection).MakeExpanded(80.0f);
// Order below other selected comments
bool hasCommentsSelected = false;
int lowestCommentOrder = int.MaxValue;
for (int i = 0; i < selection.Count; i++)
{
if (selection[i] is not SurfaceComment || selection[i].IndexInParent >= lowestCommentOrder)
continue;
hasCommentsSelected = true;
lowestCommentOrder = selection[i].IndexInParent;
}
return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f), hasCommentsSelected ? lowestCommentOrder : -1);
}
private static Rectangle GetNodesBounds(List 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;
}
///
/// Deletes the specified collection of the controls.
///
/// The controls.
/// True if use undo/redo action for node removing.
public void Delete(IEnumerable controls, bool withUndo = true)
{
if (!CanEdit || controls == null || !controls.Any())
return;
var selectionChanged = false;
List 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();
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();
// 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);
}
AddBatchedUndoAction(new MultiUndoAction(actions, nodes.Count == 1 ? "Remove node" : "Remove nodes"));
}
}
MarkAsEdited();
}
///
/// Deletes the specified control.
///
/// The control.
/// True if use undo/redo action for node removing.
public void Delete(SurfaceControl control, bool withUndo = true)
{
if (!CanEdit)
return;
Delete(new[] { control }, withUndo);
}
///
/// Deletes the selected controls.
///
public void Delete()
{
if (!CanEdit)
return;
Delete(SelectedControls, true);
}
///
/// Finds the node of the given type.
///
/// The group identifier.
/// The type identifier.
/// Found node or null if cannot.
public SurfaceNode FindNode(ushort groupId, ushort typeId)
{
return _context.FindNode(groupId, typeId);
}
///
/// Finds the node with the given ID.
///
/// The identifier.
/// Found node or null if cannot.
public SurfaceNode FindNode(int id)
{
return _context.FindNode(id);
}
///
/// Finds the node with the given ID.
///
/// The identifier.
/// Found node or null if cannot.
public SurfaceNode FindNode(uint id)
{
return _context.FindNode(id);
}
///
/// Adds the undo action to be batched (eg. if multiple undo actions is performed in a sequence during single update).
///
/// The action.
public void AddBatchedUndoAction(IUndoAction action)
{
if (Undo == null || !Undo.Enabled)
return;
if (_batchedUndoActions == null)
_batchedUndoActions = new List();
_batchedUndoActions.Add(action);
}
///
/// Called when node gets spawned in the surface (via UI).
///
public virtual void OnNodeSpawned(SurfaceNode node)
{
NodeSpawned?.Invoke(node);
}
///
/// Called when node values gets modified in the surface (via UI).
///
public virtual void OnNodeValuesEdited(SurfaceNode node)
{
NodeValuesEdited?.Invoke(node);
}
///
/// Called when node breakpoint gets modified.
///
public virtual void OnNodeBreakpointEdited(SurfaceNode node)
{
NodeBreakpointEdited?.Invoke(node);
}
///
/// Called when node gets removed from the surface (via UI).
///
public virtual void OnNodeDeleted(SurfaceNode node)
{
NodeDeleted?.Invoke(node);
}
///
/// Called when two nodes gets connected (via UI).
///
public virtual void OnNodesConnected(IConnectionInstigator a, IConnectionInstigator b)
{
NodesConnected?.Invoke(a, b);
MarkAsEdited();
}
///
/// Called when two nodes gets disconnected (via UI).
///
public virtual void OnNodesDisconnected(IConnectionInstigator a, IConnectionInstigator b)
{
NodesDisconnected?.Invoke(a, b);
}
///
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();
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
base.OnDestroy();
}
}
}