diff --git a/README.md b/README.md index 9e5ff0f..f1cac8a 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,21 @@ Tested in Unity 2020.1, 2019.3 and 2018.3. Unsure if it'll work in versions prio ![To Do](gif.gif) -• Attached as a **Component** on a **GameObject**. Can have as many as you want.
-• Uses a **Custom Inspector** based on **UnityEditorInternal.ReorderableList**. Easily reorganise tasks by dragging the left side of each element
+## Features : +• Attached as a **Component** (on a GameObject in Scene or Prefab), or as a **ScriptableObject** Asset. Can have as many as you want.
+• Uses a **Custom Inspector** based on **UnityEditorInternal.ReorderableList**. Easily reorganise tasks by dragging the left side of each element.
• Title of ReorderableList can be edited.
-• Each task includes : **Completion Tickbox**, **Text Area**, and the option to link a **GameObject**, **Component** or **Asset** by dragging it onto the task. Cross-scene references are not supported and will be cleared when reloading the scene. Once an object is linked, it can be removed by using the **Object Field** that appears and selecting *None* at the top.
+• Each task includes : **Completion Tickbox**, **Text Area**, and the option to link a **GameObject**, **Component** or **Asset** by dragging it onto the task. Once an object is linked, it can be removed by using the **Object Field** that appears and selecting *None* at the top.
• Completed tasks turn green. Use the *"Remove Completed Tasks"* button to remove all completed tasks.
• Supports **Rich Text**, (but is disabled while editing the task, so you can see what you are writing)
• Has **Undo / Redo** Support.
-• **Export** Tasks to a txt file (object references will be lost though).
• **Import** Tasks from a txt file. Each line in the file will be added as a task. Lines starting with *"[Complete]"* are marked as completed.
+• **Export** Tasks to a txt file (object references will be lost though).
+• **Cross-Scene Object References** (and scene object references for ScriptableObject) is supported. Linking an object in the same scene will still use the direct object reference, but linking an object from a *different* scene will automatically create and use a hidden GameObject & SceneReferenceHandler script, which assigns a unique ID for the object. This handler is saved in the scene to keep references to objects in that scene, while the SceneAsset and ID is saved in the ToDo list. Cross-scene linked objects will show an asterisk on the task, and the object can only be obtained if the scene is loaded.
+
+## Known Bugs : +• Regular object references may be lost if a To Do list component is moved between scenes. (Cross-scene object references are unaffected)
+• Creating a Prefab from a GameObject containing the To Do list will update regular object references to the cross-scene method so they are not lost. However this can only occur if the To Do list is selected / visible in the inspector currently.

@Cyanilux
:) diff --git a/ToDo/Editor/ToDoEditor.cs b/ToDo/Editor/ToDoEditor.cs index 44c54ff..1836a56 100644 --- a/ToDo/Editor/ToDoEditor.cs +++ b/ToDo/Editor/ToDoEditor.cs @@ -1,18 +1,99 @@ -using UnityEngine; +using System.Collections.Generic; +using UnityEngine; using UnityEditor; using UnityEditorInternal; using System.IO; using System.Text; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using System.Linq; namespace Cyan.ToDo { + [CustomEditor(typeof(ToDoSO))] + public class ToDoSOEditor : ToDoEditor { + + protected override void Awake() { + base.Awake(); + todoList = (target as ToDoSO).list; + } + + } + [CustomEditor(typeof(ToDo))] + public class ToDoMBEditor : ToDoEditor { + + protected override void Awake() { + base.Awake(); + + ToDo todo = target as ToDo; + todoList = todo.list; + scenePath = todo.gameObject.scene.path; + + PrefabUtility.prefabInstanceUpdated += PrefabUpdated; + } + + private void OnDisable() { + PrefabUtility.prefabInstanceUpdated -= PrefabUpdated; + } + + private void PrefabUpdated(GameObject prefab) { + // Prefab created / updated + + // This would usually break all scene GameObject/Component references + // But we can convert + serialise an ID, in order to retain references (see SceneReferencesHandler) + + // Note : This isn't called unless the object is SELECTED when the prefab is created since it's in a custom inspector!! + + if (scenePath == null || scenePath == "") return; // Ignore updates to the Prefab asset itself, we only care about scene overrides + + string path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(prefab); + if (path == null || path == "") return; + + ToDo todo = target as ToDo; + for (int i = 0; i < todo.list.tasks.Count; i++) { + ToDoElement element = todo.list.tasks[i]; + if (element.objectReference != null) { + // Has reference + if (element.objectReferenceID == null || element.objectReferenceID == "") { + // Has no objectReferenceID, so is a scene-reference or Asset (not cross-scene already) + if (!IsReferenceAllowed(element.objectReference, null, out GameObject gameObject)) { + // Is in-scene object reference! + if (gameObject == prefab) { + // It's okay, it's the prefab itself, so we can ignore it! + } else { + // Is in-scene object reference, need to swap to cross-scene reference + LinkCrossSceneReference(element, element.objectReference, gameObject); + } + } + } + } + } + + // Apply Prefab Changes + PrefabUtility.prefabInstanceUpdated -= PrefabUpdated; + PrefabUtility.ApplyObjectOverride(todo, path, InteractionMode.AutomatedAction); + PrefabUtility.prefabInstanceUpdated += PrefabUpdated; + // Note, in 2018.3 seems to cause unity to crash/hold due to calling PrefabUpdated + // so need to unregister + reregister to prevent infinite loop. + // This didn't seem to happen in 2020.1, but should probably do it still just to be safe + } + } + + /// + /// Editor / Custom Inspector for the To Do lists. + /// Awake method should be overriden, and todoList value should be set. + /// public class ToDoEditor : Editor { - private ToDo todoList; + protected ToDoList todoList; + protected string scenePath; + // If MonoBehaviour, this is the scene the object is a part of. Used to prevent cross-scene GameObject/Component references. + // If ScriptableObject, is null and used to warn about GameObject/Component scene references + private ReorderableList reorderableList; - private static Color textColor;// = new Color(0.8f, 0.8f, 0.8f); + private static Color textColor; private static Color focusColor = new Color(0, 170 / 255f, 187 / 255f, 0.5f); private static Color activeColor = new Color(0, 170 / 255f, 187 / 255f, 0.3f); @@ -22,28 +103,60 @@ public class ToDoEditor : Editor { private GUIStyle style_greyLabel; private GUIStyle style_textArea; + private GUIStyle style_label; private bool actions; - private void Awake() { - todoList = target as ToDo; + [MenuItem("GameObject/Create Other/To Do List (MonoBehaviour)")] + static void Create() { + GameObject obj = new GameObject("To Do"); + obj.AddComponent(); + Selection.activeGameObject = obj; } - public override void OnInspectorGUI() { - //base.OnInspectorGUI(); - - if (style_textArea == null) { - style_textArea = new GUIStyle(GUI.skin.label); - style_textArea.alignment = TextAnchor.UpperLeft; - style_textArea.wordWrap = true; - style_textArea.richText = true; + [MenuItem("GameObject/Cyan.ToDo/Remove Scene References Handler")] + static void DeleteSceneReferences() { + if (EditorUtility.DisplayDialog("Remove Scene Reference Handler", + "Hey! This will remove the hidden GameObject containing the SceneReferences component in the current active scene. It handles : \n\n"+ + "\u2022 Cross-scene object references for ToDo (MonoBehaviour)\n" + + "\u2022 Scene object references for ToDo (ScriptableObject)\n\n" + + "This will break any references that tasks have to objects in this scene!", "Delete it!", "Abort!")) { + Scene scene = SceneManager.GetActiveScene(); + SceneReferencesHandler sceneReferencesHandler = SceneReferencesHandler.GetSceneReferencesHandler(scene); + if (sceneReferencesHandler != null) { + Debug.Log($"Cyan.ToDo : Scene References Handler has been removed from scene \"{scene.name}\""); + DestroyImmediate(sceneReferencesHandler.gameObject); + } + } + } - textColor = style_textArea.normal.textColor; + [MenuItem("GameObject/Cyan.ToDo/Select Scene References Handler")] + static void SelectSceneReferences() { + Scene scene = SceneManager.GetActiveScene(); + SceneReferencesHandler sceneReferencesHandler = SceneReferencesHandler.GetSceneReferencesHandler(scene); + if (sceneReferencesHandler != null) { + Selection.activeGameObject = sceneReferencesHandler.gameObject; } + } + + [MenuItem("GameObject/Cyan.ToDo/Remove Scene References Handler", true)] + [MenuItem("GameObject/Cyan.ToDo/Select Scene References Handler", true)] + static bool ValidateSceneReferences() { + Scene scene = SceneManager.GetActiveScene(); + return (SceneReferencesHandler.GetSceneReferencesHandler(scene) != null); + } + + protected virtual void Awake() { } + + public override void OnInspectorGUI() { + //base.OnInspectorGUI(); + CreateTextStyles(); + ResetTextColor(); + if (reorderableList == null) { // Create Reorderable List - reorderableList = new ReorderableList(todoList.list, typeof(ToDoElement)); + reorderableList = new ReorderableList(todoList.tasks, typeof(ToDoElement)); reorderableList.drawHeaderCallback = DrawHeader; reorderableList.elementHeightCallback = ElementHeight; reorderableList.drawElementCallback = DrawElement; @@ -66,8 +179,9 @@ public override void OnInspectorGUI() { if (EditorUtility.DisplayDialog("Remove Completed Tasks", $"Are you sure you want to remove all completed tasks for the To Do List \"{todoList.listName}\"?", "Yes!", "Nooo!")) { // Remove Completed Tasks - Undo.RecordObject(todoList, "Remove Completed Tasks"); + Undo.RecordObject(target, "Remove Completed Tasks"); todoList.RemoveCompleted(); + RecordPrefabInstancePropertyModifications(target); } } @@ -81,11 +195,12 @@ public override void OnInspectorGUI() { if (GUILayout.Button(new GUIContent("Import Tasks", "Import tasks from Text File (won't affect existing tasks)"))) { // Import Tasks - Undo.RecordObject(todoList, "Import Tasks"); + Undo.RecordObject(target, "Import Tasks"); string path = EditorUtility.OpenFilePanel("Import", "Assets", "txt"); if (path.Length != 0) { Import(path); } + RecordPrefabInstancePropertyModifications(target); } } @@ -95,13 +210,15 @@ public override void OnInspectorGUI() { } private void OnAdd(ReorderableList list) { - Undo.RecordObject(todoList, "Task Added"); - todoList.list.Add(new ToDoElement()); + Undo.RecordObject(target, "Task Added"); + todoList.tasks.Add(new ToDoElement()); + RecordPrefabInstancePropertyModifications(target); } private void OnRemove(ReorderableList list) { - Undo.RecordObject(todoList, "Task Removed"); - todoList.list.RemoveAt(list.index); + Undo.RecordObject(target, "Task Removed"); + todoList.tasks.RemoveAt(list.index); + RecordPrefabInstancePropertyModifications(target); } private void DrawHeader(Rect rect) { @@ -111,12 +228,16 @@ private void DrawHeader(Rect rect) { style_greyLabel.normal.textColor = Color.grey; } EditorGUI.LabelField(rect, "@Cyanilux", style_greyLabel); - + + CreateTextStyles(); + ResetTextColor(); + EditorGUI.BeginChangeCheck(); string listName = EditorGUI.TextField(rect, todoList.listName, style_textArea); if (EditorGUI.EndChangeCheck()) { - Undo.RecordObject(todoList, "List Name Change"); + Undo.RecordObject(target, "List Name Change"); todoList.listName = listName; + RecordPrefabInstancePropertyModifications(target); } } @@ -124,15 +245,44 @@ private float GetWidth() { return Screen.width - 87; } - private float ElementHeight(int index) { - ToDoElement element = todoList.list[index]; - - float width = GetWidth(); + private void CreateTextStyles() { if (style_textArea == null) { style_textArea = new GUIStyle(GUI.skin.label); style_textArea.alignment = TextAnchor.UpperLeft; style_textArea.wordWrap = true; + style_textArea.richText = true; + textColor = style_textArea.normal.textColor; + } + + if (style_label == null) { + style_label = new GUIStyle(GUI.skin.label); + style_label.alignment = TextAnchor.UpperLeft; + style_label.wordWrap = false; + style_label.richText = false; } + } + + private void ResetTextColor() { + SetTextColor(textColor); + } + + private void SetTextColor(Color textColor) { + style_textArea.normal.textColor = textColor; + style_textArea.focused.textColor = textColor; + style_textArea.hover.textColor = textColor; + + style_label.normal.textColor = textColor; + style_label.focused.textColor = textColor; + style_label.hover.textColor = textColor; + + GUI.skin.settings.cursorColor = textColor; + } + + private float ElementHeight(int index) { + ToDoElement element = todoList.tasks[index]; + + float width = GetWidth(); + CreateTextStyles(); if (element.editing) { style_textArea.richText = false; } @@ -150,7 +300,7 @@ private float ElementHeight(int index) { private void DrawElementBackground(Rect rect, int index, bool active, bool focus) { if (index < 0) return; - ToDoElement element = todoList.list[index]; + ToDoElement element = todoList.tasks[index]; Rect elementRect = new Rect(rect.x, rect.y + 1, rect.width, rect.height - 1); @@ -171,7 +321,7 @@ private void DrawElementBackground(Rect rect, int index, bool active, bool focus } private void DrawElement(Rect rect, int index, bool active, bool focus) { - ToDoElement element = todoList.list[index]; + ToDoElement element = todoList.tasks[index]; #if !UNITY_2020_1_OR_NEWER // rect.height is wrong for older versions (tested: 2018.3, 2019.3), so needs recalculating @@ -195,28 +345,25 @@ private void DrawElement(Rect rect, int index, bool active, bool focus) { new Rect(rect.x + 2, rect.y + 2, h, h), element.completed); if (EditorGUI.EndChangeCheck()) { - Undo.RecordObject(todoList, "Toggled Task Completion"); + Undo.RecordObject(target, "Toggled Task Completion"); element.completed = completed; + RecordPrefabInstancePropertyModifications(target); + } + + // Text Colours + if (completed) { + SetTextColor(completedTextColor); + } else { + ResetTextColor(); } // This prevents text area from highlighting all text on focus bool preventSelection = (UnityEngine.Event.current.type == EventType.MouseDown); - Color cursorColor = GUI.skin.settings.cursorColor; + //Color cursorColor = GUI.skin.settings.cursorColor; if (preventSelection) { GUI.skin.settings.cursorColor = new Color(0, 0, 0, 0); } - - // Text Colours - if (completed) { - style_textArea.normal.textColor = completedTextColor; - style_textArea.focused.textColor = completedTextColor; - style_textArea.hover.textColor = completedTextColor; - } else { - style_textArea.normal.textColor = textColor; - style_textArea.focused.textColor = textColor; - style_textArea.hover.textColor = textColor; - } - + // If editing, turn off richText if (element.editing) { style_textArea.richText = false; @@ -230,6 +377,7 @@ private void DrawElement(Rect rect, int index, bool active, bool focus) { } EditorGUI.BeginChangeCheck(); GUI.SetNextControlName("TextArea"); + string text = EditorGUI.TextArea( new Rect(rect.x + x, rect.y + 2, rect.width - x, textHeight), element.text, style_textArea); @@ -238,30 +386,67 @@ private void DrawElement(Rect rect, int index, bool active, bool focus) { style_textArea.richText = true; if (EditorGUI.EndChangeCheck()) { - Undo.RecordObject(todoList, "Edited Task Text"); + Undo.RecordObject(target, "Edited Task Text"); element.text = text; + RecordPrefabInstancePropertyModifications(target); } - - // Reset Cursor Color - if (preventSelection) { - GUI.skin.settings.cursorColor = cursorColor; - } - - // Object Field - if (element.objectReference) { - EditorGUI.BeginChangeCheck(); - EditorGUI.LabelField( - new Rect(rect.x + x, rect.y + rect.height + 5 - 25, rect.width - 27, h), - "Linked Object : ", - style_textArea); - x += EditorGUIUtility.labelWidth; - Object obj = EditorGUI.ObjectField( - new Rect(rect.x + x, rect.y + rect.height + 5 - 25, rect.width - x, h), - element.objectReference, - typeof(Object), true); - if (EditorGUI.EndChangeCheck()) { - Undo.RecordObject(todoList, "Changed Task Object"); - element.objectReference = obj; + + // Object Reference + if (element.objectReference) { // either in-scene reference, or is SceneAsset + Object objectReference = null; + bool isNormalReference = false; + bool valid = true; + string scenePath = null; + if (element.objectReferenceID != null && element.objectReference.GetType() == typeof(SceneAsset)) { + // Cross-scene object reference (or scene object refence for ScriptableObject) + if (element.tempObjectReference != null) { + objectReference = element.tempObjectReference; + } else { + SceneAsset sceneAsset = element.objectReference as SceneAsset; + scenePath = AssetDatabase.GetAssetPath(sceneAsset); + Scene scene = SceneManager.GetSceneByPath(scenePath); + valid = scene.IsValid(); + if (valid) { + SceneReferencesHandler sceneReferencesHandler = SceneReferencesHandler.GetSceneReferencesHandler(scene); + if (sceneReferencesHandler != null) { + objectReference = sceneReferencesHandler.GetObjectFromID(element.objectReferenceID); + element.tempObjectReference = objectReference; + } + } + } + } else { + // Normal in-same-scene object reference + objectReference = element.objectReference; + isNormalReference = true; + } + + // Object Field + if (valid) { + if (objectReference != null) { + EditorGUI.BeginChangeCheck(); + EditorGUI.LabelField( + new Rect(rect.x + x, rect.y + rect.height + 5 - 25, rect.width - 27, h), + "Linked Object : " + (isNormalReference ? "" : "*"), + style_label); + x += 100; + Object obj = EditorGUI.ObjectField( + new Rect(rect.x + x, rect.y + rect.height + 5 - 25, rect.width - x, h), + objectReference, + typeof(Object), true); + if (EditorGUI.EndChangeCheck()) { + SetTaskObject(element, obj); + } + } else { + EditorGUI.LabelField( + new Rect(rect.x + x, rect.y + rect.height + 5 - 25, rect.width - 27, h), + "Linked Object : * (Cross-scene reference lost?)", + style_label); + } + } else { + EditorGUI.LabelField( + new Rect(rect.x + x, rect.y + rect.height + 5 - 25, rect.width - 27, h), + $"Linked Object : * (Object in scene '{scenePath}')", + style_label); } } @@ -275,19 +460,128 @@ private void DrawElement(Rect rect, int index, bool active, bool focus) { DragAndDrop.AcceptDrag(); if (DragAndDrop.objectReferences.Length > 0) { Object obj = DragAndDrop.objectReferences[0]; - Undo.RecordObject(todoList, "Changed Task Object"); - element.objectReference = obj; + SetTaskObject(element, obj); } currentEvent.Use(); } } + } + private void RecordPrefabInstancePropertyModifications(Object target) { +#if UNITY_2018_3_OR_NEWER + bool isPrefabInstance = PrefabUtility.IsPartOfPrefabInstance(target); +#else + bool isPrefabInstance = (PrefabUtility.GetPrefabType(target) == PrefabType.PrefabInstance); +#endif + if (isPrefabInstance) { + PrefabUtility.RecordPrefabInstancePropertyModifications(target); + } + } + + protected void LinkCrossSceneReference(ToDoElement element, Object obj, GameObject gameObject) { + Scene scene = gameObject.scene; + SceneAsset sceneAsset = GetSceneAsset(scene); + SceneReferencesHandler sceneReferencesHandler = SceneReferencesHandler.GetSceneReferencesHandler(scene, true); + + Undo.RecordObjects(new Object[] { target, sceneReferencesHandler }, "Changed Task Object"); + + element.objectReference = sceneAsset; + element.objectReferenceID = sceneReferencesHandler.RegisterObject(obj); + element.tempObjectReference = null; + + //Debug.Log("SceneObjectReference : " + element.objectReferenceID); + RecordPrefabInstancePropertyModifications(target); + EditorUtility.SetDirty(target); + } + + protected void LinkObjectReference(ToDoElement element, Object obj) { + Undo.RecordObject(target, "Changed Task Object"); + + element.objectReference = obj; + element.objectReferenceID = null; + element.tempObjectReference = null; + + //Debug.Log("ObjectReference"); + RecordPrefabInstancePropertyModifications(target); + EditorUtility.SetDirty(target); + } + + private void SetTaskObject(ToDoElement element, Object obj) { + if (scenePath == null) { + // ToDo is Prefab / ScriptableObject Asset + if (IsReferenceAllowed(obj, null, out GameObject gameObject)) { + // Asset + LinkObjectReference(element, obj); + } else { + // Scene Reference, Store SceneAsset in objectReference + LinkCrossSceneReference(element, obj, gameObject); + } + } else if (!IsReferenceAllowed(obj, scenePath, out GameObject gameObject)) { + // ToDo is MonoBehaviour in Scene, but obj is in different scene. Cross-scene reference + LinkCrossSceneReference(element, obj, gameObject); + } else { + // ToDo is MonoBehaviour, obj is GameObject/Component in same scene or is an Asset + LinkObjectReference(element, obj); + } + } + + private SceneAsset GetSceneAsset(Scene scene) { + return AssetDatabase.LoadAssetAtPath(scene.path); + } + + /* Unused, but might allow for object references that doesn't dirty the scene. + * Rather than registering the object with SceneReferencesHandler + * It could just save the object's path in the Hierarchy. + * However, renames/changes to that path will break the reference. + * If you want to implement this, you'd likely need to use it in the LinkCrossSceneReference method + private string GetObjectPath(GameObject gameObject, Object obj) { + if (gameObject == null) return null; + List path = new List(); + + // If component, append component type to path + Component component = obj as Component; + if (component != null) { + path.Add(":" + component.GetType()); + } + + // Get Hierarchy / Transforms from GameObject to Root + Transform t = gameObject.transform; + path.Add(t.name); + while (t.parent != null) { + t = t.parent; + path.Add(t.name + "/"); + } + + // Turn into string (e.g. "Root/Child/GameObject:ComponentType") + StringBuilder stringBuilder = new StringBuilder(); + foreach (string s in Enumerable.Reverse(path)) { + stringBuilder.Append(s); + } + return stringBuilder.ToString(); + }*/ + + protected bool IsReferenceAllowed(Object obj, string scenePath, out GameObject gameObject) { + // Get GameObject + gameObject = obj as GameObject; + if (gameObject == null) { + Component component = obj as Component; + if (component != null) { + gameObject = component.gameObject; + } + } + if (gameObject == null) return true; // Not a GameObject, would be an Asset, so always allowed. + return (gameObject.scene.path == scenePath); // Same scene = allow, Cross-scene = not allowed } + /// + /// Exports the To Do tasks list to a txt file at the specifed path. + /// Note, doesn't include object references + /// + /// private void Export(string path) { StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < todoList.list.Count; i++) { - ToDoElement element = todoList.list[i]; + for (int i = 0; i < todoList.tasks.Count; i++) { + ToDoElement element = todoList.tasks[i]; string line = ""; if (element.completed) line += "[Complete]"; line += element.text.Replace("\n", @"\n"); @@ -297,6 +591,9 @@ private void Export(string path) { File.WriteAllText(path, stringBuilder.ToString()); } + /// + /// Imports tasks from a txt file at the specified path. + /// private void Import(string path) { string[] lines = File.ReadAllLines(path); @@ -316,8 +613,8 @@ private void Import(string path) { text = text, completed = complete }; - todoList.list.Add(element); + todoList.tasks.Add(element); } } } -} +} \ No newline at end of file diff --git a/ToDo/SceneReferencesHandler.cs b/ToDo/SceneReferencesHandler.cs new file mode 100644 index 0000000..e3505e1 --- /dev/null +++ b/ToDo/SceneReferencesHandler.cs @@ -0,0 +1,102 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Cyan.ToDo { + /// + /// Stores references to objects in the scene, assigning a GUID. + /// That GUID can then be used to obtain the object, allowing for cross-scene references + /// (as well as references to scene objects for prefabs + ScriptableObject) + /// + [AddComponentMenu("Cyan/Scene References Handler", 2)] + public class SceneReferencesHandler : MonoBehaviour { + + public static bool disallowSceneReferences = false; + /* + * Set to true if you want to disable this functionality. + * Storing scene references dirties the scene, so you might want to disable it if working with collab. + * Any existing references won't be lost, but GetSceneReferencesHandler(scene) will return null. + * + * If you want to remove/break existing references for a scene, use GameObject -> Cyan.ToDo -> Remove Scene References Handler + */ + +#if UNITY_2020_1_OR_NEWER + [SerializeField] private SerializableDictionary objects = new SerializableDictionary(); +#else + [SerializeField] private SerializableDictionary_StringObject objects = new SerializableDictionary_StringObject(); +#endif + + private void Reset() { + if (gameObject.hideFlags != HideFlags.HideInHierarchy) { + Debug.LogWarning("Cyan.ToDo : Adding a SceneReferences component to a scene manually is not advised. (see console for more info)\n" + + "The ToDo system automatically adds this component when required to handle scene object references " + + "for the ScriptableObject version of the To Do list, or cross-scene references for the MonoBehaviour version. " + + "Regardless of adding the component manually, SceneReferences.GetSceneReferencesHandler will always use the hidden one, " + + "or create a hidden one if it doesn't exist."); + } + } + + /// + /// Obtains the SceneReferences component on a hidden GameObject inside the given Scene. + /// If createIfNull is true, and it doesn't exist, it will create and add it to the scene. + /// Otherwise returns null. + /// + public static SceneReferencesHandler GetSceneReferencesHandler(Scene scene, bool createIfNull = false) { + if (disallowSceneReferences) return null; + GameObject[] roots = scene.GetRootGameObjects(); + + SceneReferencesHandler sceneReferencesHandler = null; + for (int i = 0; i < roots.Length; i++) { + GameObject root = roots[i]; +#if UNITY_2019_2_OR_NEWER + if (root.TryGetComponent(out sceneReferencesHandler)) { + return sceneReferencesHandler; + } +#else + sceneReferencesHandler = root.GetComponent(); + if (sceneReferencesHandler != null) { + return sceneReferencesHandler; + } +#endif + } + + // Create Scene Reference Handler in scene + if (createIfNull) { + GameObject obj = new GameObject(); + obj.hideFlags = HideFlags.HideInHierarchy; + SceneManager.MoveGameObjectToScene(obj, scene); + obj.transform.SetSiblingIndex(0); + sceneReferencesHandler = obj.AddComponent(); + } + return sceneReferencesHandler; + } + + public string RegisterObject(Object obj) { + string key = GetKeyFromObject(obj); + if (key != null) return key; + System.Guid guid = System.Guid.NewGuid(); + string id = guid.ToString(); + objects.Add(id, obj); + return id; + } + + public Object GetObjectFromID(string id) { + if (objects.TryGetValue(id, out Object obj)) { + return obj; + } + return null; + } + + public string GetKeyFromObject(Object obj) { + foreach (KeyValuePair entry in objects) { + if (entry.Value == obj) { + return entry.Key; + } + } + return null; + } + + } + +} \ No newline at end of file diff --git a/ToDo/SceneReferencesHandler.cs.meta b/ToDo/SceneReferencesHandler.cs.meta new file mode 100644 index 0000000..75dde7a --- /dev/null +++ b/ToDo/SceneReferencesHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 893a10e8f0caf1d4aa9664661a3e455c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ToDo/SerializableDictionary.cs b/ToDo/SerializableDictionary.cs new file mode 100644 index 0000000..3c89990 --- /dev/null +++ b/ToDo/SerializableDictionary.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Cyan.ToDo { + +#if !UNITY_2020_1_OR_NEWER + // Unity can't serialise generic fields like "SerializableDictionary fieldName" (prior to 2020.1) + // But it can serialise a class that inherits a generic class, so we need this for previous versions : + [System.Serializable] + public class SerializableDictionary_StringObject : SerializableDictionary { + + } +#endif + + /// + /// A version of Dictionary which is serialisable by converting it to and from Lists + /// + [System.Serializable] + public class SerializableDictionary : Dictionary, ISerializationCallbackReceiver { + // Note : TKey and TVaue must be Serializable + + [SerializeField] private List keys = new List(); + [SerializeField] private List values = new List(); + + public void OnBeforeSerialize() { + // Convert Dictionary to Lists, so Unity can Serialize it + keys.Clear(); + values.Clear(); + foreach (KeyValuePair pair in this) { + keys.Add(pair.Key); + values.Add(pair.Value); + } + } + + public void OnAfterDeserialize() { + // Convert Lists to Dictionary + Clear(); + for (int i = 0; i < keys.Count; i++) { + Add(keys[i], values[i]); + } + } + } +} \ No newline at end of file diff --git a/ToDo/SerializableDictionary.cs.meta b/ToDo/SerializableDictionary.cs.meta new file mode 100644 index 0000000..1d8d915 --- /dev/null +++ b/ToDo/SerializableDictionary.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b56fe249d9db40048ba40c0d193e590f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ToDo/ToDo.cs b/ToDo/ToDo.cs index fa9c324..e415eca 100644 --- a/ToDo/ToDo.cs +++ b/ToDo/ToDo.cs @@ -3,32 +3,46 @@ namespace Cyan.ToDo { + /// + /// MonoBehaviour version of the To Do list + /// + [AddComponentMenu("Cyan/To Do", 1)] public class ToDo : MonoBehaviour { - public string listName = "To Do"; - public List list = new List(); + public ToDoList list = new ToDoList(); public void OnDrawGizmos() { } + } + + [System.Serializable] + public class ToDoList { + + public string listName = "To Do"; + public List tasks = new List(); + public void RemoveCompleted() { - for (int i = list.Count - 1; i >= 0; i--) { - ToDoElement element = list[i]; + for (int i = tasks.Count - 1; i >= 0; i--) { + ToDoElement element = tasks[i]; if (element.completed) { - list.RemoveAt(i); + tasks.RemoveAt(i); } } } - } [System.Serializable] public class ToDoElement { public bool completed = false; public string text = ""; - public Object objectReference; + + public Object objectReference; // (if cross-scene reference, this is the SceneAsset instead, but that only works in-editor) + public string objectReferenceID; // ID for serializing cross-scene reference (see SceneReferences) + [System.NonSerialized] public Object tempObjectReference; // actual object for cross-scene reference - public bool editing = false; + [System.NonSerialized] public bool editing = false; public ToDoElement() { } } + } diff --git a/ToDo/ToDoSO.cs b/ToDo/ToDoSO.cs new file mode 100644 index 0000000..93ce2fb --- /dev/null +++ b/ToDo/ToDoSO.cs @@ -0,0 +1,17 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Cyan.ToDo { + + /// + /// ScriptableObject version of the To Do list + /// + [CreateAssetMenu(fileName = "To Do", menuName = "To Do List (ScriptableObject)", order = 1)] + public class ToDoSO : ScriptableObject { + + public ToDoList list = new ToDoList(); + + } + +} \ No newline at end of file diff --git a/ToDo/ToDoSO.cs.meta b/ToDo/ToDoSO.cs.meta new file mode 100644 index 0000000..50b5bd4 --- /dev/null +++ b/ToDo/ToDoSO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3516055b53ac5ba4180637ebba7efd4d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 992609eb7d8267442b2a7b5ce0291eef, type: 3} + userData: + assetBundleName: + assetBundleVariant: