//#define runtest using System; using System.Collections.Generic; using System.Linq; //If this fails, its because you have an assembly definition file in your project that is not set to allow unit tests //Solution, either allow assembly definition to run test, of comment out the top line #if runtest using NUnit.Framework; #endif namespace HeurekaGames.AssetHunterPRO.BaseTreeviewImpl { // The TreeModel is a utility class working on a list of serializable TreeElements where the order and the depth of each TreeElement define // the tree structure. Note that the TreeModel itself is not serializable (in Unity we are currently limited to serializing lists/arrays) but the // input list is. // The tree representation (parent and children references) are then build internally using TreeElementUtility.ListToTree (using depth // values of the elements). // The first element of the input list is required to have depth == -1 (the hiddenroot) and the rest to have // depth >= 0 (otherwise an exception will be thrown) public class TreeModel where T : TreeElement { IList m_Data; T m_Root; int m_MaxID; public T root { get { return m_Root; } set { m_Root = value; } } public event Action modelChanged; public int numberOfDataElements { get { return m_Data.Count; } } public TreeModel(IList data) { SetData(data); } public T Find(int id) { return m_Data.FirstOrDefault(element => element.id == id); } public void SetData(IList data) { Init(data); } void Init(IList data) { if (data == null) throw new ArgumentNullException("data", "Input data is null. Ensure input is a non-null list."); m_Data = data; if (m_Data.Count > 0) m_Root = TreeElementUtility.ListToTree(data); m_MaxID = m_Data.Max(e => e.id); } public int GenerateUniqueID() { return ++m_MaxID; } public IList GetAncestors(int id) { var parents = new List(); TreeElement T = Find(id); if (T != null) { while (T.parent != null) { parents.Add(T.parent.id); T = T.parent; } } return parents; } public IList GetDescendantsThatHaveChildren(int id) { T searchFromThis = Find(id); if (searchFromThis != null) { return GetParentsBelowStackBased(searchFromThis); } return new List(); } IList GetParentsBelowStackBased(TreeElement searchFromThis) { Stack stack = new Stack(); stack.Push(searchFromThis); var parentsBelow = new List(); while (stack.Count > 0) { TreeElement current = stack.Pop(); if (current.hasChildren) { parentsBelow.Add(current.id); foreach (var T in current.children) { stack.Push(T); } } } return parentsBelow; } public void RemoveElements(IList elementIDs) { IList elements = m_Data.Where(element => elementIDs.Contains(element.id)).ToArray(); RemoveElements(elements); } public void RemoveElements(IList elements) { foreach (var element in elements) if (element == m_Root) throw new ArgumentException("It is not allowed to remove the root element"); var commonAncestors = TreeElementUtility.FindCommonAncestorsWithinList(elements); foreach (var element in commonAncestors) { element.parent.children.Remove(element); element.parent = null; } TreeElementUtility.TreeToList(m_Root, m_Data); Changed(); } public void AddElements(IList elements, TreeElement parent, int insertPosition) { if (elements == null) throw new ArgumentNullException("elements", "elements is null"); if (elements.Count == 0) throw new ArgumentNullException("elements", "elements Count is 0: nothing to add"); if (parent == null) throw new ArgumentNullException("parent", "parent is null"); if (parent.children == null) parent.children = new List(); parent.children.InsertRange(insertPosition, elements.Cast()); foreach (var element in elements) { element.parent = parent; element.depth = parent.depth + 1; TreeElementUtility.UpdateDepthValues(element); } TreeElementUtility.TreeToList(m_Root, m_Data); Changed(); } public void AddRoot(T root) { if (root == null) throw new ArgumentNullException("root", "root is null"); if (m_Data == null) throw new InvalidOperationException("Internal Error: data list is null"); if (m_Data.Count != 0) throw new InvalidOperationException("AddRoot is only allowed on empty data list"); root.id = GenerateUniqueID(); root.depth = -1; m_Data.Add(root); } public void AddElement(T element, TreeElement parent, int insertPosition) { if (element == null) throw new ArgumentNullException("element", "element is null"); if (parent == null) throw new ArgumentNullException("parent", "parent is null"); if (parent.children == null) parent.children = new List(); parent.children.Insert(insertPosition, element); element.parent = parent; TreeElementUtility.UpdateDepthValues(parent); TreeElementUtility.TreeToList(m_Root, m_Data); Changed(); } public void MoveElements(TreeElement parentElement, int insertionIndex, List elements) { if (insertionIndex < 0) throw new ArgumentException("Invalid input: insertionIndex is -1, client needs to decide what index elements should be reparented at"); // Invalid reparenting input if (parentElement == null) return; // We are moving items so we adjust the insertion index to accomodate that any items above the insertion index is removed before inserting if (insertionIndex > 0) insertionIndex -= parentElement.children.GetRange(0, insertionIndex).Count(elements.Contains); // Remove draggedItems from their parents foreach (var draggedItem in elements) { draggedItem.parent.children.Remove(draggedItem); // remove from old parent draggedItem.parent = parentElement; // set new parent } if (parentElement.children == null) parentElement.children = new List(); // Insert dragged items under new parent parentElement.children.InsertRange(insertionIndex, elements); TreeElementUtility.UpdateDepthValues(root); TreeElementUtility.TreeToList(m_Root, m_Data); Changed(); } void Changed() { if (modelChanged != null) modelChanged(); } } #if runtest #region Tests class TreeModelTests { [Test] public static void TestTreeModelCanAddElements() { var root = new TreeElement { Name = "Root", depth = -1 }; var listOfElements = new List(); listOfElements.Add(root); var model = new TreeModel(listOfElements); model.AddElement(new TreeElement { Name = "Element" }, root, 0); model.AddElement(new TreeElement { Name = "Element " + root.children.Count }, root, 0); model.AddElement(new TreeElement { Name = "Element " + root.children.Count }, root, 0); model.AddElement(new TreeElement { Name = "Sub Element" }, root.children[1], 0); // Assert order is correct string[] namesInCorrectOrder = { "Root", "Element 2", "Element 1", "Sub Element", "Element" }; Assert.AreEqual(namesInCorrectOrder.Length, listOfElements.Count, "Result count does not match"); for (int i = 0; i < namesInCorrectOrder.Length; ++i) Assert.AreEqual(namesInCorrectOrder[i], listOfElements[i].Name); // Assert depths are valid TreeElementUtility.ValidateDepthValues(listOfElements); } [Test] public static void TestTreeModelCanRemoveElements() { var root = new TreeElement { Name = "Root", depth = -1 }; var listOfElements = new List(); listOfElements.Add(root); var model = new TreeModel(listOfElements); model.AddElement(new TreeElement { Name = "Element" }, root, 0); model.AddElement(new TreeElement { Name = "Element " + root.children.Count }, root, 0); model.AddElement(new TreeElement { Name = "Element " + root.children.Count }, root, 0); model.AddElement(new TreeElement { Name = "Sub Element" }, root.children[1], 0); model.RemoveElements(new[] { root.children[1].children[0], root.children[1] }); // Assert order is correct string[] namesInCorrectOrder = { "Root", "Element 2", "Element" }; Assert.AreEqual(namesInCorrectOrder.Length, listOfElements.Count, "Result count does not match"); for (int i = 0; i < namesInCorrectOrder.Length; ++i) Assert.AreEqual(namesInCorrectOrder[i], listOfElements[i].Name); // Assert depths are valid TreeElementUtility.ValidateDepthValues(listOfElements); } } #endregion #endif }