diff --git a/.gitignore b/.gitignore
index 68af404..1460751 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,8 @@ sysinfo.txt
/Examples/
README.md.meta
LICENSE.md.meta
-CONTRIBUTING.md.meta
\ No newline at end of file
+CONTRIBUTING.md.meta
+
+.git.meta
+.gitignore.meta
+.gitattributes.meta
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b96a660..8638230 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,6 +18,14 @@ If your feature aims to cover something not related to editing nodes, it general
Skim through the code and you'll get the hang of it quickly.
* Methods, Types and properties PascalCase
* Variables camelCase
-* Public methods XML commented
+* Public methods XML commented. Params described if not obvious
+* Explicit usage of brackets when doing multiple math operations on the same line
+
+## Formatting
+I use VSCode with the C# FixFormat extension and the following setting overrides:
+```json
+"csharpfixformat.style.spaces.beforeParenthesis": false,
+"csharpfixformat.style.indent.regionIgnored": true
+```
* Open braces on same line as condition
* 4 spaces for indentation.
diff --git a/README.md b/README.md
index dcaa133..2b59422 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
[](https://discord.gg/qgPrHv4)
[](https://github.com/Siccity/xNode/issues)
@@ -15,7 +15,9 @@ Thinking of developing a node-based plugin? Then this is for you. You can downlo
xNode is super userfriendly, intuitive and will help you reap the benefits of node graphs in no time.
With a minimal footprint, it is ideal as a base for custom state machines, dialogue systems, decision makers etc.
-
+
+
+
### Key features
* Lightweight in runtime
@@ -24,6 +26,7 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
* No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.)
* Does not rely on any 3rd party plugins
* Custom node inspector code is very similar to regular custom inspector code
+* Supported from Unity 5.3 and up
### Wiki
* [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph
@@ -31,7 +34,7 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
### Node example:
```csharp
-[System.Serializable]
+// public classes deriving from Node are registered as nodes for use within a graph
public class MathNode : Node {
// Adding [Input] or [Output] is all you need to do to register a field as a valid port on your node
[Input] public float a;
@@ -71,3 +74,4 @@ Feel free to also leave suggestions/requests in the [issues](https://github.com/
Projects using xNode:
* [Graphmesh](https://github.com/Siccity/Graphmesh "Go to github page")
* [Dialogue](https://github.com/Siccity/Dialogue "Go to github page")
+* [qAI](https://github.com/jlreymendez/qAI "Go to github page")
diff --git a/Scripts/Attributes.meta b/Scripts/Attributes.meta
new file mode 100644
index 0000000..c0be849
--- /dev/null
+++ b/Scripts/Attributes.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 5644dfc7eed151045af664a9d4fd1906
+folderAsset: yes
+timeCreated: 1541633926
+licenseType: Free
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Attributes/NodeEnum.cs b/Scripts/Attributes/NodeEnum.cs
new file mode 100644
index 0000000..9cdaef4
--- /dev/null
+++ b/Scripts/Attributes/NodeEnum.cs
@@ -0,0 +1,5 @@
+using UnityEngine;
+
+/// Draw enums correctly within nodes. Without it, enums show up at the wrong positions.
+/// Enums with this attribute are not detected by EditorGui.ChangeCheck due to waiting before executing
+public class NodeEnumAttribute : PropertyAttribute { }
\ No newline at end of file
diff --git a/Scripts/Attributes/NodeEnum.cs.meta b/Scripts/Attributes/NodeEnum.cs.meta
new file mode 100644
index 0000000..813a80b
--- /dev/null
+++ b/Scripts/Attributes/NodeEnum.cs.meta
@@ -0,0 +1,13 @@
+fileFormatVersion: 2
+guid: 10a8338f6c985854697b35459181af0a
+timeCreated: 1541633942
+licenseType: Free
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Drawers.meta b/Scripts/Editor/Drawers.meta
new file mode 100644
index 0000000..b69e0ac
--- /dev/null
+++ b/Scripts/Editor/Drawers.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 7adf21edfb51f514fa991d7556ecd0ef
+folderAsset: yes
+timeCreated: 1541971984
+licenseType: Free
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Drawers/NodeEnumDrawer.cs b/Scripts/Editor/Drawers/NodeEnumDrawer.cs
new file mode 100644
index 0000000..7478f94
--- /dev/null
+++ b/Scripts/Editor/Drawers/NodeEnumDrawer.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+using XNode;
+using XNodeEditor;
+
+namespace XNodeEditor {
+ [CustomPropertyDrawer(typeof(NodeEnumAttribute))]
+ public class NodeEnumDrawer : PropertyDrawer {
+ public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
+ EditorGUI.BeginProperty(position, label, property);
+
+ // Throw error on wrong type
+ if (property.propertyType != SerializedPropertyType.Enum) {
+ throw new ArgumentException("Parameter selected must be of type System.Enum");
+ }
+
+ // Add label
+ position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
+
+ // Get current enum name
+ string enumName = "";
+ if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length) enumName = property.enumDisplayNames[property.enumValueIndex];
+
+#if UNITY_2017_1_OR_NEWER
+ // Display dropdown
+ if (EditorGUI.DropdownButton(position, new GUIContent(enumName), FocusType.Passive)) {
+ // Position is all wrong if we show the dropdown during the node draw phase.
+ // Instead, add it to onLateGUI to display it later.
+ NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
+ }
+#else
+ // Display dropdown
+ if (GUI.Button(position, new GUIContent(enumName), "MiniPopup")) {
+ // Position is all wrong if we show the dropdown during the node draw phase.
+ // Instead, add it to onLateGUI to display it later.
+ NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
+ }
+#endif
+ EditorGUI.EndProperty();
+ }
+
+ private void ShowContextMenuAtMouse(SerializedProperty property) {
+ // Initialize menu
+ GenericMenu menu = new GenericMenu();
+
+ // Add all enum display names to menu
+ for (int i = 0; i < property.enumDisplayNames.Length; i++) {
+ int index = i;
+ menu.AddItem(new GUIContent(property.enumDisplayNames[i]), false, () => SetEnum(property, index));
+ }
+
+ // Display at cursor position
+ Rect r = new Rect(Event.current.mousePosition, new Vector2(0, 0));
+ menu.DropDown(r);
+ }
+
+ private void SetEnum(SerializedProperty property, int index) {
+ property.enumValueIndex = index;
+ property.serializedObject.ApplyModifiedProperties();
+ property.serializedObject.Update();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta b/Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta
new file mode 100644
index 0000000..beacf6b
--- /dev/null
+++ b/Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta
@@ -0,0 +1,13 @@
+fileFormatVersion: 2
+guid: 83db81f92abadca439507e25d517cabe
+timeCreated: 1541633798
+licenseType: Free
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs
index 1469e2d..a6d1748 100644
--- a/Scripts/Editor/NodeEditor.cs
+++ b/Scripts/Editor/NodeEditor.cs
@@ -13,32 +13,9 @@ namespace XNodeEditor {
/// Fires every whenever a node was modified through the editor
public static Action onUpdateNode;
public static Dictionary portPositions;
- public static int renaming;
-
- /// Draws the node GUI.
- /// Port handle positions need to be returned to the NodeEditorWindow
- public void OnNodeGUI() {
- OnHeaderGUI();
- OnBodyGUI();
- }
public virtual void OnHeaderGUI() {
- string title = target.name;
- if (renaming != 0 && Selection.Contains(target)) {
- int controlID = EditorGUIUtility.GetControlID(FocusType.Keyboard) + 1;
- if (renaming == 1) {
- EditorGUIUtility.keyboardControl = controlID;
- EditorGUIUtility.editingTextField = true;
- renaming = 2;
- }
- target.name = EditorGUILayout.TextField(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
- if (!EditorGUIUtility.editingTextField) {
- Rename(target.name);
- renaming = 0;
- }
- } else {
- GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
- }
+ GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
}
/// Draws standard field editors for all public fields
@@ -50,6 +27,7 @@ namespace XNodeEditor {
string[] excludes = { "m_Script", "graph", "position", "ports" };
portPositions = new Dictionary();
+ // Iterate through serialized properties and draw them like the Inspector (But with ports)
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
EditorGUIUtility.labelWidth = 84;
@@ -58,6 +36,13 @@ namespace XNodeEditor {
if (excludes.Contains(iterator.name)) continue;
NodeEditorGUILayout.PropertyField(iterator, true);
}
+
+ // Iterate through instance ports and draw them in the order in which they are serialized
+ foreach (XNode.NodePort instancePort in target.InstancePorts) {
+ if (NodeEditorGUILayout.IsInstancePortListPort(instancePort)) continue;
+ NodeEditorGUILayout.PortField(instancePort);
+ }
+
serializedObject.ApplyModifiedProperties();
}
@@ -75,11 +60,33 @@ namespace XNodeEditor {
else return Color.white;
}
- public void InitiateRename() {
- renaming = 1;
+ public virtual GUIStyle GetBodyStyle() {
+ return NodeEditorResources.styles.nodeBody;
}
+ /// Add items for the context menu when right-clicking this node. Override to add custom menu items.
+ public virtual void AddContextMenuItems(GenericMenu menu) {
+ // Actions if only one node is selected
+ if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
+ XNode.Node node = Selection.activeObject as XNode.Node;
+ menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node));
+ menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode);
+ }
+
+ // Add actions to any number of selected nodes
+ menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes);
+ menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
+
+ // Custom sctions if only one node is selected
+ if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
+ XNode.Node node = Selection.activeObject as XNode.Node;
+ NodeEditorWindow.AddCustomContextMenuItems(menu, node);
+ }
+ }
+
+ /// Rename the node asset. This will trigger a reimport of the node.
public void Rename(string newName) {
+ if (newName == null || newName.Trim() == "") newName = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name);
target.name = newName;
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
}
@@ -90,7 +97,6 @@ namespace XNodeEditor {
private Type inspectedType;
/// Tells a NodeEditor which Node type it is an editor for
/// Type that this editor can edit
- /// Path to the node
public CustomNodeEditorAttribute(Type inspectedType) {
this.inspectedType = inspectedType;
}
diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs
index 9682d17..0570777 100644
--- a/Scripts/Editor/NodeEditorAction.cs
+++ b/Scripts/Editor/NodeEditorAction.cs
@@ -22,11 +22,11 @@ namespace XNodeEditor {
[NonSerialized] private List draggedOutputReroutes = new List();
private RerouteReference hoveredReroute = new RerouteReference();
private List selectedReroutes = new List();
- private Rect nodeRects;
private Vector2 dragBoxStart;
private UnityEngine.Object[] preBoxSelection;
private RerouteReference[] preBoxSelectionReroute;
private Rect selectionBox;
+ private bool isDoubleClick = false;
private struct RerouteReference {
public XNode.NodePort port;
@@ -52,13 +52,15 @@ namespace XNodeEditor {
case EventType.MouseMove:
break;
case EventType.ScrollWheel:
+ float oldZoom = zoom;
if (e.delta.y > 0) zoom += 0.1f * zoom;
else zoom -= 0.1f * zoom;
+ if (NodeEditorPreferences.GetSettings().zoomToMouse) panOffset += (1 - oldZoom / zoom) * (WindowToGridPosition(e.mousePosition) + panOffset);
break;
case EventType.MouseDrag:
if (e.button == 0) {
if (IsDraggingPort) {
- if (IsHoveringPort && hoveredPort.IsInput) {
+ if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) {
if (!draggedOutput.IsConnectedTo(hoveredPort)) {
draggedOutputTarget = hoveredPort;
}
@@ -134,12 +136,7 @@ namespace XNodeEditor {
Repaint();
}
} else if (e.button == 1 || e.button == 2) {
- Vector2 tempOffset = panOffset;
- tempOffset += e.delta * zoom;
- // Round value to increase crispyness of UI text
- tempOffset.x = Mathf.Round(tempOffset.x);
- tempOffset.y = Mathf.Round(tempOffset.y);
- panOffset = tempOffset;
+ panOffset += e.delta * zoom;
isPanning = true;
}
break;
@@ -170,6 +167,10 @@ namespace XNodeEditor {
SelectNode(hoveredNode, e.control || e.shift);
if (!e.control && !e.shift) selectedReroutes.Clear();
} else if (e.control || e.shift) DeselectNode(hoveredNode);
+
+ // Cache double click state, but only act on it in MouseUp - Except ClickCount only works in mouseDown.
+ isDoubleClick = (e.clickCount == 2);
+
e.Use();
currentActivity = NodeActivity.HoldNode;
} else if (IsHoveringReroute) {
@@ -229,6 +230,7 @@ namespace XNodeEditor {
// If click outside node, release field focus
if (!isPanning) {
EditorGUI.FocusTextInControl(null);
+ EditorGUIUtility.editingTextField = false;
}
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
@@ -237,6 +239,12 @@ namespace XNodeEditor {
if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) {
selectedReroutes.Clear();
SelectNode(hoveredNode, false);
+
+ // Double click to center node
+ if (isDoubleClick) {
+ Vector2 nodeDimension = nodeSizes.ContainsKey(hoveredNode) ? nodeSizes[hoveredNode] / 2 : Vector2.zero;
+ panOffset = -hoveredNode.position - nodeDimension;
+ }
}
// If click reroute, select it.
@@ -260,26 +268,42 @@ namespace XNodeEditor {
ShowPortContextMenu(hoveredPort);
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false);
- ShowNodeContextMenu();
+ GenericMenu menu = new GenericMenu();
+ NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu);
+ menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
+ e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places.
} else if (!IsHoveringNode) {
- ShowGraphContextMenu();
+ GenericMenu menu = new GenericMenu();
+ graphEditor.AddContextMenuItems(menu);
+ menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
}
}
isPanning = false;
}
+ // Reset DoubleClick
+ isDoubleClick = false;
break;
case EventType.KeyDown:
if (EditorGUIUtility.editingTextField) break;
else if (e.keyCode == KeyCode.F) Home();
- if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) {
+ if (IsMac()) {
if (e.keyCode == KeyCode.Return) RenameSelectedNode();
} else {
if (e.keyCode == KeyCode.F2) RenameSelectedNode();
}
break;
case EventType.ValidateCommand:
- if (e.commandName == "SoftDelete") RemoveSelectedNodes();
- else if (e.commandName == "Duplicate") DublicateSelectedNodes();
+ case EventType.ExecuteCommand:
+ if (e.commandName == "SoftDelete") {
+ if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
+ e.Use();
+ } else if (IsMac() && e.commandName == "Delete") {
+ if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
+ e.Use();
+ } else if (e.commandName == "Duplicate") {
+ if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes();
+ e.Use();
+ }
Repaint();
break;
case EventType.Ignore:
@@ -292,6 +316,14 @@ namespace XNodeEditor {
}
}
+ public bool IsMac() {
+#if UNITY_2017_1_OR_NEWER
+ return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
+#else
+ return SystemInfo.operatingSystem.StartsWith("Mac");
+#endif
+ }
+
private void RecalculateDragOffsets(Event current) {
dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count];
// Selected nodes
@@ -314,15 +346,6 @@ namespace XNodeEditor {
panOffset = Vector2.zero;
}
- public void CreateNode(Type type, Vector2 position) {
- XNode.Node node = graph.AddNode(type);
- node.position = position;
- node.name = UnityEditor.ObjectNames.NicifyVariableName(type.Name);
- AssetDatabase.AddObjectToAsset(node, graph);
- if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
- Repaint();
- }
-
/// Remove nodes in the graph in Selection.objects
public void RemoveSelectedNodes() {
// We need to delete reroutes starting at the highest point index to avoid shifting indices
@@ -343,7 +366,12 @@ namespace XNodeEditor {
public void RenameSelectedNode() {
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
XNode.Node node = Selection.activeObject as XNode.Node;
- NodeEditor.GetEditor(node).InitiateRename();
+ Vector2 size;
+ if (nodeSizes.TryGetValue(node, out size)) {
+ RenamePopup.Show(Selection.activeObject, size.x);
+ } else {
+ RenamePopup.Show(Selection.activeObject);
+ }
}
}
@@ -356,8 +384,8 @@ namespace XNodeEditor {
}
}
- /// Dublicate selected nodes and select the dublicates
- public void DublicateSelectedNodes() {
+ /// Duplicate selected nodes and select the duplicates
+ public void DuplicateSelectedNodes() {
UnityEngine.Object[] newNodes = new UnityEngine.Object[Selection.objects.Length];
Dictionary substitutes = new Dictionary();
for (int i = 0; i < Selection.objects.Length; i++) {
@@ -404,7 +432,7 @@ namespace XNodeEditor {
Rect fromRect;
if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return;
Vector2 from = fromRect.center;
- col.a = 0.6f;
+ col.a = draggedOutputTarget != null ? 1.0f : 0.6f;
Vector2 to = Vector2.zero;
for (int i = 0; i < draggedOutputReroutes.Count; i++) {
to = draggedOutputReroutes[i];
diff --git a/Scripts/Editor/NodeEditorBase.cs b/Scripts/Editor/NodeEditorBase.cs
index 2a16f18..ab463e6 100644
--- a/Scripts/Editor/NodeEditorBase.cs
+++ b/Scripts/Editor/NodeEditorBase.cs
@@ -7,14 +7,18 @@ using UnityEngine;
namespace XNodeEditor.Internal {
/// Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type)
- public class NodeEditorBase where A : Attribute, NodeEditorBase.INodeEditorAttrib where T : NodeEditorBase where K : ScriptableObject {
+ /// Editor Type. Should be the type of the deriving script itself (eg. NodeEditor)
+ /// Attribute Type. The attribute used to connect with the runtime type (eg. CustomNodeEditorAttribute)
+ /// Runtime Type. The ScriptableObject this can be an editor for (eg. Node)
+ public abstract class NodeEditorBase where A : Attribute, NodeEditorBase.INodeEditorAttrib where T : NodeEditorBase where K : ScriptableObject {
/// Custom editors defined with [CustomNodeEditor]
private static Dictionary editorTypes;
private static Dictionary editors = new Dictionary();
+ public NodeEditorWindow window;
public K target;
public SerializedObject serializedObject;
- public static T GetEditor(K target) {
+ public static T GetEditor(K target, NodeEditorWindow window) {
if (target == null) return null;
T editor;
if (!editors.TryGetValue(target, out editor)) {
@@ -23,9 +27,12 @@ namespace XNodeEditor.Internal {
editor = Activator.CreateInstance(editorType) as T;
editor.target = target;
editor.serializedObject = new SerializedObject(target);
+ editor.window = window;
+ editor.OnCreate();
editors.Add(target, editor);
}
if (editor.target == null) editor.target = target;
+ if (editor.window != window) editor.window = window;
if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target);
return editor;
}
@@ -53,6 +60,9 @@ namespace XNodeEditor.Internal {
}
}
+ /// Called on creation, after references have been set
+ public virtual void OnCreate() { }
+
public interface INodeEditorAttrib {
Type GetInspectedType();
}
diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs
index d00bb8d..0ae44ab 100644
--- a/Scripts/Editor/NodeEditorGUI.cs
+++ b/Scripts/Editor/NodeEditorGUI.cs
@@ -10,14 +10,15 @@ namespace XNodeEditor {
public NodeGraphEditor graphEditor;
private List selectionCache;
private List culledNodes;
+ private int topPadding { get { return isDocked() ? 19 : 22; } }
+ /// Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run.
+ public event Action onLateGUI;
private void OnGUI() {
Event e = Event.current;
Matrix4x4 m = GUI.matrix;
if (graph == null) return;
- graphEditor = NodeGraphEditor.GetEditor(graph);
- graphEditor.position = position;
-
+ ValidateGraphEditor();
Controls();
DrawGrid(position, zoom, panOffset);
@@ -28,25 +29,31 @@ namespace XNodeEditor {
DrawTooltip();
graphEditor.OnGUI();
+ // Run and reset onLateGUI
+ if (onLateGUI != null) {
+ onLateGUI();
+ onLateGUI = null;
+ }
+
GUI.matrix = m;
}
- public static void BeginZoomed(Rect rect, float zoom) {
+ public static void BeginZoomed(Rect rect, float zoom, float topPadding) {
GUI.EndClip();
GUIUtility.ScaleAroundPivot(Vector2.one / zoom, rect.size * 0.5f);
- Vector4 padding = new Vector4(0, 22, 0, 0);
+ Vector4 padding = new Vector4(0, topPadding, 0, 0);
padding *= zoom;
- GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (22 * zoom),
+ GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (topPadding * zoom),
rect.width * zoom,
rect.height * zoom));
}
- public static void EndZoomed(Rect rect, float zoom) {
+ public static void EndZoomed(Rect rect, float zoom, float topPadding) {
GUIUtility.ScaleAroundPivot(Vector2.one * zoom, rect.size * 0.5f);
Vector3 offset = new Vector3(
(((rect.width * zoom) - rect.width) * 0.5f),
- (((rect.height * zoom) - rect.height) * 0.5f) + (-22 * zoom) + 22,
+ (((rect.height * zoom) - rect.height) * 0.5f) + (-topPadding * zoom) + topPadding,
0);
GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one);
}
@@ -107,60 +114,6 @@ namespace XNodeEditor {
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
- /// Show right-click context menu for selected nodes
- public void ShowNodeContextMenu() {
- GenericMenu contextMenu = new GenericMenu();
- // If only one node is selected
- if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
- XNode.Node node = Selection.activeObject as XNode.Node;
- contextMenu.AddItem(new GUIContent("Move To Top"), false, () => MoveNodeToTop(node));
- contextMenu.AddItem(new GUIContent("Rename"), false, RenameSelectedNode);
- }
-
- contextMenu.AddItem(new GUIContent("Duplicate"), false, DublicateSelectedNodes);
- contextMenu.AddItem(new GUIContent("Remove"), false, RemoveSelectedNodes);
-
- // If only one node is selected
- if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
- XNode.Node node = Selection.activeObject as XNode.Node;
- AddCustomContextMenuItems(contextMenu, node);
- }
-
- contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
- }
-
- /// Show right-click context menu for current graph
- void ShowGraphContextMenu() {
- GenericMenu contextMenu = new GenericMenu();
- Vector2 pos = WindowToGridPosition(Event.current.mousePosition);
- for (int i = 0; i < nodeTypes.Length; i++) {
- Type type = nodeTypes[i];
-
- //Get node context menu path
- string path = graphEditor.GetNodeMenuName(type);
- if (string.IsNullOrEmpty(path)) continue;
-
- contextMenu.AddItem(new GUIContent(path), false, () => {
- CreateNode(type, pos);
- });
- }
- contextMenu.AddSeparator("");
- contextMenu.AddItem(new GUIContent("Preferences"), false, () => OpenPreferences());
- AddCustomContextMenuItems(contextMenu, graph);
- contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
- }
-
- void AddCustomContextMenuItems(GenericMenu contextMenu, object obj) {
- KeyValuePair[] items = GetContextMenuMethods(obj);
- if (items.Length != 0) {
- contextMenu.AddSeparator("");
- for (int i = 0; i < items.Length; i++) {
- KeyValuePair kvp = items[i];
- contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null));
- }
- }
- }
-
/// Draw a bezier from startpoint to endpoint, both in grid coordinates
public void DrawConnection(Vector2 startPoint, Vector2 endPoint, Color col) {
startPoint = GridToWindowPosition(startPoint);
@@ -229,7 +182,7 @@ namespace XNodeEditor {
Rect fromRect;
if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue;
- Color connectionColor = graphEditor.GetTypeColor(output.ValueType);
+ Color connectionColor = graphEditor.GetPortColor(output);
for (int k = 0; k < output.ConnectionCount; k++) {
XNode.NodePort input = output.GetConnection(k);
@@ -286,15 +239,13 @@ namespace XNodeEditor {
selectionCache = new List(Selection.objects);
}
- //Active node is hashed before and after node GUI to detect changes
- int nodeHash = 0;
System.Reflection.MethodInfo onValidate = null;
if (Selection.activeObject != null && Selection.activeObject is XNode.Node) {
onValidate = Selection.activeObject.GetType().GetMethod("OnValidate");
- if (onValidate != null) nodeHash = Selection.activeObject.GetHashCode();
+ if (onValidate != null) EditorGUI.BeginChangeCheck();
}
- BeginZoomed(position, zoom);
+ BeginZoomed(position, zoom, topPadding);
Vector2 mousePos = Event.current.mousePosition;
@@ -335,7 +286,7 @@ namespace XNodeEditor {
_portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
- NodeEditor nodeEditor = NodeEditor.GetEditor(node);
+ NodeEditor nodeEditor = NodeEditor.GetEditor(node, this);
NodeEditor.portPositions = new Dictionary();
@@ -347,25 +298,26 @@ namespace XNodeEditor {
bool selected = selectionCache.Contains(graph.nodes[n]);
if (selected) {
- GUIStyle style = new GUIStyle(NodeEditorResources.styles.nodeBody);
+ GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight);
highlightStyle.padding = style.padding;
style.padding = new RectOffset();
GUI.color = nodeEditor.GetTint();
- GUILayout.BeginVertical(new GUIStyle(style));
+ GUILayout.BeginVertical(style);
GUI.color = NodeEditorPreferences.GetSettings().highlightColor;
GUILayout.BeginVertical(new GUIStyle(highlightStyle));
} else {
- GUIStyle style = NodeEditorResources.styles.nodeBody;
+ GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
GUI.color = nodeEditor.GetTint();
- GUILayout.BeginVertical(new GUIStyle(style));
+ GUILayout.BeginVertical(style);
}
GUI.color = guiColor;
EditorGUI.BeginChangeCheck();
//Draw node contents
- nodeEditor.OnNodeGUI();
+ nodeEditor.OnHeaderGUI();
+ nodeEditor.OnBodyGUI();
//If user changed a value, notify other scripts through onUpdateNode
if (EditorGUI.EndChangeCheck()) {
@@ -425,14 +377,12 @@ namespace XNodeEditor {
}
if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray();
- EndZoomed(position, zoom);
+ EndZoomed(position, zoom, topPadding);
- //If a change in hash is detected in the selected node, call OnValidate method.
+ //If a change in is detected in the selected node, call OnValidate method.
//This is done through reflection because OnValidate is only relevant in editor,
//and thus, the code should not be included in build.
- if (nodeHash != 0) {
- if (onValidate != null && nodeHash != Selection.activeObject.GetHashCode()) onValidate.Invoke(Selection.activeObject, null);
- }
+ if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null);
}
private bool ShouldBeCulled(XNode.Node node) {
diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs
index 3a90e36..e3f7c60 100644
--- a/Scripts/Editor/NodeEditorGUILayout.cs
+++ b/Scripts/Editor/NodeEditorGUILayout.cs
@@ -1,14 +1,19 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
+using UnityEditorInternal;
using UnityEngine;
namespace XNodeEditor {
/// xNode-specific version of
public static class NodeEditorGUILayout {
+ private static readonly Dictionary> reorderableListCache = new Dictionary>();
+ private static int reorderableListIndex = -1;
+
/// Make a field for a serialized property. Automatically displays relevant node port.
public static void PropertyField(SerializedProperty property, bool includeChildren = true, params GUILayoutOption[] options) {
PropertyField(property, (GUIContent) null, includeChildren, options);
@@ -36,13 +41,36 @@ namespace XNodeEditor {
else {
Rect rect = new Rect();
+ float spacePadding = 0;
+ SpaceAttribute spaceAttribute;
+ if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out spaceAttribute)) spacePadding = spaceAttribute.height;
+
// If property is an input, display a regular property field and put a port handle on the left side
if (port.direction == XNode.NodePort.IO.Input) {
// Get data from [Input] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.InputAttribute inputAttribute;
- if (NodeEditorUtilities.GetAttrib(port.node.GetType(), property.name, out inputAttribute)) showBacking = inputAttribute.backingValue;
+ bool instancePortList = false;
+ if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) {
+ instancePortList = inputAttribute.instancePortList;
+ showBacking = inputAttribute.backingValue;
+ }
+ //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
+ bool useLayoutSpace = instancePortList ||
+ showBacking == XNode.Node.ShowBackingValue.Never ||
+ (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
+ if (spacePadding > 0 && useLayoutSpace) {
+ GUILayout.Space(spacePadding);
+ spacePadding = 0;
+ }
+
+ if (instancePortList) {
+ Type type = GetType(property);
+ XNode.Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
+ InstancePortList(property.name, type, property.serializedObject, port.direction, connectionType);
+ return;
+ }
switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected
@@ -61,14 +89,33 @@ namespace XNodeEditor {
}
rect = GUILayoutUtility.GetLastRect();
- rect.position = rect.position - new Vector2(16, 0);
+ rect.position = rect.position - new Vector2(16, -spacePadding);
// If property is an output, display a text label and put a port handle on the right side
} else if (port.direction == XNode.NodePort.IO.Output) {
// Get data from [Output] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.OutputAttribute outputAttribute;
- if (NodeEditorUtilities.GetAttrib(port.node.GetType(), property.name, out outputAttribute)) showBacking = outputAttribute.backingValue;
+ bool instancePortList = false;
+ if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) {
+ instancePortList = outputAttribute.instancePortList;
+ showBacking = outputAttribute.backingValue;
+ }
+ //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
+ bool useLayoutSpace = instancePortList ||
+ showBacking == XNode.Node.ShowBackingValue.Never ||
+ (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
+ if (spacePadding > 0 && useLayoutSpace) {
+ GUILayout.Space(spacePadding);
+ spacePadding = 0;
+ }
+
+ if (instancePortList) {
+ Type type = GetType(property);
+ XNode.Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
+ InstancePortList(property.name, type, property.serializedObject, port.direction, connectionType);
+ return;
+ }
switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected
@@ -87,7 +134,7 @@ namespace XNodeEditor {
}
rect = GUILayoutUtility.GetLastRect();
- rect.position = rect.position + new Vector2(rect.width, 0);
+ rect.position = rect.position + new Vector2(rect.width, spacePadding);
}
rect.size = new Vector2(16, 16);
@@ -95,7 +142,7 @@ namespace XNodeEditor {
Color backgroundColor = new Color32(90, 97, 105, 255);
Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
- Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType);
+ Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col);
// Register the handle position
@@ -105,6 +152,12 @@ namespace XNodeEditor {
}
}
+ private static System.Type GetType(SerializedProperty property) {
+ System.Type parentType = property.serializedObject.targetObject.GetType();
+ System.Reflection.FieldInfo fi = NodeEditorWindow.GetFieldInfo(parentType, property.name);
+ return fi.FieldType;
+ }
+
/// Make a simple port field.
public static void PortField(XNode.NodePort port, params GUILayoutOption[] options) {
PortField(null, port, options);
@@ -114,7 +167,7 @@ namespace XNodeEditor {
public static void PortField(GUIContent label, XNode.NodePort port, params GUILayoutOption[] options) {
if (port == null) return;
if (options == null) options = new GUILayoutOption[] { GUILayout.MinWidth(30) };
- Rect rect = new Rect();
+ Vector2 position = Vector3.zero;
GUIContent content = label != null ? label : new GUIContent(ObjectNames.NicifyVariableName(port.fieldName));
// If property is an input, display a regular property field and put a port handle on the left side
@@ -122,23 +175,31 @@ namespace XNodeEditor {
// Display a label
EditorGUILayout.LabelField(content, options);
- rect = GUILayoutUtility.GetLastRect();
- rect.position = rect.position - new Vector2(16, 0);
- // If property is an output, display a text label and put a port handle on the right side
- } else if (port.direction == XNode.NodePort.IO.Output) {
+ Rect rect = GUILayoutUtility.GetLastRect();
+ position = rect.position - new Vector2(16, 0);
+
+ }
+ // If property is an output, display a text label and put a port handle on the right side
+ else if (port.direction == XNode.NodePort.IO.Output) {
// Display a label
EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options);
- rect = GUILayoutUtility.GetLastRect();
- rect.position = rect.position + new Vector2(rect.width, 0);
+ Rect rect = GUILayoutUtility.GetLastRect();
+ position = rect.position + new Vector2(rect.width, 0);
}
+ PortField(position, port);
+ }
- rect.size = new Vector2(16, 16);
+ /// Make a simple port field.
+ public static void PortField(Vector2 position, XNode.NodePort port) {
+ if (port == null) return;
+
+ Rect rect = new Rect(position, new Vector2(16, 16));
Color backgroundColor = new Color32(90, 97, 105, 255);
Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
- Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType);
+ Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col);
// Register the handle position
@@ -167,7 +228,7 @@ namespace XNodeEditor {
Color backgroundColor = new Color32(90, 97, 105, 255);
Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
- Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType);
+ Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col);
// Register the handle position
@@ -193,9 +254,16 @@ namespace XNodeEditor {
GUI.color = col;
}
- [Obsolete("Use InstancePortList(string, Type, SerializedObject, NodePort.IO, Node.ConnectionType) instead")]
- public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple) {
- InstancePortList(fieldName, type, serializedObject, XNode.NodePort.IO.Output, connectionType);
+ /// Is this port part of an InstancePortList?
+ public static bool IsInstancePortListPort(XNode.NodePort port) {
+ string[] parts = port.fieldName.Split(' ');
+ if (parts.Length != 2) return false;
+ Dictionary cache;
+ if (reorderableListCache.TryGetValue(port.node, out cache)) {
+ ReorderableList list;
+ if (cache.TryGetValue(parts[0], out list)) return true;
+ }
+ return false;
}
/// Draw an editable list of instance ports. Port names are named as "[fieldName] [index]"
@@ -203,28 +271,156 @@ namespace XNodeEditor {
/// Value type of added instance ports
/// The serializedObject of the node
/// Connection type of added instance ports
- public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple) {
+ /// Called on the list on creation. Use this if you want to customize the created ReorderableList
+ public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action onCreation = null) {
XNode.Node node = serializedObject.targetObject as XNode.Node;
- SerializedProperty arrayData = serializedObject.FindProperty(fieldName);
+
+ var indexedPorts = node.InstancePorts.Select(x => {
+ string[] split = x.fieldName.Split(' ');
+ if (split != null && split.Length == 2 && split[0] == fieldName) {
+ int i = -1;
+ if (int.TryParse(split[1], out i)) {
+ return new { index = i, port = x };
+ }
+ }
+ return new { index = -1, port = (XNode.NodePort) null };
+ });
+ List instancePorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
+
+ ReorderableList list = null;
+ Dictionary rlc;
+ if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
+ if (!rlc.TryGetValue(fieldName, out list)) list = null;
+ }
+ // If a ReorderableList isn't cached for this array, do so.
+ if (list == null) {
+ SerializedProperty arrayData = serializedObject.FindProperty(fieldName);
+ list = CreateReorderableList(fieldName, instancePorts, arrayData, type, serializedObject, io, connectionType, typeConstraint, onCreation);
+ if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) rlc.Add(fieldName, list);
+ else reorderableListCache.Add(serializedObject.targetObject, new Dictionary() { { fieldName, list } });
+ }
+ list.list = instancePorts;
+ list.DoLayoutList();
+ }
+
+ private static ReorderableList CreateReorderableList(string fieldName, List instancePorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action onCreation) {
bool hasArrayData = arrayData != null && arrayData.isArray;
- int arraySize = hasArrayData ? arrayData.arraySize : 0;
+ XNode.Node node = serializedObject.targetObject as XNode.Node;
+ ReorderableList list = new ReorderableList(instancePorts, null, true, true, true, true);
+ string label = arrayData != null ? arrayData.displayName : ObjectNames.NicifyVariableName(fieldName);
- Predicate isMatchingInstancePort =
- x => {
- string[] split = x.Split(' ');
- if (split != null && split.Length == 2) return split[0] == fieldName;
- else return false;
+ list.drawElementCallback =
+ (Rect rect, int index, bool isActive, bool isFocused) => {
+ XNode.NodePort port = node.GetPort(fieldName + " " + index);
+ if (hasArrayData) {
+ if (arrayData.arraySize <= index) {
+ EditorGUI.LabelField(rect, "Invalid element " + index);
+ return;
+ }
+ SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index);
+ EditorGUI.PropertyField(rect, itemData, true);
+ } else EditorGUI.LabelField(rect, port.fieldName);
+ if (port != null) {
+ Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0));
+ NodeEditorGUILayout.PortField(pos, port);
+ }
};
- List instancePorts = node.InstancePorts.Where(x => isMatchingInstancePort(x.fieldName)).OrderBy(x => x.fieldName).ToList();
+ list.elementHeightCallback =
+ (int index) => {
+ if (hasArrayData) {
+ if (arrayData.arraySize <= index) return EditorGUIUtility.singleLineHeight;
+ SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index);
+ return EditorGUI.GetPropertyHeight(itemData);
+ } else return EditorGUIUtility.singleLineHeight;
+ };
+ list.drawHeaderCallback =
+ (Rect rect) => {
+ EditorGUI.LabelField(rect, label);
+ };
+ list.onSelectCallback =
+ (ReorderableList rl) => {
+ reorderableListIndex = rl.index;
+ };
+ list.onReorderCallback =
+ (ReorderableList rl) => {
- for (int i = 0; i < instancePorts.Count(); i++) {
- GUILayout.BeginHorizontal();
- // 'Remove' button
- if (GUILayout.Button("-", GUILayout.Width(20))) {
+ // Move up
+ if (rl.index > reorderableListIndex) {
+ for (int i = reorderableListIndex; i < rl.index; ++i) {
+ XNode.NodePort port = node.GetPort(fieldName + " " + i);
+ XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i + 1));
+ port.SwapConnections(nextPort);
+
+ // Swap cached positions to mitigate twitching
+ Rect rect = NodeEditorWindow.current.portConnectionPoints[port];
+ NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort];
+ NodeEditorWindow.current.portConnectionPoints[nextPort] = rect;
+ }
+ }
+ // Move down
+ else {
+ for (int i = reorderableListIndex; i > rl.index; --i) {
+ XNode.NodePort port = node.GetPort(fieldName + " " + i);
+ XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i - 1));
+ port.SwapConnections(nextPort);
+
+ // Swap cached positions to mitigate twitching
+ Rect rect = NodeEditorWindow.current.portConnectionPoints[port];
+ NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort];
+ NodeEditorWindow.current.portConnectionPoints[nextPort] = rect;
+ }
+ }
+ // Apply changes
+ serializedObject.ApplyModifiedProperties();
+ serializedObject.Update();
+
+ // Move array data if there is any
+ if (hasArrayData) {
+ arrayData.MoveArrayElement(reorderableListIndex, rl.index);
+ }
+
+ // Apply changes
+ serializedObject.ApplyModifiedProperties();
+ serializedObject.Update();
+ NodeEditorWindow.current.Repaint();
+ EditorApplication.delayCall += NodeEditorWindow.current.Repaint;
+ };
+ list.onAddCallback =
+ (ReorderableList rl) => {
+ // Add instance port postfixed with an index number
+ string newName = fieldName + " 0";
+ int i = 0;
+ while (node.HasPort(newName)) newName = fieldName + " " + (++i);
+
+ if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, XNode.Node.TypeConstraint.None, newName);
+ else node.AddInstanceInput(type, connectionType, typeConstraint, newName);
+ serializedObject.Update();
+ EditorUtility.SetDirty(node);
+ if (hasArrayData) {
+ arrayData.InsertArrayElementAtIndex(arrayData.arraySize);
+ }
+ serializedObject.ApplyModifiedProperties();
+ };
+ list.onRemoveCallback =
+ (ReorderableList rl) => {
+
+ var indexedPorts = node.InstancePorts.Select(x => {
+ string[] split = x.fieldName.Split(' ');
+ if (split != null && split.Length == 2 && split[0] == fieldName) {
+ int i = -1;
+ if (int.TryParse(split[1], out i)) {
+ return new { index = i, port = x };
+ }
+ }
+ return new { index = -1, port = (XNode.NodePort) null };
+ });
+ instancePorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
+
+ int index = rl.index;
// Clear the removed ports connections
- instancePorts[i].ClearConnections();
+ instancePorts[index].ClearConnections();
// Move following connections one step up to replace the missing connection
- for (int k = i + 1; k < instancePorts.Count(); k++) {
+ for (int k = index + 1; k < instancePorts.Count(); k++) {
for (int j = 0; j < instancePorts[k].ConnectionCount; j++) {
XNode.NodePort other = instancePorts[k].GetConnection(j);
instancePorts[k].Disconnect(other);
@@ -236,54 +432,39 @@ namespace XNodeEditor {
serializedObject.Update();
EditorUtility.SetDirty(node);
if (hasArrayData) {
- arrayData.DeleteArrayElementAtIndex(i);
- arraySize--;
+ arrayData.DeleteArrayElementAtIndex(index);
// Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues
- if (instancePorts.Count <= arraySize) {
- while (instancePorts.Count <= arraySize) {
- arrayData.DeleteArrayElementAtIndex(--arraySize);
+ if (instancePorts.Count <= arrayData.arraySize) {
+ while (instancePorts.Count <= arrayData.arraySize) {
+ arrayData.DeleteArrayElementAtIndex(arrayData.arraySize - 1);
}
- Debug.LogWarning("Array size exceeded instance ports size. Excess items removed.");
+ UnityEngine.Debug.LogWarning("Array size exceeded instance ports size. Excess items removed.");
}
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
}
- i--;
- GUILayout.EndHorizontal();
- } else {
- if (hasArrayData) {
- if (i < arraySize) {
- SerializedProperty itemData = arrayData.GetArrayElementAtIndex(i);
- if (itemData != null) EditorGUILayout.PropertyField(itemData, new GUIContent(ObjectNames.NicifyVariableName(fieldName) + " " + i), true);
- else EditorGUILayout.LabelField("[Missing array data]");
- } else EditorGUILayout.LabelField("[Out of bounds]");
+ };
- } else {
- EditorGUILayout.LabelField(instancePorts[i].fieldName);
- }
-
- GUILayout.EndHorizontal();
- NodeEditorGUILayout.AddPortField(node.GetPort(instancePorts[i].fieldName));
+ if (hasArrayData) {
+ int instancePortCount = instancePorts.Count;
+ while (instancePortCount < arrayData.arraySize) {
+ // Add instance port postfixed with an index number
+ string newName = arrayData.name + " 0";
+ int i = 0;
+ while (node.HasPort(newName)) newName = arrayData.name + " " + (++i);
+ if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, typeConstraint, newName);
+ else node.AddInstanceInput(type, connectionType, typeConstraint, newName);
+ EditorUtility.SetDirty(node);
+ instancePortCount++;
+ }
+ while (arrayData.arraySize < instancePortCount) {
+ arrayData.InsertArrayElementAtIndex(arrayData.arraySize);
}
- // GUILayout.EndHorizontal();
- }
- GUILayout.BeginHorizontal();
- GUILayout.FlexibleSpace();
- // 'Add' button
- if (GUILayout.Button("+", GUILayout.Width(20))) {
-
- string newName = fieldName + " 0";
- int i = 0;
- while (node.HasPort(newName)) newName = fieldName + " " + (++i);
-
- if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, newName);
- else node.AddInstanceInput(type, connectionType, newName);
- serializedObject.Update();
- EditorUtility.SetDirty(node);
- if (hasArrayData) arrayData.InsertArrayElementAtIndex(arraySize);
serializedObject.ApplyModifiedProperties();
+ serializedObject.Update();
}
- GUILayout.EndHorizontal();
+ if (onCreation != null) onCreation(list);
+ return list;
}
}
}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs
index 210a619..bd2cb55 100644
--- a/Scripts/Editor/NodeEditorPreferences.cs
+++ b/Scripts/Editor/NodeEditorPreferences.cs
@@ -12,7 +12,7 @@ namespace XNodeEditor {
/// The last key we checked. This should be the one we modify
private static string lastKey = "xNode.Settings";
- private static Dictionary typeColors = new Dictionary();
+ private static Dictionary typeColors = new Dictionary();
private static Dictionary settings = new Dictionary();
[System.Serializable]
@@ -23,9 +23,11 @@ namespace XNodeEditor {
[SerializeField] private Color32 _gridBgColor = new Color(0.18f, 0.18f, 0.18f);
public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } }
+ public float zoomOutLimit = 5f;
public Color32 highlightColor = new Color32(255, 255, 255, 255);
public bool gridSnap = true;
public bool autoSave = true;
+ public bool zoomToMouse = true;
[SerializeField] private string typeColorsData = "";
[NonSerialized] public Dictionary typeColors = new Dictionary();
public NoodleType noodleType = NoodleType.Curve;
@@ -80,7 +82,20 @@ namespace XNodeEditor {
return settings[lastKey];
}
+#if UNITY_2019_1_OR_NEWER
+ [SettingsProvider]
+ public static SettingsProvider CreateXNodeSettingsProvider() {
+ SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) {
+ guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); },
+ keywords = new HashSet(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" })
+ };
+ return provider;
+ }
+#endif
+
+#if !UNITY_2019_1_OR_NEWER
[PreferenceItem("Node Editor")]
+#endif
private static void PreferencesGUI() {
VerifyLoaded();
Settings settings = NodeEditorPreferences.settings[lastKey];
@@ -98,7 +113,8 @@ namespace XNodeEditor {
//Label
EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel);
settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap);
-
+ settings.zoomToMouse = EditorGUILayout.Toggle(new GUIContent("Zoom to Mouse", "Zooms towards mouse position"), settings.zoomToMouse);
+ settings.zoomOutLimit = EditorGUILayout.FloatField(new GUIContent("Zoom out Limit", "Upper limit to zoom"), settings.zoomOutLimit);
settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor);
settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor);
if (GUI.changed) {
@@ -134,15 +150,16 @@ namespace XNodeEditor {
EditorGUILayout.LabelField("Types", EditorStyles.boldLabel);
//Display type colors. Save them if they are edited by the user
- List typeColorKeys = new List(typeColors.Keys);
- foreach (string typeColorKey in typeColorKeys) {
- Color col = typeColors[typeColorKey];
+ foreach (var typeColor in typeColors) {
+ Type type = typeColor.Key;
+ string typeColorKey = NodeEditorUtilities.PrettyName(type);
+ Color col = typeColor.Value;
EditorGUI.BeginChangeCheck();
EditorGUILayout.BeginHorizontal();
col = EditorGUILayout.ColorField(typeColorKey, col);
EditorGUILayout.EndHorizontal();
if (EditorGUI.EndChangeCheck()) {
- typeColors[typeColorKey] = col;
+ typeColors[type] = col;
if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col;
else settings.typeColors.Add(typeColorKey, col);
SavePrefs(typeColorKey, settings);
@@ -165,7 +182,7 @@ namespace XNodeEditor {
public static void ResetPrefs() {
if (EditorPrefs.HasKey(lastKey)) EditorPrefs.DeleteKey(lastKey);
if (settings.ContainsKey(lastKey)) settings.Remove(lastKey);
- typeColors = new Dictionary();
+ typeColors = new Dictionary();
VerifyLoaded();
NodeEditorWindow.RepaintAll();
}
@@ -184,19 +201,21 @@ namespace XNodeEditor {
public static Color GetTypeColor(System.Type type) {
VerifyLoaded();
if (type == null) return Color.gray;
- string typeName = type.PrettyName();
- if (!typeColors.ContainsKey(typeName)) {
- if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(typeName, settings[lastKey].typeColors[typeName]);
+ Color col;
+ if (!typeColors.TryGetValue(type, out col)) {
+ string typeName = type.PrettyName();
+ if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]);
else {
#if UNITY_5_4_OR_NEWER
UnityEngine.Random.InitState(typeName.GetHashCode());
#else
UnityEngine.Random.seed = typeName.GetHashCode();
#endif
- typeColors.Add(typeName, new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value));
+ col = new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value);
+ typeColors.Add(type, col);
}
}
- return typeColors[typeName];
+ return col;
}
}
}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditorReflection.cs b/Scripts/Editor/NodeEditorReflection.cs
index 7eaec2b..18b72fa 100644
--- a/Scripts/Editor/NodeEditorReflection.cs
+++ b/Scripts/Editor/NodeEditorReflection.cs
@@ -22,6 +22,18 @@ namespace XNodeEditor {
[NonSerialized] private static Type[] _nodeTypes = null;
+ private Func isDocked {
+ get {
+ if (_isDocked == null) {
+ BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
+ MethodInfo isDockedMethod = typeof(NodeEditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true);
+ _isDocked = (Func) Delegate.CreateDelegate(typeof(Func), this, isDockedMethod);
+ }
+ return _isDocked;
+ }
+ }
+ private Func _isDocked;
+
public static Type[] GetNodeTypes() {
//Get all classes deriving from Node via reflection
return GetDerivedTypes(typeof(XNode.Node));
@@ -30,9 +42,9 @@ namespace XNodeEditor {
public static Dictionary GetNodeTint() {
Dictionary tints = new Dictionary();
for (int i = 0; i < nodeTypes.Length; i++) {
- var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTint), true);
+ var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTintAttribute), true);
if (attribs == null || attribs.Length == 0) continue;
- XNode.Node.NodeTint attrib = attribs[0] as XNode.Node.NodeTint;
+ XNode.Node.NodeTintAttribute attrib = attribs[0] as XNode.Node.NodeTintAttribute;
tints.Add(nodeTypes[i], attrib.color);
}
return tints;
@@ -41,32 +53,44 @@ namespace XNodeEditor {
public static Dictionary GetNodeWidth() {
Dictionary widths = new Dictionary();
for (int i = 0; i < nodeTypes.Length; i++) {
- var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidth), true);
+ var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidthAttribute), true);
if (attribs == null || attribs.Length == 0) continue;
- XNode.Node.NodeWidth attrib = attribs[0] as XNode.Node.NodeWidth;
+ XNode.Node.NodeWidthAttribute attrib = attribs[0] as XNode.Node.NodeWidthAttribute;
widths.Add(nodeTypes[i], attrib.width);
}
return widths;
}
+ /// Get FieldInfo of a field, including those that are private and/or inherited
+ public static FieldInfo GetFieldInfo(Type type, string fieldName) {
+ // If we can't find field in the first run, it's probably a private field in a base class.
+ FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ // Search base classes for private fields only. Public fields are found above
+ while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
+ return field;
+ }
+
/// Get all classes deriving from baseType via reflection
public static Type[] GetDerivedTypes(Type baseType) {
List types = new List();
System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies) {
- types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
+ try {
+ types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
+ } catch(ReflectionTypeLoadException) {}
}
return types.ToArray();
}
- public static object ObjectFromType(Type type) {
- return Activator.CreateInstance(type);
- }
-
- public static object ObjectFromFieldName(object obj, string fieldName) {
- Type type = obj.GetType();
- FieldInfo fieldInfo = type.GetField(fieldName);
- return fieldInfo.GetValue(obj);
+ public static void AddCustomContextMenuItems(GenericMenu contextMenu, object obj) {
+ KeyValuePair[] items = GetContextMenuMethods(obj);
+ if (items.Length != 0) {
+ contextMenu.AddSeparator("");
+ for (int i = 0; i < items.Length; i++) {
+ KeyValuePair kvp = items[i];
+ contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null));
+ }
+ }
}
public static KeyValuePair[] GetContextMenuMethods(object obj) {
@@ -99,6 +123,9 @@ namespace XNodeEditor {
/// Very crude. Uses a lot of reflection.
public static void OpenPreferences() {
try {
+#if UNITY_2018_3_OR_NEWER
+ SettingsService.OpenUserPreferences("Preferences/Node Editor");
+#else
//Open preferences window
Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow));
Type type = assembly.GetType("UnityEditor.PreferencesWindow");
@@ -130,10 +157,11 @@ namespace XNodeEditor {
return;
}
}
+#endif
} catch (Exception e) {
Debug.LogError(e);
Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number.");
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs
index 0d98420..18e295f 100644
--- a/Scripts/Editor/NodeEditorUtilities.cs
+++ b/Scripts/Editor/NodeEditorUtilities.cs
@@ -15,6 +15,9 @@ namespace XNodeEditor {
/// C#'s Script Icon [The one MonoBhevaiour Scripts have].
private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D);
+ /// Saves Attribute from Type+Field for faster lookup. Resets on recompiles.
+ private static Dictionary>> typeAttributes = new Dictionary>>();
+
public static bool GetAttrib(Type classType, out T attribOut) where T : Attribute {
object[] attribs = classType.GetCustomAttributes(typeof(T), false);
return GetAttrib(attribs, out attribOut);
@@ -22,7 +25,7 @@ namespace XNodeEditor {
public static bool GetAttrib(object[] attribs, out T attribOut) where T : Attribute {
for (int i = 0; i < attribs.Length; i++) {
- if (attribs[i].GetType() == typeof(T)) {
+ if (attribs[i] is T){
attribOut = attribs[i] as T;
return true;
}
@@ -32,7 +35,15 @@ namespace XNodeEditor {
}
public static bool GetAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute {
- object[] attribs = classType.GetField(fieldName).GetCustomAttributes(typeof(T), false);
+ // If we can't find field in the first run, it's probably a private field in a base class.
+ FieldInfo field = NodeEditorWindow.GetFieldInfo(classType, fieldName);
+ // This shouldn't happen. Ever.
+ if (field == null) {
+ Debug.LogWarning("Field " + fieldName + " couldnt be found");
+ attribOut = null;
+ return false;
+ }
+ object[] attribs = field.GetCustomAttributes(typeof(T), true);
return GetAttrib(attribs, out attribOut);
}
@@ -45,6 +56,34 @@ namespace XNodeEditor {
return false;
}
+ public static bool GetCachedAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute {
+ Dictionary> typeFields;
+ if (!typeAttributes.TryGetValue(classType, out typeFields)) {
+ typeFields = new Dictionary>();
+ typeAttributes.Add(classType, typeFields);
+ }
+
+ Dictionary typeTypes;
+ if (!typeFields.TryGetValue(fieldName, out typeTypes)) {
+ typeTypes = new Dictionary();
+ typeFields.Add(fieldName, typeTypes);
+ }
+
+ Attribute attr;
+ if (!typeTypes.TryGetValue(typeof(T), out attr)) {
+ if (GetAttrib(classType, fieldName, out attribOut)) typeTypes.Add(typeof(T), attribOut);
+ else typeTypes.Add(typeof(T), null);
+ }
+
+ if (attr == null) {
+ attribOut = null;
+ return false;
+ }
+
+ attribOut = attr as T;
+ return true;
+ }
+
/// Returns true if this can be casted to
public static bool IsCastableTo(this Type from, Type to) {
if (to.IsAssignableFrom(from)) return true;
diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs
index 72fd4ce..a575c8d 100644
--- a/Scripts/Editor/NodeEditorWindow.cs
+++ b/Scripts/Editor/NodeEditorWindow.cs
@@ -61,15 +61,38 @@ namespace XNodeEditor {
public XNode.NodeGraph graph;
public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } }
private Vector2 _panOffset;
- public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, 1f, 5f); Repaint(); } }
+ public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, 1f, NodeEditorPreferences.GetSettings().zoomOutLimit); Repaint(); } }
private float _zoom = 1;
void OnFocus() {
current = this;
- graphEditor = NodeGraphEditor.GetEditor(graph);
+ ValidateGraphEditor();
if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
+ [InitializeOnLoadMethod]
+ private static void OnLoad() {
+ Selection.selectionChanged -= OnSelectionChanged;
+ Selection.selectionChanged += OnSelectionChanged;
+ }
+
+ /// Handle Selection Change events
+ private static void OnSelectionChanged() {
+ XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph;
+ if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) {
+ Open(nodeGraph);
+ }
+ }
+
+ /// Make sure the graph editor is assigned and to the right object
+ private void ValidateGraphEditor() {
+ NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this);
+ if (this.graphEditor != graphEditor) {
+ this.graphEditor = graphEditor;
+ graphEditor.OnOpen();
+ }
+ }
+
/// Create editor window
public static NodeEditorWindow Init() {
NodeEditorWindow w = CreateInstance();
@@ -123,8 +146,9 @@ namespace XNodeEditor {
public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) {
Vector2 center = position.size * 0.5f;
- float xOffset = (center.x * zoom + (panOffset.x + gridPosition.x));
- float yOffset = (center.y * zoom + (panOffset.y + gridPosition.y));
+ // UI Sharpness complete fix - Round final offset not panOffset
+ float xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x));
+ float yOffset = Mathf.Round(center.y * zoom + (panOffset.y + gridPosition.y));
return new Vector2(xOffset, yOffset);
}
@@ -146,14 +170,21 @@ namespace XNodeEditor {
public static bool OnOpen(int instanceID, int line) {
XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph;
if (nodeGraph != null) {
- NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
- w.wantsMouseMove = true;
- w.graph = nodeGraph;
+ Open(nodeGraph);
return true;
}
return false;
}
+ /// Open the provided graph in the NodeEditor
+ public static void Open(XNode.NodeGraph graph) {
+ if (!graph) return;
+
+ NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
+ w.wantsMouseMove = true;
+ w.graph = graph;
+ }
+
/// Repaint all open NodeEditorWindows.
public static void RepaintAll() {
NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll();
@@ -162,4 +193,4 @@ namespace XNodeEditor {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs
index bcb95e1..8a9d2f0 100644
--- a/Scripts/Editor/NodeGraphEditor.cs
+++ b/Scripts/Editor/NodeGraphEditor.cs
@@ -8,13 +8,16 @@ namespace XNodeEditor {
/// Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor.
[CustomNodeGraphEditor(typeof(XNode.NodeGraph))]
public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase {
- /// The position of the window in screen space.
- public Rect position;
+ [Obsolete("Use window.position instead")]
+ public Rect position { get { return window.position; } set { window.position = value; } }
/// Are we currently renaming a node?
protected bool isRenaming;
public virtual void OnGUI() { }
+ /// Called when opened by NodeEditorWindow
+ public virtual void OnOpen() { }
+
public virtual Texture2D GetGridTexture() {
return NodeEditorPreferences.GetSettings().gridTexture;
}
@@ -38,10 +41,43 @@ namespace XNodeEditor {
return ObjectNames.NicifyVariableName(type.ToString().Replace('.', '/'));
}
+ /// Add items for the context menu when right-clicking this node. Override to add custom menu items.
+ public virtual void AddContextMenuItems(GenericMenu menu) {
+ Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition);
+ for (int i = 0; i < NodeEditorWindow.nodeTypes.Length; i++) {
+ Type type = NodeEditorWindow.nodeTypes[i];
+
+ //Get node context menu path
+ string path = GetNodeMenuName(type);
+ if (string.IsNullOrEmpty(path)) continue;
+
+ menu.AddItem(new GUIContent(path), false, () => {
+ CreateNode(type, pos);
+ });
+ }
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorWindow.OpenPreferences());
+ NodeEditorWindow.AddCustomContextMenuItems(menu, target);
+ }
+
+ public virtual Color GetPortColor(XNode.NodePort port) {
+ return GetTypeColor(port.ValueType);
+ }
+
public virtual Color GetTypeColor(Type type) {
return NodeEditorPreferences.GetTypeColor(type);
}
+ /// Create a node and save it in the graph asset
+ public virtual void CreateNode(Type type, Vector2 position) {
+ XNode.Node node = target.AddNode(type);
+ node.position = position;
+ if (string.IsNullOrEmpty(node.name)) node.name = UnityEditor.ObjectNames.NicifyVariableName(type.Name);
+ AssetDatabase.AddObjectToAsset(node, target);
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ NodeEditorWindow.RepaintAll();
+ }
+
/// Creates a copy of the original node in the graph
public XNode.Node CopyNode(XNode.Node original) {
XNode.Node node = target.CopyNode(original);
@@ -65,7 +101,7 @@ namespace XNodeEditor {
public string editorPrefsKey;
/// Tells a NodeGraphEditor which Graph type it is an editor for
/// Type that this editor can edit
- /// Define unique key for unique layout settings instance
+ /// Define unique key for unique layout settings instance
public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") {
this.inspectedType = inspectedType;
this.editorPrefsKey = editorPrefsKey;
diff --git a/Scripts/Editor/RenamePopup.cs b/Scripts/Editor/RenamePopup.cs
new file mode 100644
index 0000000..a49a948
--- /dev/null
+++ b/Scripts/Editor/RenamePopup.cs
@@ -0,0 +1,66 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace XNodeEditor {
+ /// Utility for renaming assets
+ public class RenamePopup : EditorWindow {
+ public static RenamePopup current { get; private set; }
+ public Object target;
+ public string input;
+
+ private bool firstFrame = true;
+
+ /// Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply.
+ public static RenamePopup Show(Object target, float width = 200) {
+ RenamePopup window = EditorWindow.GetWindow(true, "Rename " + target.name, true);
+ if (current != null) current.Close();
+ current = window;
+ window.target = target;
+ window.input = target.name;
+ window.minSize = new Vector2(100, 44);
+ window.position = new Rect(0, 0, width, 44);
+ GUI.FocusControl("ClearAllFocus");
+ window.UpdatePositionToMouse();
+ return window;
+ }
+
+ private void UpdatePositionToMouse() {
+ if (Event.current == null) return;
+ Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
+ Rect pos = position;
+ pos.x = mousePoint.x - position.width * 0.5f;
+ pos.y = mousePoint.y - 10;
+ position = pos;
+ }
+
+ private void OnLostFocus() {
+ // Make the popup close on lose focus
+ Close();
+ }
+
+ private void OnGUI() {
+ if (firstFrame) {
+ UpdatePositionToMouse();
+ firstFrame = false;
+ }
+ input = EditorGUILayout.TextField(input);
+ Event e = Event.current;
+ // If input is empty, revert name to default instead
+ if (input == null || input.Trim() == "") {
+ if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) {
+ target.name = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name);
+ AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
+ Close();
+ }
+ }
+ // Rename asset to input text
+ else {
+ if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) {
+ target.name = input;
+ AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
+ Close();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/RenamePopup.cs.meta b/Scripts/Editor/RenamePopup.cs.meta
new file mode 100644
index 0000000..5c40a02
--- /dev/null
+++ b/Scripts/Editor/RenamePopup.cs.meta
@@ -0,0 +1,13 @@
+fileFormatVersion: 2
+guid: 4ef3ddc25518318469bce838980c64be
+timeCreated: 1552067957
+licenseType: Free
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Node.cs b/Scripts/Node.cs
index 08d91a3..4710f48 100644
--- a/Scripts/Node.cs
+++ b/Scripts/Node.cs
@@ -41,6 +41,16 @@ namespace XNode {
Override,
}
+ /// Tells which types of input to allow
+ public enum TypeConstraint {
+ /// Allow all types of input
+ None,
+ /// Allow similar and inherited types
+ Inherited,
+ /// Allow only similar types
+ Strict,
+ }
+
/// Iterate over all ports on this node.
public IEnumerable Ports { get { foreach (NodePort port in ports.Values) yield return port; } }
/// Iterate over all outputs on this node.
@@ -60,7 +70,12 @@ namespace XNode {
/// It is recommended not to modify these at hand. Instead, see and
[SerializeField] private NodePortDictionary ports = new NodePortDictionary();
+ /// Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable
+ public static NodeGraph graphHotfix;
+
protected void OnEnable() {
+ if (graphHotfix != null) graph = graphHotfix;
+ graphHotfix = null;
UpdateStaticPorts();
Init();
}
@@ -70,7 +85,7 @@ namespace XNode {
NodeDataCache.UpdatePorts(this, ports);
}
- /// Initialize node. Called on creation.
+ /// Initialize node. Called on enable.
protected virtual void Init() { }
/// Checks all connections for invalid references, and removes them.
@@ -79,27 +94,24 @@ namespace XNode {
}
#region Instance Ports
- /// Convenience function.
- ///
+ /// Convenience function.
///
///
- public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) {
- return AddInstancePort(type, NodePort.IO.Input, connectionType, fieldName);
+ public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
+ return AddInstancePort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName);
}
- /// Convenience function.
- ///
+ /// Convenience function.
///
///
- public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) {
- return AddInstancePort(type, NodePort.IO.Output, connectionType, fieldName);
+ public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
+ return AddInstancePort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName);
}
- /// Add a dynamic, serialized port to this node.
- ///
+ /// Add a dynamic, serialized port to this node.
///
///
- private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) {
+ private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
if (fieldName == null) {
fieldName = "instanceInput_0";
int i = 0;
@@ -108,13 +120,15 @@ namespace XNode {
Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this);
return ports[fieldName];
}
- NodePort port = new NodePort(fieldName, type, direction, connectionType, this);
+ NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this);
ports.Add(fieldName, port);
return port;
}
/// Remove an instance port from the node
public void RemoveInstancePort(string fieldName) {
+ NodePort instancePort = GetPort(fieldName);
+ if (instancePort == null) throw new ArgumentException("port " + fieldName + " doesn't exist");
RemoveInstancePort(GetPort(fieldName));
}
@@ -198,22 +212,25 @@ namespace XNode {
foreach (NodePort port in Ports) port.ClearConnections();
}
- public override int GetHashCode() {
- return JsonUtility.ToJson(this).GetHashCode();
- }
-
+#region Attributes
/// Mark a serializable field as an input port. You can access this through
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public class InputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
+ public bool instancePortList;
+ public TypeConstraint typeConstraint;
/// Mark a serializable field as an input port. You can access this through
/// Should we display the backing value for this port as an editor field?
/// Should we allow multiple connections?
- public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple) {
+ /// Constrains which input connections can be made to this port
+ /// If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays
+ public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool instancePortList = false) {
this.backingValue = backingValue;
this.connectionType = connectionType;
+ this.instancePortList = instancePortList;
+ this.typeConstraint = typeConstraint;
}
}
@@ -222,13 +239,16 @@ namespace XNode {
public class OutputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
+ public bool instancePortList;
/// Mark a serializable field as an output port. You can access this through
/// Should we display the backing value for this port as an editor field?
/// Should we allow multiple connections?
- public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple) {
+ /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays
+ public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool instancePortList = false) {
this.backingValue = backingValue;
this.connectionType = connectionType;
+ this.instancePortList = instancePortList;
}
}
@@ -243,19 +263,19 @@ namespace XNode {
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
- public class NodeTint : Attribute {
+ public class NodeTintAttribute : Attribute {
public Color color;
/// Specify a color for this node type
/// Red [0.0f .. 1.0f]
/// Green [0.0f .. 1.0f]
/// Blue [0.0f .. 1.0f]
- public NodeTint(float r, float g, float b) {
+ public NodeTintAttribute(float r, float g, float b) {
color = new Color(r, g, b);
}
/// Specify a color for this node type
/// HEX color value
- public NodeTint(string hex) {
+ public NodeTintAttribute(string hex) {
ColorUtility.TryParseHtmlString(hex, out color);
}
@@ -263,20 +283,21 @@ namespace XNode {
/// Red [0 .. 255]
/// Green [0 .. 255]
/// Blue [0 .. 255]
- public NodeTint(byte r, byte g, byte b) {
+ public NodeTintAttribute(byte r, byte g, byte b) {
color = new Color32(r, g, b, byte.MaxValue);
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
- public class NodeWidth : Attribute {
+ public class NodeWidthAttribute : Attribute {
public int width;
/// Specify a width for this node type
/// Width
- public NodeWidth(int width) {
+ public NodeWidthAttribute(int width) {
this.width = width;
}
}
+#endregion
[Serializable] private class NodePortDictionary : Dictionary, ISerializationCallbackReceiver {
[SerializeField] private List keys = new List();
@@ -302,4 +323,4 @@ namespace XNode {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs
index 283cc9d..434ffc5 100644
--- a/Scripts/NodeDataCache.cs
+++ b/Scripts/NodeDataCache.cs
@@ -30,7 +30,7 @@ namespace XNode {
NodePort staticPort;
if (staticPorts.TryGetValue(port.fieldName, out staticPort)) {
// If port exists but with wrong settings, remove it. Re-add it later.
- if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction) ports.Remove(port.fieldName);
+ if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction || port.typeConstraint != staticPort.typeConstraint) ports.Remove(port.fieldName);
else port.ValueType = staticPort.ValueType;
}
// If port doesn't exist anymore, remove it
@@ -68,12 +68,24 @@ namespace XNode {
}
}
+ public static List GetNodeFields(System.Type nodeType) {
+ List fieldInfo = new List(nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance));
+
+ // GetFields doesnt return inherited private fields, so walk through base types and pick those up
+ System.Type tempType = nodeType;
+ while ((tempType = tempType.BaseType) != typeof(XNode.Node)) {
+ fieldInfo.AddRange(tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance));
+ }
+ return fieldInfo;
+ }
+
private static void CachePorts(System.Type nodeType) {
- System.Reflection.FieldInfo[] fieldInfo = nodeType.GetFields();
- for (int i = 0; i < fieldInfo.Length; i++) {
+ List fieldInfo = GetNodeFields(nodeType);
+
+ for (int i = 0; i < fieldInfo.Count; i++) {
//Get InputAttribute and OutputAttribute
- object[] attribs = fieldInfo[i].GetCustomAttributes(false);
+ object[] attribs = fieldInfo[i].GetCustomAttributes(true);
Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute;
Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute;
diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs
index d6d766c..6b95fc2 100644
--- a/Scripts/NodeGraph.cs
+++ b/Scripts/NodeGraph.cs
@@ -18,18 +18,20 @@ namespace XNode {
/// Add a node to the graph by type
public virtual Node AddNode(Type type) {
+ Node.graphHotfix = this;
Node node = ScriptableObject.CreateInstance(type) as Node;
- nodes.Add(node);
node.graph = this;
+ nodes.Add(node);
return node;
}
/// Creates a copy of the original node in the graph
public virtual Node CopyNode(Node original) {
+ Node.graphHotfix = this;
Node node = ScriptableObject.Instantiate(original);
+ node.graph = this;
node.ClearConnections();
nodes.Add(node);
- node.graph = this;
return node;
}
@@ -58,6 +60,7 @@ namespace XNode {
// Instantiate all nodes inside the graph
for (int i = 0; i < nodes.Count; i++) {
if (nodes[i] == null) continue;
+ Node.graphHotfix = graph;
Node node = Instantiate(nodes[i]) as Node;
node.graph = graph;
graph.nodes[i] = node;
diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs
index 9ac920a..2da73ae 100644
--- a/Scripts/NodePort.cs
+++ b/Scripts/NodePort.cs
@@ -21,6 +21,7 @@ namespace XNode {
public IO direction { get { return _direction; } }
public Node.ConnectionType connectionType { get { return _connectionType; } }
+ public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } }
/// Is this port connected to anytihng?
public bool IsConnected { get { return connections.Count != 0; } }
@@ -50,6 +51,7 @@ namespace XNode {
[SerializeField] private List connections = new List();
[SerializeField] private IO _direction;
[SerializeField] private Node.ConnectionType _connectionType;
+ [SerializeField] private Node.TypeConstraint _typeConstraint;
[SerializeField] private bool _dynamic;
/// Construct a static targetless nodeport. Used as a template.
@@ -62,6 +64,7 @@ namespace XNode {
if (attribs[i] is Node.InputAttribute) {
_direction = IO.Input;
_connectionType = (attribs[i] as Node.InputAttribute).connectionType;
+ _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint;
} else if (attribs[i] is Node.OutputAttribute) {
_direction = IO.Output;
_connectionType = (attribs[i] as Node.OutputAttribute).connectionType;
@@ -76,17 +79,19 @@ namespace XNode {
_direction = nodePort.direction;
_dynamic = nodePort._dynamic;
_connectionType = nodePort._connectionType;
+ _typeConstraint = nodePort._typeConstraint;
_node = node;
}
/// Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports.
- public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node node) {
+ public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) {
_fieldName = fieldName;
this.ValueType = type;
_direction = direction;
_node = node;
_dynamic = true;
_connectionType = connectionType;
+ _typeConstraint = typeConstraint;
}
/// Initialize the delegate for this NodePort.
@@ -160,6 +165,15 @@ namespace XNode {
port.node.OnCreateConnection(this, port);
}
+ public List GetConnections() {
+ List result = new List();
+ for (int i = 0; i < connections.Count; i++) {
+ NodePort port = GetConnection(i);
+ if (port != null) result.Add(port);
+ }
+ return result;
+ }
+
public NodePort GetConnection(int i) {
//If the connection is broken for some reason, remove it.
if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) {
@@ -189,6 +203,23 @@ namespace XNode {
return false;
}
+ /// Returns true if this port can connect to specified port
+ public bool CanConnectTo(NodePort port) {
+ // Figure out which is input and which is output
+ NodePort input = null, output = null;
+ if (IsInput) input = this;
+ else output = this;
+ if (port.IsInput) input = port;
+ else output = port;
+ // If there isn't one of each, they can't connect
+ if (input == null || output == null) return false;
+ // Check type constraints
+ if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false;
+ if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false;
+ // Success
+ return true;
+ }
+
/// Disconnect this port from another port
public void Disconnect(NodePort port) {
// Remove this ports connection to the other
@@ -210,6 +241,25 @@ namespace XNode {
if (port != null) port.node.OnRemoveConnection(port);
}
+ /// Disconnect this port from another port
+ public void Disconnect(int i) {
+ // Remove the other ports connection to this port
+ NodePort otherPort = connections[i].Port;
+ if (otherPort != null) {
+ for (int k = 0; k < otherPort.connections.Count; k++) {
+ if (otherPort.connections[k].Port == this) {
+ otherPort.connections.RemoveAt(i);
+ }
+ }
+ }
+ // Remove this ports connection to the other
+ connections.RemoveAt(i);
+
+ // Trigger OnRemoveConnection
+ node.OnRemoveConnection(this);
+ if (otherPort != null) otherPort.node.OnRemoveConnection(otherPort);
+ }
+
public void ClearConnections() {
while (connections.Count > 0) {
Disconnect(connections[0].Port);
@@ -221,6 +271,58 @@ namespace XNode {
return connections[index].reroutePoints;
}
+ /// Swap connections with another node
+ public void SwapConnections(NodePort targetPort) {
+ int aConnectionCount = connections.Count;
+ int bConnectionCount = targetPort.connections.Count;
+
+ List portConnections = new List();
+ List targetPortConnections = new List();
+
+ // Cache port connections
+ for (int i = 0; i < aConnectionCount; i++)
+ portConnections.Add(connections[i].Port);
+
+ // Cache target port connections
+ for (int i = 0; i < bConnectionCount; i++)
+ targetPortConnections.Add(targetPort.connections[i].Port);
+
+ ClearConnections();
+ targetPort.ClearConnections();
+
+ // Add port connections to targetPort
+ for (int i = 0; i < portConnections.Count; i++)
+ targetPort.Connect(portConnections[i]);
+
+ // Add target port connections to this one
+ for (int i = 0; i < targetPortConnections.Count; i++)
+ Connect(targetPortConnections[i]);
+
+ }
+
+ /// Copy all connections pointing to a node and add them to this one
+ public void AddConnections(NodePort targetPort) {
+ int connectionCount = targetPort.ConnectionCount;
+ for (int i = 0; i < connectionCount; i++) {
+ PortConnection connection = targetPort.connections[i];
+ NodePort otherPort = connection.Port;
+ Connect(otherPort);
+ }
+ }
+
+ /// Move all connections pointing to this node, to another node
+ public void MoveConnections(NodePort targetPort) {
+ int connectionCount = connections.Count;
+
+ // Add connections to target port
+ for (int i = 0; i < connectionCount; i++) {
+ PortConnection connection = targetPort.connections[i];
+ NodePort otherPort = connection.Port;
+ Connect(otherPort);
+ }
+ ClearConnections();
+ }
+
/// Swap connected nodes from the old list with nodes from the new list
public void Redirect(List oldNodes, List newNodes) {
foreach (PortConnection connection in connections) {