// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
// FlexiMotion // https://kybernetik.com.au/flexi-motion // Copyright 2023 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
namespace Animancer.Editor
//namespace FlexiMotion.Editor
{
/// An object that provides data to a .
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ITransformTreeViewSource
/// https://kybernetik.com.au/flexi-motion/api/FlexiMotion.Editor/ITransformTreeViewSource
public interface ITransformTreeViewSource
{
/************************************************************************************************************************/
/// The object at the top of the target hierarchy.
Transform Root { get; }
/// The objects to show in the view.
IList Transforms { get; }
/// Adds the items to be displayed in the view.
void AddItems(ref int id, TreeViewItem root);
/// Adds an item for the `transform` to be displayed in the view.
TreeViewItem AddItem(ref int id, TreeViewItem parent, Transform transform);
/// Called before a row is drawn.
void BeforeRowGUI(Rect area, TreeViewItem item);
/// Draws a cell in the .
void DrawCellGUI(Rect area, int column, int row, TreeViewItem item, ref bool isSelectionClick);
/************************************************************************************************************************/
}
/// A for displaying s alongside other data.
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TransformTreeView
/// https://kybernetik.com.au/flexi-motion/api/FlexiMotion.Editor/TransformTreeView
public class TransformTreeView : TreeView, IDisposable
{
/************************************************************************************************************************/
/// The ID of the root item.
public const int RootID = 0;
/// The object which defines what to show in this view.
public readonly ITransformTreeViewSource Source;
/// The field used to filter this view.
public readonly SearchField Search = new();
/// The of each row in this view.
public readonly List Transforms = new();
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// Creates a new .
public TransformTreeView(
TreeViewState state,
MultiColumnHeader header,
ITransformTreeViewSource source)
: base(state, header)
{
Source = source;
Selection.selectionChanged += OnObjectSelectionChanged;
}
/************************************************************************************************************************/
/// Cleans up this view.
public void Dispose()
{
Selection.selectionChanged -= OnObjectSelectionChanged;
}
/************************************************************************************************************************/
///
protected override TreeViewItem BuildRoot()
{
Transforms.Clear();
var id = RootID;
var root = CreateItem(ref id, -1, "");
Transforms.Add(null);
Source.AddItems(ref id, root);
return root;
}
/************************************************************************************************************************/
/// Adds a new item for the `transform` as a child of the `parent` and increments the `id`.
public virtual TreeViewItem AddItem(ref int id, TreeViewItem parent, Transform transform)
{
Transforms.Add(transform);
var item = CreateItem(ref id, parent.depth + 1, transform.name);
parent.AddChild(item);
return item;
}
/// Adds a new item for each child of the `transform` recursively.
public void AddItemRecursive(ref int id, TreeViewItem parent, Transform transform)
{
parent = Source.AddItem(ref id, parent, transform);
foreach (Transform child in transform)
AddItemRecursive(ref id, parent, child);
}
/************************************************************************************************************************/
/// Creates a new and increments the `id`.
public static TreeViewItem CreateItem(ref int id, int depth, string displayName)
=> new()
{
id = id++,
depth = depth,
displayName = displayName,
};
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region GUI
/************************************************************************************************************************/
/// Draws the for filtering this view.
public void DrawSearchField(Rect area)
{
searchString = Search.OnGUI(area, searchString);
}
/************************************************************************************************************************/
///
protected override void RowGUI(RowGUIArgs args)
{
Source.BeforeRowGUI(args.rowRect, args.item);
var currentEvent = Event.current;
var isClick =
currentEvent.type == EventType.MouseDown &&
args.rowRect.Contains(currentEvent.mousePosition);
var visibleColumnCount = args.GetNumVisibleColumns();
for (int i = 0; i < visibleColumnCount; ++i)
{
var area = args.GetCellRect(i);
if (i == 0)
area.xMin += (args.item.depth + 1) * depthIndentWidth;
Source.DrawCellGUI(area, args.GetColumn(i), args.row, args.item, ref isClick);
}
if (isClick && currentEvent.type == EventType.Used)
SelectionClick(args.item, false);
}
/************************************************************************************************************************/
private static readonly List
SelectedObjects = new();
/// Called whenever the selected rows change.
public event Action> OnSelectionChanged;
///
protected override void SelectionChanged(IList selectedIds)
{
base.SelectionChanged(selectedIds);
SelectedObjects.Clear();
for (int i = 0; i < selectedIds.Count; i++)
if (Transforms.TryGetObject(selectedIds[i], out var transform))
SelectedObjects.Add(transform.gameObject);
Selection.objects = SelectedObjects.ToArray();
OnSelectionChanged?.Invoke(selectedIds);
}
/************************************************************************************************************************/
private static readonly List
SelectedIDs = new();
/// Called whenever the change.
public void OnObjectSelectionChanged()
{
var selectedObjects = Selection.objects;
SelectedIDs.Clear();
SelectedIDs.AddRange(GetSelection());
// Remove IDs that aren't in the Selection.
for (int i = SelectedIDs.Count - 1; i >= 0; i--)
{
if (!Transforms.TryGetObject(SelectedIDs[i], out var transform) ||
Array.IndexOf(selectedObjects, transform.gameObject) < 0)
{
SelectedIDs.RemoveAt(i);
}
}
// If no selected rows correspond to a selected object, add all rows of that object.
foreach (var selected in selectedObjects)
{
if (!AnimancerUtilities.TryGetTransform(selected, out var selectedTransform) ||
IsAlreadySelected(selectedTransform))
continue;
var index = Transforms.IndexOf(selectedTransform);
if (index >= 0)
{
SelectedIDs.Add(index);
while (Transforms.TryGetObject(++index, out var transform) &&
transform == selectedTransform)
SelectedIDs.Add(index);
}
}
SetSelection(SelectedIDs);
}
private bool IsAlreadySelected(Transform transform)
{
for (int i = 0; i < SelectedIDs.Count; i++)
{
if (!Transforms.TryGetObject(SelectedIDs[i], out var selectedTransform))
continue;
if (selectedTransform == transform)
return true;
}
return false;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif