/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Editor.Controls { using System; using System.Collections.Generic; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine; /// /// Uses Unity's TreeView class to create a single column tree view that does not have any children. The elements can be reordered and searched. /// public class FlatTreeView : TreeView where T : TreeModal { private const string c_GenericDragID = "ModalDragging"; private TreeModal m_TreeModal; private List m_Rows = new List(); private TreeViewItem m_Root; private Action m_TreeChange; public TreeModal TreeModal { get { return m_TreeModal; } set { m_TreeModal = value; Reload(); } } public Action TreeChange { get { return m_TreeChange; } set { m_TreeChange = value; } } /// /// Constructor for FlatTreeView. /// /// The TreeView's state. /// The TreeView's data. public FlatTreeView(TreeViewState state, TreeModal modal) : base (state) { showBorder = true; m_TreeModal = modal; Reload(); } /// /// Creates the root element of the TreeView. /// /// The TreeView's root element. protected override TreeViewItem BuildRoot() { m_Root = new TreeViewItem(0, -1, "Root"); return m_Root; } /// /// Creates all of the TreeView rows. /// /// The root of the tree. /// A list of all of the TreeView rows. protected override IList BuildRows(TreeViewItem root) { m_Rows.Clear(); if (!string.IsNullOrEmpty(searchString) && root.children != null) { // Not all of the rows are shown while searching. Search(root, m_Rows); } else { // Show all of the rows. var rowCount = m_TreeModal.GetRowCount(); for (int i = 0; i < rowCount; ++i) { m_Rows.Add(new TreeViewItem(i, -1)); } SetupParentsAndChildrenFromDepths(root, m_Rows); // If the children list was null then the tree hasn't been initialized yet. At this point the tree would have been initialized so // perform a serach if necessary while the list is initialized. if (!string.IsNullOrEmpty(searchString)) { m_Rows.Clear(); Search(root, m_Rows); } } return m_Rows; } /// /// Searches the tree for the searchString. /// /// The root of the tree. /// Any found rows. private void Search(TreeViewItem root, List result) { for (int i = 0; i < root.children.Count; ++i) { if (m_TreeModal.MatchesSearch(root.children[i].id, searchString)) { result.Add(root.children[i]); } } } /// /// Returns a custom height for the row. /// /// The row to get the custom height of. /// The item to get the custom height of. /// The custom height for the row. protected override float GetCustomRowHeight(int row, TreeViewItem item) { var height = m_TreeModal.GetRowHeight(item, state); // -1 indicates the model doesn't supply the height. if (height != -1) { return height; } return base.GetCustomRowHeight(row, item); } /// /// Draws the row with the specified arguments. /// /// The row to draw. protected override void RowGUI(RowGUIArgs args) { var rowRect = args.rowRect; rowRect.x = GetContentIndent(args.item); m_TreeModal.RowGUI(rowRect, args.item, state); } /// /// Called when the TreeView changes selection. /// /// The new ids being selected. protected override void SelectionChanged(IList selectedIds) { base.SelectionChanged(selectedIds); RefreshCustomRowHeights(); Repaint(); // Notify those interested that there was a change - this allows the tree to be serialized. if (m_TreeChange != null) { m_TreeChange(); } } /// /// Can the TreeView have multiple selections? /// /// Can this item be part of a multiselection? /// True if the TreeView can have multiple selections. protected override bool CanMultiSelect(TreeViewItem item) { return false; } /// /// Can the row be dragged? /// /// The row that is trying to be dragged. /// True if the row can be dragged. protected override bool CanStartDrag(CanStartDragArgs args) { return !hasSearch; } /// /// Prepares the row for a drag. /// /// The row that is being dragged. protected override void SetupDragAndDrop(SetupDragAndDropArgs args) { DragAndDrop.PrepareStartDrag(); var draggedRows = new List(); var rows = GetRows(); // Convert the row IDs to row items. for (int i = 0; i < rows.Count; ++i) { for (int j = 0; j < args.draggedItemIDs.Count; ++j) { if (rows[i].id == args.draggedItemIDs[j]) { draggedRows.Add(rows[i]); break; } } } // Start the drag. DragAndDrop.SetGenericData(c_GenericDragID, draggedRows); DragAndDrop.objectReferences = new UnityEngine.Object[] { }; // Required for dragging to work. DragAndDrop.StartDrag("Drag"); } /// /// The row is being dragged - handle the dragging. /// /// The row that is being dragged. /// The status of the drag. protected override DragAndDropVisualMode HandleDragAndDrop(DragAndDropArgs args) { // Return early if the dragging is occurring from a different window. var draggedRows = DragAndDrop.GetGenericData(c_GenericDragID) as List; if (draggedRows == null) { return DragAndDropVisualMode.None; } switch (args.dragAndDropPosition) { case DragAndDropPosition.UponItem: // Dropping on top of other items is not allowed in a flat tree. return DragAndDropVisualMode.None; case DragAndDropPosition.BetweenItems: // The item can be dropped in between other items. { if (args.performDrop) { // Do the drop. OnDropDraggedElementsAtIndex(draggedRows, args.insertAtIndex == -1 ? 0 : args.insertAtIndex); } return DragAndDropVisualMode.Move; } case DragAndDropPosition.OutsideItems: // The item will be dropped to the last row if it is outside the tree. { if (args.performDrop) { // Do the drop. OnDropDraggedElementsAtIndex(draggedRows, m_Root.children.Count - 1); } return DragAndDropVisualMode.Move; } default: Debug.LogError("Unhandled enum " + args.dragAndDropPosition); return DragAndDropVisualMode.None; } } /// /// The rows specified have been dropped at the insert index. /// /// The rows that have been dropped. /// The index to insert the dropped rows at. public virtual void OnDropDraggedElementsAtIndex(List draggedRows, int insertIndex) { // Convert the rows indicies to row ids. var draggedElements = new List(); for (int i = 0; i < draggedRows.Count; ++i) { draggedElements.Add(draggedRows[i].id); } // Let the model to the drop. var insertIDs = m_TreeModal.MoveRows(draggedElements, insertIndex); // Update the selection. SetSelection(insertIDs, TreeViewSelectionOptions.RevealAndFrame); RefreshCustomRowHeights(); // Notify those interested that the tree has changed. if (m_TreeChange != null) { m_TreeChange(); } } } /// /// The TreeModal class acts as the data source for the tree. /// [Serializable] public abstract class TreeModal { protected Action m_BeforeModalChange; protected Action m_AfterModalChange; public Action BeforeModalChange { get { return m_BeforeModalChange; } set { m_BeforeModalChange = value; } } public Action AfterModalChange { get { return m_AfterModalChange; } set { m_AfterModalChange = value; } } /// /// Returns the number of rows in the tree. /// /// The number of rows in the tree. public abstract int GetRowCount(); /// /// Returns the height of the row. /// /// The item that occupies the row with the requested height. /// The state of the tree. /// The height of the row. public virtual float GetRowHeight(TreeViewItem item, TreeViewState state) { return -1; } /// /// Draws the GUI for the row. /// /// The rect of the row being drawn. /// The item that occupies the row which is being drawn. /// The state of the tree. public abstract void RowGUI(Rect rowRect, TreeViewItem item, TreeViewState state); /// /// Moves the rows to the specified index. /// /// The rows being moved. /// The index to insert the rows at. /// An updated list of row ids. public abstract List MoveRows(List rows, int insertIndex); /// /// Does the specified row id match the search? /// /// The id of the row. /// The string value of the search. /// True if the row matches the search string. public virtual bool MatchesSearch(int id, string searchString) { return false; } } }