Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Implemented Undo functionality with UndoPro
Seems to be solid, as far as I can tell
Supports:
- Node GUI
- Node Selection / Dragging
- Connection Creation/Deletion (including affected connections)
- Node Creation / Duplication / Deletion
- Records survive Playmode/Compilation, not scene switch (as intended)
Does NOT support:
- Node Groups (does anyone actually use them?)

ATTENTION! Modifications done to support Undo:
Undo system is REFERENCE-based, no ScriptableObjects (SO) actually gets deleted until a new canvas is loaded / the scene is switched. So SOs (even if deleted) need to survive Compilation/Playmode. Currently, a working copy is saved and again loaded afterwards. That had to change. Now, it stores a 'SO memory dump' as the CurSession, which includes current and old SOs used for undo. Working-Copy save is only used for crash-critical saves. These include all but the 'window lost focus save', which is actually faster now.

NodeEditorUndoActions only serves to simplify the actual Undo calls and hides the error-checking.
  • Loading branch information
Seneral committed Jun 30, 2019
1 parent 9123002 commit 0f69501
Show file tree
Hide file tree
Showing 12 changed files with 348 additions and 35 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
@@ -0,0 +1,3 @@
[submodule "UndoPro"]
path = UndoPro
url = https://github.com/Seneral/UndoPro
5 changes: 4 additions & 1 deletion Editor/Node_Editor/NodeEditorWindow.cs
Expand Up @@ -70,6 +70,8 @@ private void OnEnable()
EditorLoadingControl.justOpenedNewScene += NormalReInit;
SceneView.onSceneGUIDelegate -= OnSceneGUI;
SceneView.onSceneGUIDelegate += OnSceneGUI;
Undo.undoRedoPerformed -= NodeEditor.RepaintClients;
Undo.undoRedoPerformed += NodeEditor.RepaintClients;
}

private void OnDestroy()
Expand All @@ -79,6 +81,7 @@ private void OnDestroy()
EditorLoadingControl.justLeftPlayMode -= NormalReInit;
EditorLoadingControl.justOpenedNewScene -= NormalReInit;
SceneView.onSceneGUIDelegate -= OnSceneGUI;
Undo.undoRedoPerformed -= NodeEditor.RepaintClients;

// Clear Cache
canvasCache.ClearCacheEvents();
Expand All @@ -87,7 +90,7 @@ private void OnDestroy()
private void OnLostFocus ()
{ // Save any changes made while focussing this window
// Will also save before possible assembly reload, scene switch, etc. because these require focussing of a different window
canvasCache.SaveCache();
canvasCache.SaveCache(false);
}

private void OnFocus ()
Expand Down
26 changes: 25 additions & 1 deletion Node_Editor/Framework/Core/ConnectionPort.cs
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using System;
using System.Linq;
using System.Collections.Generic;
Expand Down Expand Up @@ -191,6 +191,18 @@ public void ApplyConnection (ConnectionPort port, bool silent = false)
}
port.connections.Add (this);

#if UNITY_EDITOR
if (!silent)
{ // Create Undo record
// Important: Copy variables used within anonymous functions within same level (this if block) for correct serialization!
ConnectionPort port1 = this, port2 = port;
UndoPro.UndoProManager.RecordOperation(
() => NodeEditorUndoActions.CreateConnection(port1, port2),
() => NodeEditorUndoActions.DeleteConnection(port1, port2),
"Create Connection");
}
#endif

if (!silent)
{ // Callbacks
port.body.OnAddConnection (port, this);
Expand All @@ -214,6 +226,18 @@ public void ClearConnections (bool silent = false)
/// </summary>
public void RemoveConnection (ConnectionPort port, bool silent = false)
{
#if UNITY_EDITOR
if (silent == false && port != null)
{ // Undo record
// Important: Copy variables used within anonymous functions within same level (this if block) for correct serialization!
ConnectionPort port1 = this, port2 = port;
UndoPro.UndoProManager.RecordOperation(
() => NodeEditorUndoActions.DeleteConnection(port1, port2),
() => NodeEditorUndoActions.CreateConnection(port1, port2),
"Delete Connection");
}
#endif

if (port == null)
{
Debug.LogWarning("Cannot delete null port!");
Expand Down
61 changes: 53 additions & 8 deletions Node_Editor/Framework/Core/Node.cs
Expand Up @@ -209,12 +209,12 @@ public static Node Create (string nodeID, Vector2 pos, NodeCanvas hostCanvas, Co
ConnectionPortManager.UpdateConnectionPorts (node);
if (init)
node.OnCreate();

if (connectingPort != null)
{ // Handle auto-connection and link the output to the first compatible input
for (int i = 0; i < node.connectionPorts.Count; i++)
{
if (node.connectionPorts[i].TryApplyConnection (connectingPort, silent))
if (node.connectionPorts[i].TryApplyConnection (connectingPort, true))
break;
}
}
Expand All @@ -229,6 +229,26 @@ public static Node Create (string nodeID, Vector2 pos, NodeCanvas hostCanvas, Co
NodeEditor.RepaintClients();
}

#if UNITY_EDITOR
if (!silent)
{
List<ConnectionPort> connectedPorts = new List<ConnectionPort>();
foreach (ConnectionPort port in node.connectionPorts)
{ // 'Encode' connected ports in one list (double level cannot be serialized)
foreach (ConnectionPort conn in port.connections)
connectedPorts.Add(conn);
connectedPorts.Add(null);
}
Node createdNode = node;
UndoPro.UndoProManager.RecordOperation(
() => NodeEditorUndoActions.ReinstateNode(createdNode, connectedPorts),
() => NodeEditorUndoActions.RemoveNode(createdNode),
"Create Node");
// Make sure the new node is in the memory dump
NodeEditorUndoActions.CompleteSOMemoryDump(hostCanvas);
}
#endif

return node;
}

Expand All @@ -241,13 +261,31 @@ public void Delete (bool silent = false)
throw new UnityException ("The Node " + name + " does not exist on the Canvas " + canvas.name + "!");
if (!silent)
NodeEditorCallbacks.IssueOnDeleteNode (this);
canvas.nodes.Remove (this);
for (int i = 0; i < connectionPorts.Count; i++)

#if UNITY_EDITOR
if (!silent)
{
connectionPorts[i].ClearConnections (silent);
DestroyImmediate (connectionPorts[i], true);
List<ConnectionPort> connectedPorts = new List<ConnectionPort>();
foreach (ConnectionPort port in connectionPorts)
{ // 'Encode' connected ports in one list (double level cannot be serialized)
foreach (ConnectionPort conn in port.connections)
connectedPorts.Add(conn);
connectedPorts.Add(null);
}
Node deleteNode = this;
UndoPro.UndoProManager.RecordOperation(
() => NodeEditorUndoActions.RemoveNode(deleteNode),
() => NodeEditorUndoActions.ReinstateNode(deleteNode, connectedPorts),
"Delete Node");
// Make sure the deleted node is in the memory dump
NodeEditorUndoActions.CompleteSOMemoryDump(canvas);
}
DestroyImmediate (this, true);
#endif

canvas.nodes.Remove(this);
for (int i = 0; i < connectionPorts.Count; i++)
connectionPorts[i].ClearConnections(true);

if (!silent)
canvas.Validate ();
}
Expand Down Expand Up @@ -296,7 +334,14 @@ protected internal virtual void DrawNode ()

// Call NodeGUI
GUI.changed = false;
NodeGUI ();

#if UNITY_EDITOR // Record changes done in the GUI function
UnityEditor.Undo.RecordObject(this, "Node GUI");
#endif
NodeGUI();
#if UNITY_EDITOR // Make sure it doesn't record anything else after this
UnityEditor.Undo.FlushUndoRecordObjects();
#endif

if(Event.current.type == EventType.Repaint)
nodeGUIHeight = GUILayoutUtility.GetLastRect().max + contentOffset;
Expand Down
3 changes: 3 additions & 0 deletions Node_Editor/Framework/Core/NodeCanvas.cs
Expand Up @@ -27,6 +27,9 @@ public abstract class NodeCanvas : ScriptableObject

public List<Node> nodes = new List<Node> ();
public List<NodeGroup> groups = new List<NodeGroup> ();

[NonSerialized]
public List<ScriptableObject> SOMemoryDump = new List<ScriptableObject>();

#region Constructors

Expand Down
39 changes: 30 additions & 9 deletions Node_Editor/Framework/Interface/NodeEditorInputControls.cs
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using System;
using System.Collections.Generic;

Expand Down Expand Up @@ -56,7 +56,7 @@ private static void DuplicateNode (NodeEditorInputInfo inputInfo)
NodeEditorState state = inputInfo.editorState;
if (state.focusedNode != null && state.canvas.CanAddNode (state.focusedNode.GetID))
{ // Create new node of same type
Node duplicatedNode = Node.Create (state.focusedNode.GetID, NodeEditor.ScreenToCanvasSpace (inputInfo.inputPos), state.canvas, state.connectKnob);
Node.Create (state.focusedNode.GetID, NodeEditor.ScreenToCanvasSpace (inputInfo.inputPos), state.canvas, state.connectKnob);
state.connectKnob = null;
inputInfo.inputEvent.Use ();
}
Expand Down Expand Up @@ -137,7 +137,10 @@ private static void HandleNodeDragging (NodeEditorInputInfo inputInfo)
if (state.selectedNode != null && inputInfo.editorState.dragUserID == "node")
{ // Apply new position for the dragged node
state.UpdateDrag ("node", inputInfo.inputPos);
state.selectedNode.position = state.dragObjectPos;
if ((state.dragObjectPos - state.dragObjectStart).magnitude > 10)
state.selectedNode.position = state.dragObjectPos;
else
state.selectedNode.position = state.dragObjectStart;
NodeEditor.RepaintClients ();
}
else
Expand All @@ -149,14 +152,26 @@ private static void HandleNodeDragging (NodeEditorInputInfo inputInfo)
[EventHandlerAttribute (EventType.MouseUp)]
private static void HandleNodeDraggingEnd (NodeEditorInputInfo inputInfo)
{
if (inputInfo.editorState.dragUserID == "node")
if (inputInfo.editorState.dragUserID == "node")
{
Vector2 totalDrag = inputInfo.editorState.EndDrag ("node");
if (inputInfo.editorState.dragNode && inputInfo.editorState.selectedNode != null)
Vector2 dragStart = inputInfo.editorState.dragObjectStart;
Vector2 dragEnd = inputInfo.editorState.EndDrag("node");
if (inputInfo.editorState.dragNode && inputInfo.editorState.selectedNode != null && (dragStart - dragEnd).magnitude > 10)
{
inputInfo.editorState.selectedNode.position = totalDrag;
NodeEditorCallbacks.IssueOnMoveNode (inputInfo.editorState.selectedNode);
inputInfo.editorState.selectedNode.position = dragEnd;
#if UNITY_EDITOR
// Important: Copy variables used within anonymous functions within same level (this if block) for correct serialization!
float prevX = dragStart.x, prevY = dragStart.y, newX = dragEnd.x, newY = dragEnd.y;
Node draggedNode = inputInfo.editorState.selectedNode;
UndoPro.UndoProManager.RecordOperation(
() => NodeEditorUndoActions.SetNodePosition(draggedNode, new Vector2(newX, newY)),
() => NodeEditorUndoActions.SetNodePosition(draggedNode, new Vector2(prevX, prevY)),
"Node Drag", true, false);
#endif
NodeEditorCallbacks.IssueOnMoveNode(inputInfo.editorState.selectedNode);
}
else
inputInfo.editorState.selectedNode.position = dragStart;
}
inputInfo.editorState.dragNode = false;
}
Expand Down Expand Up @@ -240,7 +255,13 @@ private static void HandleApplyConnection (NodeEditorInputInfo inputInfo)
NodeEditorState state = inputInfo.editorState;
if (inputInfo.inputEvent.button == 0 && state.connectKnob != null && state.focusedNode != null && state.focusedConnectionKnob != null && state.focusedConnectionKnob != state.connectKnob)
{ // A connection curve was dragged and released onto a connection knob
state.focusedConnectionKnob.TryApplyConnection (state.connectKnob);
#if UNITY_EDITOR
UndoPro.UndoProManager.BeginRecordStack();
#endif
state.focusedConnectionKnob.TryApplyConnection(state.connectKnob);
#if UNITY_EDITOR
UndoPro.UndoProManager.EndRecordStack();
#endif
inputInfo.inputEvent.Use ();
}
state.connectKnob = null;
Expand Down
22 changes: 14 additions & 8 deletions Node_Editor/Framework/Interface/NodeEditorInputSystem.cs
Expand Up @@ -243,18 +243,24 @@ private static void HandleFocussing (NodeEditorInputInfo inputInfo)
[EventHandlerAttribute (EventType.MouseDown, -2)] // Absolute second to call!
private static void HandleSelecting (NodeEditorInputInfo inputInfo)
{
NodeEditorState state = inputInfo.editorState;
if (inputInfo.inputEvent.button == 0 && state.focusedNode != state.selectedNode)
if (inputInfo.inputEvent.button == 0 && inputInfo.editorState.focusedNode != inputInfo.editorState.selectedNode)
{ // Select focussed Node
unfocusControlsForState = state;
state.selectedNode = state.focusedNode;
NodeEditor.RepaintClients ();
unfocusControlsForState = inputInfo.editorState;
inputInfo.editorState.selectedNode = inputInfo.editorState.focusedNode;
#if UNITY_EDITOR
NodeEditorState state = inputInfo.editorState;
Node prevSelection = state.selectedNode, newSelection = state.focusedNode;
UndoPro.UndoProManager.RecordOperation(
() => NodeEditorUndoActions.SetNodeSelection(state, newSelection),
() => NodeEditorUndoActions.SetNodeSelection(state, prevSelection),
"Node Selection", false, true);
#endif
}
#if UNITY_EDITOR
if (state.selectedNode != null)
UnityEditor.Selection.activeObject = state.selectedNode;
if (inputInfo.editorState.selectedNode != null)
UnityEditor.Selection.activeObject = inputInfo.editorState.selectedNode;
else if (UnityEditor.Selection.activeObject is Node)
UnityEditor.Selection.activeObject = state.canvas;
UnityEditor.Selection.activeObject = inputInfo.editorState.canvas;
#endif
}

Expand Down

0 comments on commit 0f69501

Please sign in to comment.