diff --git a/Editor/Node_Editor/NodeEditorWindow.cs b/Editor/Node_Editor/NodeEditorWindow.cs index c45a8ad1..e02cb31f 100644 --- a/Editor/Node_Editor/NodeEditorWindow.cs +++ b/Editor/Node_Editor/NodeEditorWindow.cs @@ -5,8 +5,8 @@ using UnityEngine; using UnityEditor; -using NodeEditorFramework; using NodeEditorFramework.Utilities; +using NodeEditorFramework.IO; using GenericMenu = UnityEditor.GenericMenu; @@ -30,14 +30,23 @@ public class NodeEditorWindow : EditorWindow private int sideWindowWidth = 400; private int toolbarHeight = 17; - private Rect modalWindowRect = new Rect(20, 50, 250, 100); - + // Modal Panel private bool showSideWindow; private bool showModalPanel; + private Rect modalPanelRect = new Rect(20, 50, 250, 70); + private Action modalPanelContent; public Rect sideWindowRect { get { return new Rect (position.width - sideWindowWidth, toolbarHeight, sideWindowWidth, position.height); } } public Rect canvasWindowRect { get { return new Rect(0, toolbarHeight, position.width - (showSideWindow? sideWindowWidth : 0), position.height - toolbarHeight); } } + // IO Format modal panel + private ImportExportFormat IOFormat; + private object[] IOLocationArgs; + private delegate bool? DefExportLocationGUI(string canvasName, ref object[] locationArgs); + private delegate bool? DefImportLocationGUI(ref object[] locationArgs); + private DefImportLocationGUI ImportLocationGUI; + private DefExportLocationGUI ExportLocationGUI; + #region General /// @@ -135,26 +144,6 @@ private void DrawSceneGUI() SceneView.lastActiveSceneView.Repaint(); } - private void DoModalWindow(int unusedWindowID) - { - GUILayout.BeginHorizontal (); - sceneCanvasName = GUILayout.TextField (sceneCanvasName, GUILayout.ExpandWidth (true)); - bool overwrite = NodeEditorSaveManager.HasSceneSave (sceneCanvasName); - if (overwrite) - GUILayout.Label (new GUIContent ("!!!", "A canvas with the specified name already exists. It will be overwritten!"), GUILayout.ExpandWidth (false)); - GUILayout.EndHorizontal (); - - GUILayout.BeginHorizontal (); - if (GUILayout.Button("Cancel")) - showModalPanel = false; - if (GUILayout.Button (new GUIContent (overwrite? "Overwrite" : "Save", "Save the canvas to the Scene"))) - { - showModalPanel = false; - canvasCache.SaveSceneNodeCanvas (sceneCanvasName); - } - GUILayout.EndHorizontal (); - } - private void OnGUI() { // Initiation @@ -193,6 +182,9 @@ private void OnGUI() // Draw Toolbar DrawToolbarGUI(); + // Draw Modal Panel + DrawModalPanel (); + if (showSideWindow) { // Draw Side Window sideWindowWidth = Math.Min(600, Math.Max(200, (int)(position.width / 5))); @@ -201,13 +193,6 @@ private void OnGUI() GUILayout.EndArea(); } - if (showModalPanel) - { - BeginWindows(); - modalWindowRect = GUILayout.Window(0, modalWindowRect, DoModalWindow, "Save to Scene"); - EndWindows(); - } - NodeEditorGUI.EndNodeGUI(); } @@ -222,7 +207,7 @@ private void DrawToolbarGUI() // Canvas creation NodeCanvasManager.FillCanvasTypeMenu(ref menu, CreateCanvas, "New Canvas/"); - menu.AddSeparator(""); + menu.AddSeparator(""); // Scene Saving menu.AddItem(new GUIContent("Load Canvas", "Loads an asset canvas"), false, LoadCanvas); @@ -232,13 +217,18 @@ private void DrawToolbarGUI() menu.AddItem(new GUIContent("Save Canvas As"), false, SaveCanvasAs); menu.AddSeparator(""); + // Import / Export filled with import/export types + ImportExportManager.FillImportFormatMenu(ref menu, ImportCanvasCallback, "Import/"); + ImportExportManager.FillExportFormatMenu(ref menu, ExportCanvasCallback, "Export/"); + menu.AddSeparator(""); + // Scene Saving foreach (string sceneSave in NodeEditorSaveManager.GetSceneSaves()) { if (sceneSave.ToLower () != "lastsession") - menu.AddItem(new GUIContent("Load Canvas from Scene/" + sceneSave), false, LoadSceneCanvasCallback, sceneSave); + menu.AddItem(new GUIContent("Load Canvas from Scene/" + sceneSave), false, LoadSceneCanvasCallback, sceneSave); } - menu.AddItem( new GUIContent("Save Canvas to Scene"), false, () => showModalPanel = true); + menu.AddItem( new GUIContent("Save Canvas to Scene"), false, SaveSceneCanvasCallback); menu.DropDown (new Rect (5, toolbarHeight, 0, 0)); } @@ -261,12 +251,47 @@ private void DrawToolbarGUI() GUI.backgroundColor = new Color(1, 0.3f, 0.3f, 1); if (GUILayout.Button("Force Re-init", EditorStyles.toolbarButton, GUILayout.Width(80))) - NodeEditor.ReInit (true); + NodeEditor.ReInit(true); GUI.backgroundColor = Color.white; EditorGUILayout.EndHorizontal(); } + private void SaveSceneCanvasPanel() + { + GUILayout.Label("Save Canvas To Scene"); + + GUILayout.BeginHorizontal(); + sceneCanvasName = GUILayout.TextField(sceneCanvasName, GUILayout.ExpandWidth(true)); + bool overwrite = NodeEditorSaveManager.HasSceneSave(sceneCanvasName); + if (overwrite) + GUILayout.Label(new GUIContent("!!!", "A canvas with the specified name already exists. It will be overwritten!"), GUILayout.ExpandWidth(false)); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Cancel")) + showModalPanel = false; + if (GUILayout.Button(new GUIContent(overwrite ? "Overwrite" : "Save", "Save the canvas to the Scene"))) + { + showModalPanel = false; + if (!string.IsNullOrEmpty (sceneCanvasName)) + canvasCache.SaveSceneNodeCanvas(sceneCanvasName); + } + GUILayout.EndHorizontal(); + } + + public void DrawModalPanel() + { + if (showModalPanel) + { + if (modalPanelContent == null) + return; + GUILayout.BeginArea(modalPanelRect, NodeEditorGUI.nodeBox); + modalPanelContent.Invoke(); + GUILayout.EndArea(); + } + } + private void DrawSideWindow() { GUILayout.Label (new GUIContent ("" + canvasCache.nodeCanvas.saveName + " (" + (canvasCache.nodeCanvas.livesInScene? "Scene Save" : "Asset Save") + ")", "Opened Canvas path: " + canvasCache.nodeCanvas.savePath), NodeEditorGUI.nodeLabelBold); @@ -374,12 +399,12 @@ private void DrawSideWindow() #endregion - #region Menu Callbacks +#region Menu Callbacks private void CreateCanvas(Type type) { editor.canvasCache.NewNodeCanvas(type); - } + } private void LoadCanvas() { @@ -390,37 +415,37 @@ private void LoadCanvas() ShowNotification(new GUIContent("You should select an asset inside your project folder!")); } else - canvasCache.LoadNodeCanvas (path); + canvasCache.LoadNodeCanvas(path); } private void ReloadCanvas() { string path = canvasCache.nodeCanvas.savePath; - if (!string.IsNullOrEmpty (path)) + if (!string.IsNullOrEmpty(path)) { - if (path.StartsWith ("SCENE/")) - canvasCache.LoadSceneNodeCanvas (path.Substring (6)); + if (path.StartsWith("SCENE/")) + canvasCache.LoadSceneNodeCanvas(path.Substring(6)); else - canvasCache.LoadNodeCanvas (path); - ShowNotification (new GUIContent ("Canvas Reloaded!")); + canvasCache.LoadNodeCanvas(path); + ShowNotification(new GUIContent("Canvas Reloaded!")); } else - ShowNotification (new GUIContent ("Cannot reload canvas as it has not been saved yet!")); + ShowNotification(new GUIContent("Cannot reload canvas as it has not been saved yet!")); } private void SaveCanvas() { string path = canvasCache.nodeCanvas.savePath; - if (!string.IsNullOrEmpty (path)) + if (!string.IsNullOrEmpty(path)) { - if (path.StartsWith ("SCENE/")) - canvasCache.SaveSceneNodeCanvas (path.Substring (6)); + if (path.StartsWith("SCENE/")) + canvasCache.SaveSceneNodeCanvas(path.Substring(6)); else - canvasCache.SaveNodeCanvas (path); - ShowNotification (new GUIContent ("Canvas Saved!")); + canvasCache.SaveNodeCanvas(path); + ShowNotification(new GUIContent("Canvas Saved!")); } else - ShowNotification (new GUIContent ("No save location found. Use 'Save As'!")); + ShowNotification(new GUIContent("No save location found. Use 'Save As'!")); } private void SaveCanvasAs() @@ -429,17 +454,88 @@ private void SaveCanvasAs() if (canvasCache.nodeCanvas != null && !string.IsNullOrEmpty(canvasCache.nodeCanvas.savePath)) panelPath = canvasCache.nodeCanvas.savePath; string path = EditorUtility.SaveFilePanelInProject ("Save Node Canvas", "Node Canvas", "asset", "", panelPath); - if (!string.IsNullOrEmpty (path)) - canvasCache.SaveNodeCanvas (path); + if (!string.IsNullOrEmpty(path)) + canvasCache.SaveNodeCanvas(path); } - public void LoadSceneCanvasCallback (object canvas) + private void LoadSceneCanvasCallback (object canvas) { - canvasCache.LoadSceneNodeCanvas ((string)canvas); + canvasCache.LoadSceneNodeCanvas((string)canvas); //Atheos auto save the sceneCanvasName to (sava to scence) after (Load from scene) - sceneCanvasName = canvasCache.nodeCanvas.name; + sceneCanvasName = canvasCache.nodeCanvas.name; } - #endregion + private void SaveSceneCanvasCallback() + { + modalPanelContent = SaveSceneCanvasPanel; + showModalPanel = true; + } + + + private void ImportCanvasCallback(string formatID) + { + IOFormat = ImportExportManager.ParseFormat(formatID); + if (IOFormat.RequiresLocationGUI) + { + ImportLocationGUI = IOFormat.ImportLocationArgsGUI; + modalPanelContent = ImportCanvasGUI; + showModalPanel = true; + } + else if (IOFormat.ImportLocationArgsSelection(out IOLocationArgs)) + canvasCache.SetCanvas(ImportExportManager.ImportCanvas(IOFormat, IOLocationArgs)); + } + + private void ImportCanvasGUI() + { + if (ImportLocationGUI != null) + { + bool? state = ImportLocationGUI(ref IOLocationArgs); + if (state == null) + return; + + if (state == true) + canvasCache.SetCanvas(ImportExportManager.ImportCanvas(IOFormat, IOLocationArgs)); + + ImportLocationGUI = null; + modalPanelContent = null; + showModalPanel = false; + } + else + showModalPanel = false; + } + + private void ExportCanvasCallback(string formatID) + { + IOFormat = ImportExportManager.ParseFormat(formatID); + if (IOFormat.RequiresLocationGUI) + { + ExportLocationGUI = IOFormat.ExportLocationArgsGUI; + modalPanelContent = ExportCanvasGUI; + showModalPanel = true; + } + else if (IOFormat.ExportLocationArgsSelection(canvasCache.nodeCanvas.saveName, out IOLocationArgs)) + ImportExportManager.ExportCanvas(canvasCache.nodeCanvas, IOFormat, IOLocationArgs); + } + + private void ExportCanvasGUI() + { + if (ExportLocationGUI != null) + { + bool? state = ExportLocationGUI(canvasCache.nodeCanvas.saveName, ref IOLocationArgs); + if (state == null) + return; + + if (state == true) + ImportExportManager.ExportCanvas(canvasCache.nodeCanvas, IOFormat, IOLocationArgs); + + ImportLocationGUI = null; + modalPanelContent = null; + showModalPanel = false; + } + else + showModalPanel = false; + } + +#endregion } } diff --git a/Node_Editor/Default/XMLImportExport.cs b/Node_Editor/Default/XMLImportExport.cs new file mode 100644 index 00000000..ffc243ae --- /dev/null +++ b/Node_Editor/Default/XMLImportExport.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Text; +using System.Xml; +using System.Xml.Serialization; +using System.Xml.XPath; +using UnityEngine; + +namespace NodeEditorFramework.IO +{ + public class XMLImportExport : StructuredImportExportFormat + { + public override string FormatIdentifier { get { return "XML"; } } + public override string FormatExtension { get { return "xml"; } } + + public override void ExportData (CanvasData data, params object[] args) + { + if (args == null || args.Length != 1 || args[0].GetType () != typeof(string)) + throw new ArgumentException ("Location Arguments"); + string path = (string)args[0]; + + XmlDocument saveDoc = new XmlDocument(); + XmlDeclaration decl = saveDoc.CreateXmlDeclaration("1.0", "UTF-8", null); + saveDoc.InsertBefore(decl, saveDoc.DocumentElement); + + // CANVAS + + XmlElement canvas = saveDoc.CreateElement("NodeCanvas"); + canvas.SetAttribute("type", data.type.FullName); + saveDoc.AppendChild(canvas); + + // EDITOR STATES + + XmlElement editorStates = saveDoc.CreateElement("EditorStates"); + canvas.AppendChild(editorStates); + foreach (EditorStateData stateData in data.editorStates) + { + XmlElement editorState = saveDoc.CreateElement("EditorState"); + editorState.SetAttribute("selected", stateData.selectedNode != null ? stateData.selectedNode.nodeID.ToString() : ""); + editorState.SetAttribute("pan", stateData.panOffset.x + "," + stateData.panOffset.y); + editorState.SetAttribute("zoom", stateData.zoom.ToString()); + editorStates.AppendChild(editorState); + } + + // NODES + + XmlElement nodes = saveDoc.CreateElement("Nodes"); + canvas.AppendChild(nodes); + foreach (NodeData nodeData in data.nodes.Values) + { + XmlElement node = saveDoc.CreateElement("Node"); + node.SetAttribute("ID", nodeData.nodeID.ToString()); + node.SetAttribute("type", nodeData.typeID); + node.SetAttribute("pos", nodeData.nodePos.x + "," + nodeData.nodePos.y); + nodes.AppendChild(node); + // Write port records + //XmlElement connectionPorts = saveDoc.CreateElement("ConnectionPorts"); + //node.AppendChild(connectionPorts); + foreach (PortData portData in nodeData.connectionPorts) + { + XmlElement port = saveDoc.CreateElement("Port"); + port.SetAttribute("ID", portData.portID.ToString ()); + port.SetAttribute("varName", portData.varName); + node.AppendChild(port); + // Connections + /*foreach (PortData conData in portData.connections) + { // TODO: Write immediate connections. Not needed, only for readability. + XmlElement connection = saveDoc.CreateElement("Connection"); + connection.SetAttribute("ID", conData.portID.ToString()); + port.AppendChild(connection); + }*/ + } + // Write variable data + //XmlElement variables = saveDoc.CreateElement("Variables"); + //node.AppendChild(variables); + foreach (VariableData varData in nodeData.variables) + { + XmlElement variable = saveDoc.CreateElement("Variable"); + variable.SetAttribute("name", varData.name); + node.AppendChild(variable); + if (varData.refObject != null) + variable.SetAttribute("refID", varData.refObject.refID.ToString()); + else + { // Serialize value and append + variable.SetAttribute("type", varData.value.GetType ().FullName); + SerializeObjectToXML(variable, varData.value); + } + } + } + + // CONNECTIONS + + XmlElement connections = saveDoc.CreateElement("Connections"); + canvas.AppendChild(connections); + foreach (ConnectionData connectionData in data.connections) + { + XmlElement connection = saveDoc.CreateElement("Connection"); + connection.SetAttribute("port1ID", connectionData.port1.portID.ToString ()); + connection.SetAttribute("port2ID", connectionData.port2.portID.ToString ()); + connections.AppendChild(connection); + } + + // OBJECTS + + XmlElement objects = saveDoc.CreateElement("Objects"); + canvas.AppendChild(objects); + foreach (ObjectData objectData in data.objects.Values) + { + XmlElement obj = saveDoc.CreateElement("Object"); + obj.SetAttribute("refID", objectData.refID.ToString()); + obj.SetAttribute("type", objectData.data.GetType().FullName); + objects.AppendChild(obj); + SerializeObjectToXML(obj, objectData.data); + } + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + using (XmlTextWriter writer = new XmlTextWriter (path, Encoding.UTF8)) + { + writer.Formatting = Formatting.Indented; + writer.Indentation = 1; + writer.IndentChar = '\t'; + saveDoc.Save (writer); + } + } + + public override CanvasData ImportData (params object[] args) + { + if (args == null || args.Length != 1 || args[0].GetType () != typeof(string)) + throw new ArgumentException ("Location Arguments"); + string path = (string)args[0]; + + using (FileStream fs = new FileStream (path, FileMode.Open)) + { + XmlDocument data = new XmlDocument (); + data.Load (fs); + + // CANVAS + + string canvasName = Path.GetFileNameWithoutExtension(path); + XmlElement xmlCanvas = (XmlElement)data.SelectSingleNode ("//NodeCanvas"); + Type canvasType = NodeCanvasManager.GetCanvasTypeData (xmlCanvas.GetAttribute ("type")).CanvasType; + if (canvasType == null) + throw new XmlException ("Could not find NodeCanvas of type '" + xmlCanvas.GetAttribute ("type") + "'!"); + CanvasData canvasData = new CanvasData (canvasType, canvasName); + Dictionary ports = new Dictionary(); + + // OBJECTS + + IEnumerable xmlObjects = xmlCanvas.SelectNodes("Objects/Object").OfType(); + foreach (XmlElement xmlObject in xmlObjects) + { + int refID = GetIntegerAttribute(xmlObject, "refID"); + string typeName = xmlObject.GetAttribute("type"); + Type type = Type.GetType(typeName, true); + object obj = DeserializeObjectFromXML(xmlObject, type); + ObjectData objData = new ObjectData(refID, obj); + canvasData.objects.Add(refID, objData); + } + + // NODES + + IEnumerable xmlNodes = xmlCanvas.SelectNodes("Nodes/Node").OfType(); + foreach (XmlElement xmlNode in xmlNodes) + { + int nodeID = GetIntegerAttribute(xmlNode, "ID"); + string typeID = xmlNode.GetAttribute("type"); + Vector2 nodePos = GetVectorAttribute(xmlNode, "pos"); + // Record + NodeData node = new NodeData(typeID, nodeID, nodePos); + canvasData.nodes.Add(nodeID, node); + // Validate and record ports + IEnumerable xmlConnectionPorts = xmlNode.SelectNodes("Port").OfType(); + foreach (XmlElement xmlPort in xmlConnectionPorts) + { + int portID = GetIntegerAttribute(xmlPort, "ID"); + string varName = xmlPort.GetAttribute("varName"); + PortData port = new PortData(node, varName, portID); + node.connectionPorts.Add(port); + ports.Add(portID, port); + } + // Read in variable data + IEnumerable xmlVariables = xmlNode.SelectNodes("Variable").OfType(); + foreach (XmlElement xmlVariable in xmlVariables) + { + string varName = xmlVariable.GetAttribute("name"); + VariableData varData = new VariableData(varName); + if (xmlVariable.HasAttribute("refID")) + { // Read from objects + int refID = GetIntegerAttribute(xmlVariable, "refID"); + ObjectData objData; + if (canvasData.objects.TryGetValue(refID, out objData)) + varData.refObject = objData; + } + else + { // Read directly + string typeName = xmlVariable.GetAttribute("type"); + Type type = Type.GetType(typeName, true); + varData.value = DeserializeObjectFromXML(xmlVariable, type); + } + node.variables.Add(varData); + } + } + + // CONNECTIONS + + IEnumerable xmlConnections = xmlCanvas.SelectNodes("Connections/Connection").OfType(); + foreach (XmlElement xmlConnection in xmlConnections) + { + int port1ID = GetIntegerAttribute(xmlConnection, "port1ID"); + int port2ID = GetIntegerAttribute(xmlConnection, "port2ID"); + PortData port1, port2; + if (ports.TryGetValue(port1ID, out port1) && ports.TryGetValue(port2ID, out port2)) + canvasData.RecordConnection(port1, port2); + } + + // EDITOR STATES + + IEnumerable xmlEditorStates = xmlCanvas.SelectNodes("EditorStates/EditorState").OfType(); + List editorStates = new List(); + foreach (XmlElement xmlEditorState in xmlEditorStates) + { + Vector2 pan = GetVectorAttribute(xmlEditorState, "pan"); + float zoom; + if (!float.TryParse(xmlEditorState.GetAttribute("zoom"), out zoom)) + zoom = 1; + // Selected Node + NodeData selectedNode = null; + int selectedNodeID; + if (int.TryParse(xmlEditorState.GetAttribute("selected"), out selectedNodeID)) + selectedNode = canvasData.FindNode(selectedNodeID); + // Create state + editorStates.Add(new EditorStateData(selectedNode, pan, zoom)); + } + canvasData.editorStates = editorStates.ToArray(); + + return canvasData; + } + } + + #region Utility + + private void SerializeObjectToXML(XmlElement parent, object obj) + { + XmlSerializer serializer = new XmlSerializer(obj.GetType()); + XPathNavigator navigator = parent.CreateNavigator(); + using (XmlWriter writer = navigator.AppendChild()) + serializer.Serialize(writer, obj); + } + + private object DeserializeObjectFromXML(XmlElement parent, Type type) + { + if (!parent.HasChildNodes) + return null; + XmlSerializer serializer = new XmlSerializer(type); + XPathNavigator navigator = parent.FirstChild.CreateNavigator(); + using (XmlReader reader = navigator.ReadSubtree()) + return serializer.Deserialize(reader); + } + + private int GetIntegerAttribute(XmlElement element, string attribute, bool throwIfInvalid = true) + { + int result = 0; + if (!int.TryParse(element.GetAttribute(attribute), out result) && throwIfInvalid) + throw new XmlException("Invalid " + attribute + " for element " + element.Name + "!"); + return result; + } + + private float GetFloatAttribute(XmlElement element, string attribute, bool throwIfInvalid = true) + { + float result = 0; + if (!float.TryParse(element.GetAttribute(attribute), out result) && throwIfInvalid) + throw new XmlException("Invalid " + attribute + " for element " + element.Name + "!"); + return result; + } + + private Vector2 GetVectorAttribute(XmlElement element, string attribute, bool throwIfInvalid = true) + { + string[] vecString = element.GetAttribute(attribute).Split(','); + Vector2 vector = new Vector2(0, 0); + float vecX, vecY; + if (vecString.Length == 2 && float.TryParse(vecString[0], out vecX) && float.TryParse(vecString[1], out vecY)) + vector = new Vector2(vecX, vecY); + return vector; + } + + #endregion + } +} \ No newline at end of file diff --git a/Node_Editor/Default/XMLImportExport.cs.meta b/Node_Editor/Default/XMLImportExport.cs.meta new file mode 100644 index 00000000..e584efe0 --- /dev/null +++ b/Node_Editor/Default/XMLImportExport.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 4b4fed10ebd11f149aca4caaba5a4c9b +timeCreated: 1497983217 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Node_Editor/Framework/Core/NodeEditorState.cs b/Node_Editor/Framework/Core/NodeEditorState.cs index 041fdd06..0182cfa5 100644 --- a/Node_Editor/Framework/Core/NodeEditorState.cs +++ b/Node_Editor/Framework/Core/NodeEditorState.cs @@ -11,7 +11,7 @@ public partial class NodeEditorState : ScriptableObject public NodeEditorState parentEditor; // Canvas options - public bool drawing = true; // whether to draw the canvas + [NonSerialized] public bool drawing = true; // whether to draw the canvas // Selection State public Node selectedNode; // selected Node diff --git a/Node_Editor/Framework/NodeEditor.cs b/Node_Editor/Framework/NodeEditor.cs index 8411db40..1ffd21d9 100644 --- a/Node_Editor/Framework/NodeEditor.cs +++ b/Node_Editor/Framework/NodeEditor.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; -using NodeEditorFramework; using NodeEditorFramework.Utilities; +using NodeEditorFramework.IO; using Object = UnityEngine.Object; @@ -79,6 +79,7 @@ private static void setupBaseFramework () NodeTypes.FetchNodeTypes (); NodeCanvasManager.FetchCanvasTypes (); ConnectionPortManager.FetchNodeConnectionDeclarations (); + ImportExportManager.FetchIOFormats (); // Setup Callback system NodeEditorCallbacks.SetupReceivers (); diff --git a/Node_Editor/Framework/SaveSystem/ImportExportFormat.cs b/Node_Editor/Framework/SaveSystem/ImportExportFormat.cs new file mode 100644 index 00000000..9d90ee38 --- /dev/null +++ b/Node_Editor/Framework/SaveSystem/ImportExportFormat.cs @@ -0,0 +1,213 @@ +using System.IO; +using UnityEngine; +using NodeEditorFramework; +using NodeEditorFramework.Utilities; + +namespace NodeEditorFramework.IO +{ + /// + /// Base class of an arbitrary Import/Export format based directly on the NodeCanvas + /// + public abstract class ImportExportFormat + { + /// + /// Identifier for this format, must be unique (e.g. 'XML') + /// + public abstract string FormatIdentifier { get; } + + /// + /// Optional format description (e.g. 'Legacy', shown as 'XML (Legacy)') + /// + public virtual string FormatDescription { get { return ""; } } + + /// + /// Optional extension for this format if saved as a file, e.g. 'xml', default equals FormatIdentifier + /// + public virtual string FormatExtension { get { return FormatIdentifier; } } + + /// + /// Returns whether the location selection needs to be performed through a GUI (e.g. for a custom database access) + /// If true, the Import-/ExportLocationArgsGUI functions are called, else Import-/ExportLocationArgsSelection + /// + public virtual bool RequiresLocationGUI { get { +#if UNITY_EDITOR + return false; // In the editor, use file browser seletion +#else + return true; // At runtime, use GUI to select a file in a fixed folder +#endif + } + } + + /// + /// Folder for runtime IO operations relative to the game folder. + /// + public virtual string RuntimeIOPath { get { return "Files/NodeEditor/"; } } + +#if !UNITY_EDITOR + private string fileSelection = ""; + private Rect fileSelectionMenuRect; +#endif + + /// + /// Called only if RequiresLocationGUI is true. + /// Displays GUI filling in locationArgs with the information necessary to locate the import operation. + /// Override along with RequiresLocationGUI for custom database access. + /// Return true or false to perform or cancel the import operation. + /// + public virtual bool? ImportLocationArgsGUI (ref object[] locationArgs) + { +#if UNITY_EDITOR + return ImportLocationArgsSelection (out locationArgs); +#else + GUILayout.Label("Import canvas from " + FormatIdentifier); + GUILayout.BeginHorizontal(); + GUILayout.Label(RuntimeIOPath, GUILayout.ExpandWidth(false)); + if (GUILayout.Button(string.IsNullOrEmpty(fileSelection)? "Select..." : fileSelection + "." + FormatExtension, GUILayout.ExpandWidth(true))) + { + // Find save files + DirectoryInfo dir = Directory.CreateDirectory(RuntimeIOPath); + FileInfo[] files = dir.GetFiles("*." + FormatExtension); + // Fill save file selection menu + GenericMenu fileSelectionMenu = new GenericMenu(false); + foreach (FileInfo file in files) + fileSelectionMenu.AddItem(new GUIContent(file.Name), false, () => fileSelection = Path.GetFileNameWithoutExtension(file.Name)); + fileSelectionMenu.DropDown(fileSelectionMenuRect); + } + if (Event.current.type == EventType.Repaint) + { + Rect popupPos = GUILayoutUtility.GetLastRect(); + fileSelectionMenuRect = new Rect(popupPos.x + 2, popupPos.yMax + 2, popupPos.width - 4, 0); + } + GUILayout.EndHorizontal(); + + // Finish operation buttons + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Cancel")) + return false; + if (GUILayout.Button("Import")) + { + if (string.IsNullOrEmpty(fileSelection) || !File.Exists(RuntimeIOPath + fileSelection + "." + FormatExtension)) + return false; + fileSelection = Path.GetFileNameWithoutExtension(fileSelection); + locationArgs = new object[] { RuntimeIOPath + fileSelection + "." + FormatExtension }; + return true; + } + GUILayout.EndHorizontal(); + + return null; +#endif + } + + /// + /// Called only if RequiresLocationGUI is false. + /// Returns the information necessary to locate the import operation. + /// By default it lets the user select a path as string[1]. + /// + public virtual bool ImportLocationArgsSelection (out object[] locationArgs) + { + string path = null; +#if UNITY_EDITOR + path = UnityEditor.EditorUtility.OpenFilePanel( + "Import " + FormatIdentifier + (!string.IsNullOrEmpty (FormatDescription)? (" (" + FormatDescription + ")") : ""), + "Assets", FormatExtension.ToLower ()); +#endif + locationArgs = new object[] { path }; + return !string.IsNullOrEmpty (path); + } + + /// + /// Called only if RequiresLocationGUI is true. + /// Displays GUI filling in locationArgs with the information necessary to locate the export operation. + /// Override along with RequiresLocationGUI for custom database access. + /// Return true or false to perform or cancel the export operation. + /// + public virtual bool? ExportLocationArgsGUI (string canvasName, ref object[] locationArgs) + { +#if UNITY_EDITOR + return ExportLocationArgsSelection(canvasName, out locationArgs); +#else + GUILayout.Label("Export canvas to " + FormatIdentifier); + + // File save field + GUILayout.BeginHorizontal(); + GUILayout.Label(RuntimeIOPath, GUILayout.ExpandWidth(false)); + fileSelection = GUILayout.TextField(fileSelection, GUILayout.ExpandWidth(true)); + GUILayout.Label("." + FormatExtension, GUILayout.ExpandWidth (false)); + GUILayout.EndHorizontal(); + + // Finish operation buttons + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Cancel")) + return false; + if (GUILayout.Button("Export")) + { + if (string.IsNullOrEmpty(fileSelection)) + return false; + fileSelection = Path.GetFileNameWithoutExtension(fileSelection); + locationArgs = new object[] { RuntimeIOPath + fileSelection + "." + FormatExtension }; + return true; + } + GUILayout.EndHorizontal(); + + return null; +#endif + } + + /// + /// Called only if RequiresLocationGUI is false. + /// Returns the information necessary to locate the export operation. + /// By default it lets the user select a path as string[1]. + /// + public virtual bool ExportLocationArgsSelection (string canvasName, out object[] locationArgs) + { + string path = null; +#if UNITY_EDITOR + path = UnityEditor.EditorUtility.SaveFilePanel( + "Export " + FormatIdentifier + (!string.IsNullOrEmpty (FormatDescription)? (" (" + FormatDescription + ")") : ""), + "Assets", canvasName, FormatExtension.ToLower ()); +#endif + locationArgs = new object[] { path }; + return !string.IsNullOrEmpty (path); + } + + /// + /// Imports the canvas at the location specified in the args (usually string[1] containing the path) and returns it's simplified canvas data + /// + public abstract NodeCanvas Import (params object[] locationArgs); + + /// + /// Exports the given simplified canvas data to the location specified in the args (usually string[1] containing the path) + /// + public abstract void Export (NodeCanvas canvas, params object[] locationArgs); + } + + /// + /// Base class of an arbitrary Import/Export format based on a simple structural data best for most formats + /// + public abstract class StructuredImportExportFormat : ImportExportFormat + { + public override NodeCanvas Import (params object[] locationArgs) + { + CanvasData data = ImportData (locationArgs); + if (data == null) + return null; + return ImportExportManager.ConvertToNodeCanvas (data); + } + + public override void Export (NodeCanvas canvas, params object[] locationArgs) + { + CanvasData data = ImportExportManager.ConvertToCanvasData (canvas); + ExportData (data, locationArgs); + } + + /// + /// Imports the canvas at the location specified in the args (usually string[1] containing the path) and returns it's simplified canvas data + /// + public abstract CanvasData ImportData (params object[] locationArgs); + + /// + /// Exports the given simplified canvas data to the location specified in the args (usually string[1] containing the path) + /// + public abstract void ExportData (CanvasData data, params object[] locationArgs); + } +} \ No newline at end of file diff --git a/Node_Editor/Framework/SaveSystem/ImportExportFormat.cs.meta b/Node_Editor/Framework/SaveSystem/ImportExportFormat.cs.meta new file mode 100644 index 00000000..82d98f77 --- /dev/null +++ b/Node_Editor/Framework/SaveSystem/ImportExportFormat.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 961d85cbdc1829442907cdcfd40aaf01 +timeCreated: 1497884522 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Node_Editor/Framework/SaveSystem/ImportExportManager.cs b/Node_Editor/Framework/SaveSystem/ImportExportManager.cs new file mode 100644 index 00000000..305dad25 --- /dev/null +++ b/Node_Editor/Framework/SaveSystem/ImportExportManager.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +using NodeEditorFramework; +using NodeEditorFramework.Utilities; + +namespace NodeEditorFramework.IO +{ + /// + /// Manager for Import Export operations, including fetching of all supported formats + /// + public static class ImportExportManager + { + private static Dictionary IOFormats; + + private static Action _importMenuCallback; + private static Action _exportMenuCallback; + + /// + /// Fetches every IO Format in the script assemblies to provide the framework with custom import and export formats + /// + public static void FetchIOFormats() + { + IOFormats = new Dictionary(); + foreach (Type type in ReflectionUtility.getSubTypes (typeof(ImportExportFormat))) + { + ImportExportFormat formatter = (ImportExportFormat)Activator.CreateInstance (type); + IOFormats.Add (formatter.FormatIdentifier, formatter); + } + } + + /// + /// Returns the format specified by the given IO format identifier + /// + public static ImportExportFormat ParseFormat (string IOformat) + { + ImportExportFormat formatter; + if (IOFormats.TryGetValue (IOformat, out formatter)) + return formatter; + else + throw new ArgumentException ("Unknown format '" + IOformat + "'"); + } + + /// + /// Imports the canvas with the given formatter from the location specified in the args and returns it + /// + public static NodeCanvas ImportCanvas (ImportExportFormat formatter, params object[] args) + { + return formatter.Import (args); + } + + /// + /// Exports the given canvas with the given formatter to the location specified in the args + /// + public static void ExportCanvas (NodeCanvas canvas, ImportExportFormat formatter, params object[] args) + { + formatter.Export (canvas, args); + } + + #region Canvas Type Menu + + public static void FillImportFormatMenu(ref GenericMenu menu, Action IOFormatSelection, string path = "") + { + _importMenuCallback = IOFormatSelection; + foreach (string formatID in IOFormats.Keys) + menu.AddItem(new GUIContent(path + formatID), false, unwrapInputFormatIDCallback, (object)formatID); + } + + #if UNITY_EDITOR + public static void FillImportFormatMenu(ref UnityEditor.GenericMenu menu, Action IOFormatSelection, string path = "") + { + _importMenuCallback = IOFormatSelection; + foreach (string formatID in IOFormats.Keys) + menu.AddItem(new GUIContent(path + formatID), false, unwrapInputFormatIDCallback, (object)formatID); + } + #endif + + public static void FillExportFormatMenu(ref GenericMenu menu, Action IOFormatSelection, string path = "") + { + _exportMenuCallback = IOFormatSelection; + foreach (string formatID in IOFormats.Keys) + menu.AddItem(new GUIContent(path + formatID), false, unwrapExportFormatIDCallback, (object)formatID); + } + + #if UNITY_EDITOR + public static void FillExportFormatMenu(ref UnityEditor.GenericMenu menu, Action IOFormatSelection, string path = "") + { + _exportMenuCallback = IOFormatSelection; + foreach (string formatID in IOFormats.Keys) + menu.AddItem(new GUIContent(path + formatID), false, unwrapExportFormatIDCallback, (object)formatID); + } + #endif + + private static void unwrapInputFormatIDCallback(object formatID) + { + _importMenuCallback((string)formatID); + } + + private static void unwrapExportFormatIDCallback(object formatID) + { + _exportMenuCallback((string)formatID); + } + + #endregion + + #region Converter + + /// + /// Converts the NodeCanvas to a simplified CanvasData + /// + internal static CanvasData ConvertToCanvasData (NodeCanvas canvas) + { + if (canvas == null) + return null; + + // Validate canvas and create canvas data for it + canvas.Validate (); + CanvasData canvasData = new CanvasData (canvas); + + // Store Lookup-Table for all ports + Dictionary portDatas = new Dictionary(); + + foreach (Node node in canvas.nodes) + { + // Create node data + NodeData nodeData = new NodeData (node); + canvasData.nodes.Add (nodeData.nodeID, nodeData); + + foreach (ConnectionPortDeclaration portDecl in ConnectionPortManager.GetPortDeclarationEnumerator(node)) + { // Fetch all connection ports and record them + ConnectionPort port = (ConnectionPort)portDecl.portField.GetValue(node); + PortData portData = new PortData(nodeData, port, portDecl.portField.Name); + nodeData.connectionPorts.Add(portData); + portDatas.Add(port, portData); + } + + // Fetch all serialized node variables specific to each node's implementation + FieldInfo[] serializedFields = ReflectionUtility.getSerializedFields (node.GetType (), typeof(Node)); + foreach (FieldInfo field in serializedFields) + { // Create variable data and enter the + if (field.FieldType.IsSubclassOf(typeof(ConnectionPort))) + continue; + VariableData varData = new VariableData (field); + nodeData.variables.Add (varData); + object varValue = field.GetValue (node); + if (field.FieldType.IsValueType) // Store value of the object + varData.value = varValue; + else // Store reference to the object + varData.refObject = canvasData.ReferenceObject (varValue); + } + } + + foreach (PortData portData in portDatas.Values) + { // Record the connections of this port + foreach (ConnectionPort conPort in portData.port.connections) + { + PortData conPortData; // Get portData associated with the connection port + if (portDatas.TryGetValue(conPort, out conPortData)) + canvasData.RecordConnection(portData, conPortData); + } + } + + canvasData.editorStates = new EditorStateData[canvas.editorStates.Length]; + for (int i = 0; i < canvas.editorStates.Length; i++) + { // Record all editorStates + NodeEditorState state = canvas.editorStates[i]; + NodeData selected = state.selectedNode == null ? null : canvasData.FindNode(state.selectedNode); + canvasData.editorStates[i] = new EditorStateData(selected, state.panOffset, state.zoom); + } + + return canvasData; + } + + /// + /// Converts the simplified CanvasData back to a proper NodeCanvas + /// + internal static NodeCanvas ConvertToNodeCanvas (CanvasData canvasData) + { + if (canvasData == null) + return null; + NodeCanvas nodeCanvas = NodeCanvas.CreateCanvas(canvasData.type); + nodeCanvas.name = nodeCanvas.saveName = canvasData.name; + nodeCanvas.nodes.Clear(); + NodeEditor.BeginEditingCanvas(nodeCanvas); + + foreach (NodeData nodeData in canvasData.nodes.Values) + { // Read all nodes + Node node = Node.Create (nodeData.typeID, nodeData.nodePos, null, true); + if (node == null) + continue; + foreach (ConnectionPortDeclaration portDecl in ConnectionPortManager.GetPortDeclarationEnumerator(node)) + { // Find stored ports for each node port declaration + PortData portData = nodeData.connectionPorts.Find((PortData data) => data.varName == portDecl.portField.Name); + if (portData != null) // Stored port has been found, record + portData.port = (ConnectionPort)portDecl.portField.GetValue(node); + } + foreach (VariableData varData in nodeData.variables) + { // Restore stored variable to node + FieldInfo field = node.GetType().GetField(varData.name); + if (field != null) + field.SetValue(node, varData.refObject != null ? varData.refObject.data : varData.value); + } + } + + foreach (ConnectionData conData in canvasData.connections) + { // Restore all connections + if (conData.port1.port == null || conData.port2.port == null) + { // Not all ports where saved in canvasData + Debug.Log("Incomplete connection " + conData.port1.varName + " and " + conData.port2.varName + "!"); + continue; + } + conData.port1.port.TryApplyConnection(conData.port2.port, true); + } + + nodeCanvas.editorStates = new NodeEditorState[canvasData.editorStates.Length]; + for (int i = 0; i < canvasData.editorStates.Length; i++) + { // Read all editorStates + EditorStateData stateData = canvasData.editorStates[i]; + NodeEditorState state = ScriptableObject.CreateInstance(); + state.selectedNode = stateData.selectedNode == null ? null : canvasData.FindNode(stateData.selectedNode.nodeID).node; + state.panOffset = stateData.panOffset; + state.zoom = stateData.zoom; + state.canvas = nodeCanvas; + state.name = "EditorState"; + nodeCanvas.editorStates[i] = state; + } + + NodeEditor.EndEditingCanvas(); + return nodeCanvas; + } + + #endregion + } +} \ No newline at end of file diff --git a/Node_Editor/Framework/SaveSystem/ImportExportManager.cs.meta b/Node_Editor/Framework/SaveSystem/ImportExportManager.cs.meta new file mode 100644 index 00000000..775a826f --- /dev/null +++ b/Node_Editor/Framework/SaveSystem/ImportExportManager.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 7761e7c3d61739f4bbb8b023e3e91adc +timeCreated: 1497983354 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Node_Editor/Framework/SaveSystem/ImportExportStructure.cs b/Node_Editor/Framework/SaveSystem/ImportExportStructure.cs new file mode 100644 index 00000000..6b079ef5 --- /dev/null +++ b/Node_Editor/Framework/SaveSystem/ImportExportStructure.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace NodeEditorFramework.IO +{ + public class CanvasData + { + public NodeCanvas canvas; + + public string name; + public Type type; + + public EditorStateData[] editorStates; + + public Dictionary nodes = new Dictionary(); + public List connections = new List(); + public Dictionary objects = new Dictionary(); + + public CanvasData(NodeCanvas nodeCanvas) + { + canvas = nodeCanvas; + name = nodeCanvas.name; + type = nodeCanvas.GetType(); + } + + public CanvasData(Type canvasType, string canvasName) + { + type = canvasType; + name = canvasName; + } + + public ObjectData ReferenceObject(object obj) + { + foreach (ObjectData data in objects.Values) + { + if (data.data == obj) + return data; + } + ObjectData objData = new ObjectData(obj); + objects.Add(objData.refID, objData); + return objData; + } + + public ObjectData FindObject(int refID) + { + ObjectData data; + objects.TryGetValue(refID, out data); + return data; + } + + public NodeData FindNode(Node node) + { + foreach (NodeData data in nodes.Values) + { + if (data.node == node) + return data; + } + return null; + } + + public NodeData FindNode(int nodeID) + { + NodeData data; + nodes.TryGetValue(nodeID, out data); + return data; + } + + public bool RecordConnection(PortData portData1, PortData portData2) + { + if (!portData1.connections.Contains(portData2)) + portData1.connections.Add(portData2); + if (!portData2.connections.Contains(portData1)) + portData2.connections.Add(portData1); + if (!connections.Exists((ConnectionData conData) => conData.isPart(portData1) && conData.isPart(portData2))) + { // Connection hasn't already been recorded + ConnectionData conData = new ConnectionData(portData1, portData2); + connections.Add(conData); + return true; + } + return false; + } + } + + public class EditorStateData + { + public NodeData selectedNode; + public Vector2 panOffset; + public float zoom; + + public EditorStateData(NodeData SelectedNode, Vector2 PanOffset, float Zoom) + { + selectedNode = SelectedNode; + panOffset = PanOffset; + zoom = Zoom; + } + } + + public class NodeData + { + public Node node; + + public int nodeID; + public string typeID; + public Vector2 nodePos; + + public List connectionPorts = new List(); + public List variables = new List(); + + public NodeData(Node n) + { + node = n; + typeID = node.GetID; + nodeID = node.GetHashCode(); + nodePos = node.rect.position; + } + + public NodeData(string TypeID, int NodeID, Vector2 Pos) + { + typeID = TypeID; + nodeID = NodeID; + nodePos = Pos; + } + } + + public class PortData + { + public ConnectionPort port; + + public int portID; + public NodeData body; + public string varName; + + public List connections = new List(); + + public PortData(NodeData Body, ConnectionPort Port, string VarName) + { + port = Port; + portID = port.GetHashCode(); + body = Body; + varName = VarName; + } + + public PortData(NodeData Body, string VarName, int PortID) + { + portID = PortID; + body = Body; + varName = VarName; + } + } + + public class ConnectionData + { + public PortData port1; + public PortData port2; + + public ConnectionData(PortData Port1, PortData Port2) + { + port1 = Port1; + port2 = Port2; + } + + public bool isPart (PortData port) + { + return port1 == port || port2 == port; + } + } + + public class VariableData + { + public string name; + public ObjectData refObject; + public object value; + + public VariableData(FieldInfo field) + { + name = field.Name; + } + + public VariableData(string fieldName) + { + name = fieldName; + } + } + + public class ObjectData + { + public int refID; + public Type type; + public object data; + + public ObjectData(object obj) + { + refID = obj.GetHashCode(); + type = obj.GetType(); + data = obj; + } + + public ObjectData(int objRefID, object obj) + { + refID = objRefID; + type = obj.GetType(); + data = obj; + } + } +} \ No newline at end of file diff --git a/Node_Editor/Framework/SaveSystem/ImportExportStructure.cs.meta b/Node_Editor/Framework/SaveSystem/ImportExportStructure.cs.meta new file mode 100644 index 00000000..36e2df85 --- /dev/null +++ b/Node_Editor/Framework/SaveSystem/ImportExportStructure.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 84af943011bc7e6448cd010e393c6c02 +timeCreated: 1498754587 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Node_Editor/Framework/SaveSystem/NodeEditorSaveManager.cs b/Node_Editor/Framework/SaveSystem/NodeEditorSaveManager.cs index e57de749..1f19960b 100644 --- a/Node_Editor/Framework/SaveSystem/NodeEditorSaveManager.cs +++ b/Node_Editor/Framework/SaveSystem/NodeEditorSaveManager.cs @@ -534,15 +534,16 @@ private static T ReplaceSO (List scriptableObjects, List - /// Returns the editorState with the specified name in canvas. If not found it will create a new one with that name. + /// Returns the editorState with the specified name in canvas. + /// If not found but others and forceFind is false, a different one is chosen randomly, else a new one is created. /// - public static NodeEditorState ExtractEditorState (NodeCanvas canvas, string stateName) + public static NodeEditorState ExtractEditorState (NodeCanvas canvas, string stateName, bool forceFind = false) { NodeEditorState state = null; - if (canvas.editorStates.Length > 0) - { // Search for the editorState - state = canvas.editorStates.First ((NodeEditorState s) => s != null && s.name == stateName); - } + if (canvas.editorStates.Length > 0) // Search for the editorState + state = canvas.editorStates.FirstOrDefault ((NodeEditorState s) => s.name == stateName); + if (state == null && !forceFind) // Take any other if not found + state = canvas.editorStates.FirstOrDefault(); if (state == null) { // Create editorState state = ScriptableObject.CreateInstance();