using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System.Linq.Expressions;
namespace Unity.Cinemachine.Editor
{
///
/// Collection of tools and helpers for drawing inspectors
///
[InitializeOnLoad]
static partial class InspectorUtility
{
///
/// Callback that happens whenever something undoable happens, either with
/// objects or with selection. This is a good way to track user activity.
///
public static EditorApplication.CallbackFunction UserDidSomething;
static InspectorUtility()
{
ObjectChangeEvents.changesPublished -= OnUserDidSomethingStream;
ObjectChangeEvents.changesPublished += OnUserDidSomethingStream;
Selection.selectionChanged -= OnUserDidSomething;
Selection.selectionChanged += OnUserDidSomething;
static void OnUserDidSomething() => UserDidSomething?.Invoke();
static void OnUserDidSomethingStream(ref ObjectChangeEventStream stream) => UserDidSomething?.Invoke();
}
///
/// Add to a list all assets of a given type found in a given location
///
/// The asset type to look for
/// The list to add found assets to
/// The location in which to look. Path is relative to package root.
public static void AddAssetsFromPackageSubDirectory(
Type type, List assets, string path)
{
try
{
path = CinemachineCore.kPackageRoot + "/" + path;
var info = new DirectoryInfo(path);
path += "/";
var fileInfo = info.GetFiles();
for (int i = 0; i < fileInfo.Length; ++i)
{
var file = fileInfo[i];
if (file.Extension != ".asset")
continue;
var name = path + file.Name;
var a = AssetDatabase.LoadAssetAtPath(name, type) as ScriptableObject;
if (a != null)
assets.Add(a);
}
}
catch
{
}
}
///
/// Normalize a curve so that each of X and Y axes ranges from 0 to 1
///
/// Curve to normalize
/// The normalized curve
public static AnimationCurve NormalizeCurve(AnimationCurve curve)
{
return RuntimeUtility.NormalizeCurve(curve, true, true);
}
///
/// Remove the "Cinemachine" prefix, then call the standard Unity Nicify.
///
/// The name to nicify
/// The nicified name
public static string NicifyClassName(string name)
{
if (name.StartsWith("Cinemachine"))
name = name.Substring(11); // Trim the prefix
return ObjectNames.NicifyVariableName(name);
}
///
/// Remove the "Cinemachine" prefix, then call the standard Unity Nicify,
/// and add (Deprecated) to types with Obsolete attributes.
///
/// The type to nicify as a string
/// The nicified name
public static string NicifyClassName(Type type)
{
var name = NicifyClassName(type.Name);
if (type.GetCustomAttribute() != null)
name += " (Deprecated)";
return name;
}
private static int m_lastRepaintFrame;
///
/// Force a repaint of the Game View
///
/// Like it says
public static void RepaintGameView(UnityEngine.Object unused = null)
{
if (m_lastRepaintFrame == Time.frameCount)
return;
m_lastRepaintFrame = Time.frameCount;
EditorApplication.QueuePlayerLoopUpdate();
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
}
static Dictionary s_AssignableTypes = new ();
public const string s_NoneString = "(none)";
public static string GetAssignableBehaviourNames(Type inputType)
{
if (inputType == null)
return "(none)";
if (!s_AssignableTypes.ContainsKey(inputType))
{
var allSources = ReflectionHelpers.GetTypesDerivedFrom(inputType,
(t) => !t.IsAbstract && typeof(MonoBehaviour).IsAssignableFrom(t)
&& t.GetCustomAttribute() == null);
var s = string.Empty;
var iter = allSources.GetEnumerator();
while (iter.MoveNext())
{
var sep = (s.Length == 0) ? string.Empty : ", ";
s += sep + iter.Current.Name;
}
if (s.Length == 0)
s = s_NoneString;
s_AssignableTypes[inputType] = s;
}
return s_AssignableTypes[inputType];
}
/// Aligns fields created by UI toolkit the unity inspector standard way.
public static string AlignFieldClassName => BaseField.alignedFieldUssClassName;
public static float SingleLineHeight => EditorGUIUtility.singleLineHeight;
///
/// Convenience extension for UserDidSomething callbacks, making it easier to use lambdas.
/// Cleans itself up when the owner is undisplayed. Works in inspectors and PropertyDrawers.
///
public static void TrackAnyUserActivity(
this VisualElement owner, EditorApplication.CallbackFunction callback)
{
owner.RegisterCallback(_ =>
{
UserDidSomething += callback;
owner.OnInitialGeometry(callback);
owner.RegisterCallback(_ => UserDidSomething -= callback);
});
}
///
/// Convenience extension for EditorApplication.update callbacks, making it easier to use lambdas.
/// Cleans itself up when the owner is undisplayed. Works in inspectors and PropertyDrawers.
///
public static void ContinuousUpdate(
this VisualElement owner, EditorApplication.CallbackFunction callback)
{
owner.RegisterCallback(_ =>
{
owner.OnInitialGeometry(callback);
EditorApplication.update += callback;
owner.RegisterCallback(_ => EditorApplication.update -= callback);
});
}
///
/// Convenience extension to get a callback after initial geometry creation, making it easier to use lambdas.
/// Callback will only be called once. Works in inspectors and PropertyDrawers.
///
public static void OnInitialGeometry(
this VisualElement owner, EditorApplication.CallbackFunction callback)
{
owner.RegisterCallback(OnGeometryChanged);
void OnGeometryChanged(GeometryChangedEvent _)
{
owner.UnregisterCallback(OnGeometryChanged); // call only once
callback();
}
}
///
/// Convenience extension to track a property value change plus an initial callback at creation time.
/// This simplifies logic for the caller, allowing use of lambda callback.
///
public static void TrackPropertyWithInitialCallback(
this VisualElement owner, SerializedProperty property, Action callback)
{
owner.OnInitialGeometry(() => callback(property));
owner.TrackPropertyValue(property, callback);
}
/// Control the visibility of a widget
/// The widget
/// Whether it should be visible
public static void SetVisible(this VisualElement e, bool show)
=> e.style.display = show ? StyleKeyword.Null : DisplayStyle.None;
/// Is the widgte visible?
/// The widget
/// True if visible
public static bool IsVisible(this VisualElement e) => e.style.display != DisplayStyle.None;
/// Convenience method: calls e.Add(child) and returns child.///
public static T AddChild(this VisualElement e, T child) where T : VisualElement
{
e.Add(child);
return child;
}
///
/// Tries to set isDelayed of a FloatField, IntField, or TextField child, if it exists.
///
/// Parent widget
/// name of child (or null)
public static void SafeSetIsDelayed(this VisualElement e, string name = null)
{
var f = e.Q(name);
if (f != null)
f.isDelayed = true;
var i = e.Q(name);
if (i != null)
i.isDelayed = true;
var t = e.Q(name);
if (t != null)
t.isDelayed = true;
}
///
/// Draw a bold header in the inspector - hack to get around missing UITK functionality
///
/// Container in which to put the header
/// The text of the header
/// optional tooltip for the header
public static void AddHeader(this VisualElement ux, string text, string tooltip = "")
{
ux.AddChild(new Label()
{
text = $"{text}",
tooltip = tooltip,
focusable = false,
style =
{
//height = SingleLineHeight,
marginLeft = 3, // GML TODO: remove hardcoded margin
marginTop = SingleLineHeight / 2,
marginBottom = EditorGUIUtility.standardVerticalSpacing / 2,
}
});
}
///
/// Create a space between inspector sections
///
/// Container in which to add the space
public static void AddSpace(this VisualElement ux)
{
ux.Add(new VisualElement { style = { height = SingleLineHeight / 2 }});
}
///
/// Add a property dragger to a float or int label, so that dragging it changes the property value.
///
public static void AddDelayedFriendlyPropertyDragger(
this Label label, SerializedProperty p, VisualElement field,
Action OnDraggerCreated = null)
{
if (p.propertyType == SerializedPropertyType.Float || p.propertyType == SerializedPropertyType.Integer)
{
label.AddToClassList("unity-base-field__label--with-dragger");
label.OnInitialGeometry(() =>
{
if (p.propertyType == SerializedPropertyType.Float)
{
var dragger = new DelayedFriendlyFieldDragger(field.Q());
dragger.SetDragZone(label);
OnDraggerCreated?.Invoke(dragger);
}
else if (p.propertyType == SerializedPropertyType.Integer)
{
var dragger = new DelayedFriendlyFieldDragger(field.Q());
dragger.SetDragZone(label);
OnDraggerCreated?.Invoke(dragger);
}
});
}
}
public static VisualElement CreateDraggableField(Expression> exp, Label label, out IDelayedFriendlyDragger dragger)
{
var bindingPath = SerializedPropertyHelper.PropertyName(exp);
var tooltip = SerializedPropertyHelper.PropertyTooltip(exp);
return CreateDraggableField(SerializedPropertyHelper.PropertyType(exp), bindingPath, tooltip, label, out dragger);
}
public static VisualElement CreateDraggableField(Type type, string bindingPath, string tooltip, Label label, out IDelayedFriendlyDragger dragger)
{
VisualElement field;
label.AddToClassList("unity-base-field__label--with-dragger");
label.tooltip = tooltip;
label.style.alignSelf = Align.Center;
if (type == typeof(float))
{
field = new FloatField { bindingPath = bindingPath, tooltip = tooltip };
dragger = new DelayedFriendlyFieldDragger((FloatField)field);
}
else if (type == typeof(int))
{
field = new IntegerField { bindingPath = bindingPath, tooltip = tooltip };
dragger = new DelayedFriendlyFieldDragger((IntegerField)field);
}
else
{
field = new PropertyField(null, "") { bindingPath = bindingPath, tooltip = tooltip };
dragger = null;
}
var d = dragger as BaseFieldMouseDragger;
d?.SetDragZone(label);
return field;
}
/// A small warning sybmol, suitable for embedding in an inspector row
/// The tooltip text
/// The little picture: error, warning, or info
public static Label MiniHelpIcon(string tooltip, HelpBoxMessageType iconType = HelpBoxMessageType.Warning)
{
string icon = iconType switch
{
HelpBoxMessageType.Warning => "console.warnicon.sml",
HelpBoxMessageType.Error => "console.erroricon.sml",
_ => "console.infoicon.sml",
};
return new Label
{
tooltip = tooltip,
style =
{
flexGrow = 0,
flexBasis = SingleLineHeight,
backgroundImage = (StyleBackground)EditorGUIUtility.IconContent(icon).image,
width = SingleLineHeight, height = SingleLineHeight,
alignSelf = Align.Center
}
};
}
/// A small popup context menu, suitable for embedding in an inspector row
/// The tooltip text
/// The context menu to show when the button is pressed
public static Button MiniPopupButton(string tooltip = null, ContextualMenuManipulator contextMenu = null)
{
var button = new Button { tooltip = tooltip, style =
{
backgroundImage = (StyleBackground)EditorGUIUtility.IconContent("_Popup").image,
width = SingleLineHeight, height = SingleLineHeight,
alignSelf = Align.Center,
paddingLeft = 1, paddingRight = 1, marginRight = 0
}};
if (contextMenu != null)
{
contextMenu.activators.Clear();
contextMenu.activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
button.AddManipulator(contextMenu);
}
return button;
}
/// A small dropdown context menu, suitable for embedding in an inspector row
/// The tooltip text
/// The context menu to show when the button is pressed
public static Button MiniDropdownButton(string tooltip = null, ContextualMenuManipulator contextMenu = null)
{
var button = new Button { tooltip = tooltip, style =
{
backgroundImage = (StyleBackground)EditorGUIUtility.IconContent("dropdown").image,
width = SingleLineHeight, height = SingleLineHeight,
alignSelf = Align.Center,
paddingRight = 0, marginRight = 0
}};
if (contextMenu != null)
{
contextMenu.activators.Clear();
contextMenu.activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
button.AddManipulator(contextMenu);
}
return button;
}
///
/// This is an inspector container with 2 side-by-side rows. The Left row's width is
/// locked to the inspector field label size, for proper alignment.
///
public class LeftRightRow : VisualElement
{
public VisualElement Left;
public VisualElement Right;
///
/// Set this to offset the Left/Right division from the inspector's Label/Content line
///
public float DivisionOffset = 0;
///
/// Set this to zero the left margin, useful for foldouts that control the margin themselves.
///
public bool KillLeftMargin;
VisualElement Row;
public LeftRightRow(VisualElement left = null, VisualElement right = null)
{
// This is to peek at the resolved label width
Add(new AlignFieldSizer { OnLabelWidthChanged = (w) =>
{
if (KillLeftMargin)
Row.style.marginLeft = 0;
Left.style.width = w + DivisionOffset;
}});
// Actual contents will live in this row
Row = AddChild(this, new VisualElement { style = { marginLeft = 3, flexDirection = FlexDirection.Row }});
left ??= new VisualElement();
Left = Row.AddChild(left);
Left.style.flexDirection = FlexDirection.Row;
Left.style.flexGrow = 0;
right ??= new VisualElement();
Right = Row.AddChild(right);
Right.style.flexDirection = FlexDirection.Row;
Right.style.flexGrow = 1;
}
// This is a hacky thing to create custom inspector rows with labels that are the correct size
class AlignFieldSizer : BaseField // bool is just a dummy because it has to be something
{
public Action OnLabelWidthChanged;
public AlignFieldSizer() : base (" ", new VisualElement())
{
focusable = false;
style.flexDirection = FlexDirection.Row;
style.flexGrow = 1;
style.height = 0;
style.marginTop = -EditorGUIUtility.standardVerticalSpacing;
AddToClassList(AlignFieldClassName);
labelElement.RegisterCallback((_)
=> OnLabelWidthChanged?.Invoke(labelElement.resolvedStyle.width));
}
}
}
///
/// This creates a row with a properly-sized label in front of it.
/// The label's width is locked to the inspector field label size, for proper alignment.
///
public class LabeledRow : LeftRightRow
{
public Label Label { get; private set; }
public VisualElement Contents { get; private set; }
public LabeledRow(string label, string tooltip = "", VisualElement contents = null)
: base(new Label(label) { tooltip = tooltip, style = { alignSelf = Align.Center, flexGrow = 1 }}, contents)
{
Label = Left as Label;
Contents = Right;
Contents.tooltip = tooltip;
}
}
///
/// A row containing a property field. Suitable for adding widgets next to the property field.
///
public static LabeledRow PropertyRow(
SerializedProperty property, out PropertyField propertyField, string label = null)
{
var row = new LabeledRow(label ?? property.displayName, property.tooltip);
row.Contents.style.marginLeft = -2;
propertyField = row.Contents.AddChild(new PropertyField(property, "")
{ style = { flexGrow = 1, flexBasis = SingleLineHeight * 5 }});
AddDelayedFriendlyPropertyDragger(row.Label, property, propertyField, (d) => d.CancelDelayedWhenDragging = true);
return row;
}
///
/// A property field with a minimally-sized label that does not respect inspector sizing.
/// Suitable for embedding in a row within the right-hand side of the inspector.
///
public class CompactPropertyField : VisualElement
{
public Label Label;
public PropertyField Field;
public CompactPropertyField(SerializedProperty property) : this(property, property.displayName) {}
public CompactPropertyField(SerializedProperty property, string label, float minLabelWidth = 0)
{
style.flexDirection = FlexDirection.Row;
if (!string.IsNullOrEmpty(label))
Label = AddChild(this, new Label(label)
{ tooltip = property?.tooltip, style = { alignSelf = Align.Center, minWidth = minLabelWidth }});
Field = AddChild(this, new PropertyField(property, "") { style = { flexGrow = 1, flexBasis = 50 } });
Field.style.marginLeft = Field.style.marginLeft.value.value - 1;
if (Label != null && property != null)
AddDelayedFriendlyPropertyDragger(Label, property, Field, (d) => d.CancelDelayedWhenDragging = true);
}
}
/// A foldout that displays an overlay in the right-hand column when closed.
/// The overlay can optionally have a label of its own (use with caution).
public class FoldoutWithOverlay : VisualElement
{
public readonly Foldout OpenFoldout;
public readonly Foldout ClosedFoldout;
public readonly VisualElement Overlay;
public readonly Label OverlayLabel;
public FoldoutWithOverlay(Foldout foldout, VisualElement overlay, Label overlayLabel)
{
OpenFoldout = foldout;
Overlay = overlay;
OverlayLabel = overlayLabel;
Add(foldout);
// There are 2 modes for this element: foldout closed and foldout open.
// When closed, we cheat the layout system, and to implement this we do a switcheroo
var closedContainer = AddChild(this, new LeftRightRow() { KillLeftMargin = true, style = { flexGrow = 1 }});
var closedFoldout = new Foldout { text = foldout.text, tooltip = foldout.tooltip, value = false };
ClosedFoldout = closedFoldout;
ClosedFoldout = closedContainer.Left.AddChild(ClosedFoldout);
if (overlayLabel != null)
closedContainer.Right.Add(overlayLabel);
closedContainer.Right.Add(overlay);
// Outdent the label
if (overlayLabel != null)
closedContainer.Right.OnInitialGeometry(() =>
closedContainer.Right.style.marginLeft = -overlayLabel.resolvedStyle.width);
// Swap the open and closed foldouts when the foldout is opened or closed
foldout.SetVisible(foldout.value);
closedFoldout.RegisterValueChangedCallback((evt) =>
{
if (evt.target == closedFoldout)
{
if (evt.newValue && evt.target == closedFoldout)
{
closedContainer.SetVisible(false);
foldout.SetVisible(true);
foldout.value = true;
closedFoldout.SetValueWithoutNotify(false);
foldout.Q().Focus();
}
evt.StopPropagation();
}
});
closedContainer.SetVisible(!foldout.value);
foldout.RegisterValueChangedCallback((evt) =>
{
if (evt.target == foldout)
{
if (!evt.newValue)
{
closedContainer.SetVisible(true);
foldout.SetVisible(false);
closedFoldout.SetValueWithoutNotify(false);
foldout.value = false;
closedFoldout.Q().Focus();
}
evt.StopPropagation();
}
});
}
}
public static VisualElement HelpBoxWithButton(
string message, HelpBoxMessageType messageType,
string buttonText, Action onClicked, ContextualMenuManipulator contextMenu = null)
{
var box = new VisualElement { style =
{
flexDirection = FlexDirection.Row,
paddingTop = 8, paddingBottom = 8, paddingLeft = 8, paddingRight = 8
}};
box.AddToClassList("unity-help-box");
var innerBox = box.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Column, flexGrow = 1 }});
var row = innerBox.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }});
var icon = row.AddChild(MiniHelpIcon("", messageType));
icon.style.alignSelf = Align.Auto;
icon.style.marginRight = 6;
var text = row.AddChild(new Label(message)
{ style = { flexGrow = 1, flexBasis = 100, alignSelf = Align.Center, whiteSpace = WhiteSpace.Normal }});
var buttons = innerBox.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1, marginTop = 6 }});
buttons.Add(new VisualElement { style = { flexGrow = 1 }});
var button = buttons.AddChild(new Button(onClicked) { text = buttonText });
if (contextMenu != null)
{
contextMenu.activators.Clear();
contextMenu.activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
button.AddManipulator(contextMenu);
}
return box;
}
public static void AddRemainingProperties(VisualElement ux, SerializedProperty property)
{
if (property != null)
{
var p = property.Copy();
do
{
if (p.name != "m_Script")
ux.Add(new PropertyField(p));
}
while (p.NextVisible(false));
}
}
public static bool IsAncestorOf(this Transform p, Transform other)
{
while (other != null && p != null)
{
if (other == p)
return true;
other = other.parent;
}
return false;
}
}
}