/// ---------------------------------------------
/// 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; }
}
}