This commit is contained in:
2026-03-10 15:36:10 +08:00
parent e39164fed4
commit ab47358771
326 changed files with 39762 additions and 236 deletions
@@ -0,0 +1,8 @@
[InternetShortcut]
URL=https://kybernetik.com.au/inspector-gadgets/
IDList=
HotKey=0
IconFile=C:\Users\Kailas\AppData\Local\Mozilla\Firefox\Profiles\6ktl974p.default\shortcutCache\R+P3xDd5YF1rUUIXSlWiqg==.ico
IconIndex=0
[{000214A0-0000-0000-C000-000000000046}]
Prop3=19,11
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c53bf82a0eba7334e8acbb1c239fb084
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 17cce89e465b66d4ea61f37624d1b5ea
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c258b9b59f7ea344a9656b5f09bdd015
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e7a7d4811e564a54195de3803366412e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,164 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System;
using System.Reflection;
using System.Text;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for <see cref="BaseInspectableAttribute"/>.</summary>
public abstract class BaseInspectableAttributeDrawer<T> : BaseInspectableAttributeDrawer
where T : BaseInspectableAttribute
{
/************************************************************************************************************************/
/// <summary>The attribute being drawn.</summary>
public T Attribute { get; private set; }
/************************************************************************************************************************/
/// <inheritdoc/>
internal override string Initialize(BaseInspectableAttribute attribute, MemberInfo member)
{
Attribute = attribute as T;
return base.Initialize(attribute, member);
}
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for <see cref="BaseInspectableAttribute"/>.</summary>
public abstract class BaseInspectableAttributeDrawer : IComparable<BaseInspectableAttributeDrawer>
{
/************************************************************************************************************************/
/// <summary>The attribute being drawn.</summary>
public BaseInspectableAttribute BaseAttribute { get; private set; }
/// <summary>The attributed member.</summary>
public MemberInfo Member { get; private set; }
/************************************************************************************************************************/
/// <summary><see cref="BaseInspectableAttribute.DisplayIndex"/></summary>
public int DisplayIndex
=> BaseAttribute.DisplayIndex;
/// <summary><see cref="BaseInspectableAttribute.When"/></summary>
public EditorState When
=> BaseAttribute.WhenNullable.ValueOrDefault();
/************************************************************************************************************************/
/// <summary>Compares the <see cref="BaseAttribute"/>s.</summary>
public int CompareTo(BaseInspectableAttributeDrawer other)
=> BaseAttribute.CompareTo(other.BaseAttribute);
/************************************************************************************************************************/
/// <summary>Initializes this drawer.</summary>
internal virtual string Initialize(BaseInspectableAttribute attribute, MemberInfo member)
{
BaseAttribute = attribute;
Member = member;
return Initialize();
}
/// <summary>Initializes this drawer.</summary>
protected abstract string Initialize();
/************************************************************************************************************************/
/// <summary>
/// Logs a warning that the specified `member` can't have this kind of attribute for the given `reason`.
/// </summary>
internal void LogInvalidMember(string reason)
{
var text = new StringBuilder();
text.Append("The member: ");
text.Append(Member.DeclaringType.FullName);
text.Append('.');
text.Append(Member.Name);
text.Append(" cannot have a [");
text.Append(GetType().FullName);
text.Append("] attribute because ");
text.Append(reason);
text.Append('.');
Debug.LogWarning(text.ToString());
}
/************************************************************************************************************************/
/// <summary>Draws this inspectable using <see cref="GUILayout"/>.</summary>
public abstract void OnGUI(Object[] targets);
/************************************************************************************************************************/
/// <summary>
/// If <see cref="Event.current"/> is a Context Click within the `area`, this method creates a menu, calls
/// <see cref="PopulateContextMenu"/>, and shows it as a context menu.
/// </summary>
protected void CheckContextMenu(Rect area, Object[] targets)
{
var currentEvent = Event.current;
if (currentEvent.type != EventType.ContextClick ||
!area.Contains(currentEvent.mousePosition))
return;
var menu = new UnityEditor.GenericMenu();
PopulateContextMenu(menu, targets);
menu.ShowAsContext();
}
/// <summary>Adds various items to the `menu` relating to the `targets`.</summary>
protected virtual void PopulateContextMenu(UnityEditor.GenericMenu menu, Object[] targets) { }
/************************************************************************************************************************/
/// <summary>
/// Draws a label like <see cref="UnityEditor.EditorGUILayout.PrefixLabel(string)"/> but doesn't get greyed out
/// if the GUI is disabled for the following control.
/// </summary>
public static Rect PrefixLabel(GUIContent label)
{
var area = GUILayoutUtility.GetRect(0, UnityEditor.EditorGUIUtility.singleLineHeight, LabelStyle);
var labelWidth = UnityEditor.EditorGUIUtility.labelWidth + LabelStyle.padding.horizontal;
labelWidth -= UnityEditor.EditorGUI.indentLevel * Editor.IGEditorUtils.IndentSize;
var labelArea = Editor.IGEditorUtils.StealFromLeft(ref area, labelWidth);
labelArea = UnityEditor.EditorGUI.IndentedRect(labelArea);
GUI.Label(labelArea, label, LabelStyle);
return area;
}
/************************************************************************************************************************/
private static GUIStyle _LabelStyle;
/// <summary>A style based on the default label with the font set to italic.</summary>
public static GUIStyle LabelStyle
{
get
{
if (_LabelStyle == null)
_LabelStyle = new GUIStyle(UnityEditor.EditorStyles.label)
{
fontStyle = FontStyle.Italic
};
return _LabelStyle;
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e982d21179aefd54aa31d81d0fda05c0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,72 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System.Reflection;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor.PropertyDrawers
{
internal sealed class ButtonAttributeDrawer : BaseInspectableAttributeDrawer<ButtonAttribute>
{
/************************************************************************************************************************/
private MethodInfo _Method;
/// <summary>Initialize this button with a method.</summary>
protected override string Initialize()
{
_Method = Member as MethodInfo;
if (_Method == null)
return "it is not a method";
if (Attribute.Label == null)
Attribute.Label = IGUtils.ConvertCamelCaseToFriendly(_Method.Name);
if (Attribute.Tooltip == null)
Attribute.Tooltip = "Calls " + _Method.GetNameCS();
return null;
}
/************************************************************************************************************************/
/// <summary>Draw this button using <see cref="GUILayout"/>.</summary>
public override void OnGUI(Object[] targets)
{
if (GUILayout.Button(Attribute.Label, UnityEditor.EditorStyles.miniButton))
{
UnityEditor.EditorApplication.delayCall += () =>
{
if (Attribute.SetDirty)
UnityEditor.Undo.RecordObjects(targets, "Inspector");
if (_Method.IsStatic)// Static Method.
{
var result = _Method.Invoke(null, null);
if (_Method.ReturnType != typeof(void))
Debug.Log($"{Attribute.Label}: {result}");
}
else// Instance Method.
{
foreach (var target in targets)
{
var result = _Method.Invoke(target, null);
if (_Method.ReturnType != typeof(void))
Debug.Log($"{Attribute.Label}: {result}", target);
}
}
};
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6f9f25a826251946832ff52c7b1e800
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,295 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor.PropertyDrawers
{
internal sealed class InspectableAttributeDrawer : BaseInspectableAttributeDrawer<InspectableAttribute>
{
/************************************************************************************************************************/
private FieldInfo _Field;
private PropertyInfo _Property;
private Type _MemberType;
private SerializedPropertyType _PropertyType;
private Exception _Exception;
private bool _IsExpanded;
private Action _OnInspectorGUI;
/************************************************************************************************************************/
/// <inheritdoc/>
protected override string Initialize()
{
// Validate the member.
var field = Member as FieldInfo;
if (field != null)
{
_Field = field;
_MemberType = field.FieldType;
if (field.IsLiteral || field.IsInitOnly)
Attribute.Readonly = true;
}
else
{
var property = Member as PropertyInfo;
if (property != null)
{
if (property.GetGetMethod(true) == null)
{
return "it must have both a getter and setter";
}
if (property.GetSetMethod(true) == null)
Attribute.Readonly = true;
_Property = property;
_MemberType = property.PropertyType;
}
else
{
return "it is not a field or property";
}
}
if (Attribute.Label == null)
Attribute.Label = IGUtils.ConvertFieldNameToFriendly(Member.Name, true);
if (Attribute.Tooltip == null)
Attribute.Tooltip = "[Inspectable] " + Member.GetNameCS();
_PropertyType = Serialization.GetPropertyType(_MemberType);
return null;
}
/************************************************************************************************************************/
private object _Value;
/// <inheritdoc/>
public override void OnGUI(Object[] targets)
{
var showMixedValue = EditorGUI.showMixedValue;
if (Event.current.type == EventType.Layout)
{
try
{
_Value = GetValue(targets);
_Exception = null;
}
catch (Exception exception)
{
_Exception = exception;
}
}
GUILayout.BeginVertical();
var area = PrefixLabel(Attribute.LabelContent);
if (_Exception != null)
{
if (GUI.Button(area, _Exception.GetType().Name, EditorStyles.miniButton))
{
Debug.LogException(_Exception, targets[0]);
}
}
else
{
EditorGUI.BeginChangeCheck();
var guiEnabled = GUI.enabled;
if (Attribute.Readonly)
GUI.enabled = false;
switch (_PropertyType)
{
// Primitives.
case SerializedPropertyType.Boolean: _Value = EditorGUI.Toggle(area, (bool)_Value); break;
case SerializedPropertyType.Integer: _Value = EditorGUI.IntField(area, (int)_Value); break;
case SerializedPropertyType.Float: _Value = EditorGUI.FloatField(area, (float)_Value); break;
case SerializedPropertyType.String: _Value = EditorGUI.TextField(area, (string)_Value); break;
// Vectors.
case SerializedPropertyType.Vector2: _Value = EditorGUI.Vector2Field(area, GUIContent.none, (Vector2)_Value); break;
case SerializedPropertyType.Vector3: _Value = EditorGUI.Vector3Field(area, GUIContent.none, (Vector3)_Value); break;
case SerializedPropertyType.Vector4: _Value = EditorGUI.Vector4Field(area, GUIContent.none, (Vector4)_Value); break;
case SerializedPropertyType.Quaternion:
_Value = Quaternion.Euler(EditorGUI.Vector3Field(area, GUIContent.none, ((Quaternion)_Value).eulerAngles));
break;
// Other.
case SerializedPropertyType.Color: _Value = EditorGUI.ColorField(area, (Color)_Value); break;
case SerializedPropertyType.Gradient:
break;
case SerializedPropertyType.Rect: _Value = EditorGUI.RectField(area, (Rect)_Value); break;
case SerializedPropertyType.Bounds: _Value = EditorGUI.BoundsField(area, (Bounds)_Value); break;
case SerializedPropertyType.Vector2Int: _Value = EditorGUI.Vector2IntField(area, GUIContent.none, (Vector2Int)_Value); break;
case SerializedPropertyType.Vector3Int: _Value = EditorGUI.Vector3IntField(area, GUIContent.none, (Vector3Int)_Value); break;
case SerializedPropertyType.RectInt: _Value = EditorGUI.RectIntField(area, (RectInt)_Value); break;
case SerializedPropertyType.BoundsInt: _Value = EditorGUI.BoundsIntField(area, (BoundsInt)_Value); break;
case SerializedPropertyType.AnimationCurve: _Value = EditorGUI.CurveField(area, (AnimationCurve)_Value); break;
// Special.
case SerializedPropertyType.ObjectReference: _Value = EditorGUI.ObjectField(area, (Object)_Value, _MemberType, true); break;
case SerializedPropertyType.Enum: _Value = EditorGUI.EnumPopup(area, (Enum)_Value); break;
default:
if (_Value == null)
{
GUI.Label(area, "Null");
}
else
{
if (_OnInspectorGUI == null ||
_OnInspectorGUI.Target != _Value)
_OnInspectorGUI = MethodCache.OnInspectorGUI.GetDelegate(_Value);
if (_OnInspectorGUI != null)
{
GUI.enabled = true;
area = GUILayoutUtility.GetLastRect();
_IsExpanded = EditorGUI.Foldout(area, _IsExpanded, GUIContent.none, true);
if (_IsExpanded)
{
EditorGUI.indentLevel++;
GUI.enabled = Attribute.Readonly;
_OnInspectorGUI();
EditorGUI.indentLevel--;
}
}
else
{
string label = null;
try
{
label = _Value.ToString();
GUI.Label(area, label);
}
catch (Exception exception)
{
if (GUI.Button(area, exception.GetType().Name, EditorStyles.miniButton))
{
Debug.LogException(exception, targets[0]);
}
}
}
}
break;
}
GUI.enabled = guiEnabled;
if (EditorGUI.EndChangeCheck() && !Attribute.Readonly)
SetValue(targets, _Value);
}
GUILayout.EndVertical();
CheckContextMenu(GUILayoutUtility.GetLastRect(), targets);
if (Attribute.ConstantlyRepaint)
HandleUtility.Repaint();
EditorGUI.showMixedValue = showMixedValue;
}
/************************************************************************************************************************/
private object GetValue(Object[] targets)
{
if (targets == null)
return GetSingleValue(null);
var firstValue = GetSingleValue(targets[0]);
var i = 1;
for (; i < targets.Length; i++)
{
var value = GetSingleValue(targets[i]);
if (!Equals(firstValue, value))
{
EditorGUI.showMixedValue = true;
break;
}
}
return firstValue;
}
private object GetSingleValue(Object target)
{
if (_Field != null)
return _Field.GetValue(target);
else
return _Property.GetValue(target, null);
}
/************************************************************************************************************************/
private void SetValue(Object[] targets, object value)
{
if (targets == null)
SetSingleValue(null, value);
else
for (int i = 0; i < targets.Length; i++)
SetSingleValue(targets[i], value);
}
private void SetSingleValue(Object target, object value)
{
if (_Field != null)
_Field.SetValue(target, value);
else
_Property.SetValue(target, value, null);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void PopulateContextMenu(GenericMenu menu, Object[] targets)
{
menu.AddItem(new GUIContent("Copy to Clipboard"), false, () =>
{
var value = GetValue(targets);
EditorGUIUtility.systemCopyBuffer = value != null ? value.ToString() : "null";
});
menu.AddItem(new GUIContent("Log Value"), false, () =>
{
MemberInfo member = _Field;
if (member == null)
member = _Property;
var value = GetValue(targets);
Debug.Log(member.GetNameCS() + ": " + value, targets[0]);
});
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: abe62369061dcc2459a726fc769c071a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,169 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Editor.PropertyDrawers;
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Attributes
{
/// <summary>[Pro-Only]
/// <see cref="Editor.Editor{T}"/> uses these attributes to add extra elements to the inspector.
/// </summary>
internal static class Inspectables
{
/************************************************************************************************************************/
private static readonly Dictionary<Type, List<BaseInspectableAttributeDrawer>>
AllDrawers = new();
/************************************************************************************************************************/
internal static List<BaseInspectableAttributeDrawer> Gather(Type type)
{
if (!AllDrawers.TryGetValue(type, out var inspectables))
{
inspectables = new();
if (type.BaseType != null)
inspectables.AddRange(Gather(type.BaseType));
Gather(type, inspectables, Editor.IGEditorUtils.AnyAccessBindings | BindingFlags.DeclaredOnly);
AllDrawers.Add(type, inspectables);
}
return inspectables;
}
/************************************************************************************************************************/
private static readonly List<BaseInspectableAttribute> Attributes = new();
private static readonly List<FieldInfo> Fields = new();
private static readonly List<PropertyInfo> Properties = new();
private static readonly List<MethodInfo> Methods = new();
private static void Gather(
Type type,
List<BaseInspectableAttributeDrawer> drawers,
BindingFlags bindings)
{
Attributes.Clear();
Fields.Clear();
Properties.Clear();
Methods.Clear();
var first = Attributes.Count;
var index = first;
// Fields.
IGUtils.GetAttributedFields(type, bindings, Attributes, Fields, false);
for (int i = 0; i < Fields.Count; i++)
{
Initialize(drawers, Attributes, first, ref index, Fields[i]);
}
// Properties.
IGUtils.GetAttributedProperties(type, bindings, Attributes, Properties, false);
for (int i = 0; i < Properties.Count; i++)
{
Initialize(drawers, Attributes, first, ref index, Properties[i]);
}
// Methods.
IGUtils.GetAttributedMethods(type, bindings, Attributes, Methods, false);
for (int i = 0; i < Methods.Count; i++)
{
Initialize(drawers, Attributes, first, ref index, Methods[i]);
}
// Sort by DisplayIndex.
IGUtils.StableInsertionSort(drawers);
}
/************************************************************************************************************************/
private static void Initialize(
List<BaseInspectableAttributeDrawer> drawers,
List<BaseInspectableAttribute> attributes,
int first,
ref int index,
MemberInfo member)
{
for (int i = 0; i < first; i++)
{
if (drawers[i].Member.MetadataToken == member.MetadataToken)
{
attributes.RemoveAt(index);
return;
}
}
var attribute = attributes[index];
var drawer = CreateDrawer(attribute);
if (drawer == null)
{
attributes.RemoveAt(index);
Debug.LogError($"Unable to determine Inspectable drawer type for {attribute.GetType().FullName}");
return;
}
var error = drawer.Initialize(attribute, member);
if (error != null)
{
attributes.RemoveAt(index);
drawer.LogInvalidMember(error);
return;
}
drawers.Add(drawer);
index++;
}
/************************************************************************************************************************/
private static BaseInspectableAttributeDrawer CreateDrawer(BaseInspectableAttribute attribute)
{
if (!TryGetDrawerType(attribute.GetType(), out var drawerType))
return null;
return Activator.CreateInstance(drawerType) as BaseInspectableAttributeDrawer;
}
/************************************************************************************************************************/
private static Dictionary<Type, Type> _InspectableTypeToDrawerType;
private static bool TryGetDrawerType(Type inspectableType, out Type drawerType)
{
if (_InspectableTypeToDrawerType == null)
{
_InspectableTypeToDrawerType = new();
var types = TypeCache.GetTypesDerivedFrom(typeof(BaseInspectableAttributeDrawer));
foreach (var type in types)
{
if (type.IsAbstract)
continue;
var arguments = type.BaseType.GetGenericArguments();
if (arguments.Length != 1)
continue;
_InspectableTypeToDrawerType.Add(arguments[0], type);
}
}
return _InspectableTypeToDrawerType.TryGetValue(inspectableType, out drawerType);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9a4e659332e96cf4390c78e230503875
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,167 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System;
using System.Reflection;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor.PropertyDrawers
{
internal sealed class LabelAttributeDrawer : BaseInspectableAttributeDrawer<LabelAttribute>
{
/************************************************************************************************************************/
private Color? _Color;
private Func<object, object> _Getter;
private bool _IsStatic;
private object _Value;
private string _LabelValue;
private Exception _Exception;
/************************************************************************************************************************/
/// <inheritdoc/>
protected override string Initialize()
{
var field = Member as FieldInfo;
if (field != null)
{
Initialize(field.GetValue, field.IsStatic, field);
return null;
}
var property = Member as PropertyInfo;
if (property != null)
{
var getter = property.GetGetMethod(true);
if (getter != null)
Initialize(obj => getter.Invoke(obj, null), getter.IsStatic, property);
else
return "it has no getter";
return null;
}
var method = Member as MethodInfo;
if (method != null)
{
if (method.GetParameters().Length == 0)
Initialize(obj => method.Invoke(obj, null), method.IsStatic, method);
else
return "it has parameters";
return null;
}
return "it has an unsupported member type: " + Member.GetType().FullName;
}
/************************************************************************************************************************/
private void Initialize(Func<object, object> getter, bool isStatic, MemberInfo member)
{
_Getter = getter;
_IsStatic = isStatic;
if (Attribute.Label == null)
Attribute.Label = IGUtils.ConvertFieldNameToFriendly(member.Name, true);
if (Attribute.Tooltip == null)
Attribute.Tooltip = "[Label] " + member.GetNameCS();
var attributes = member.GetCustomAttributes(typeof(ColorAttribute), true);
if (attributes != null && attributes.Length > 0)
_Color = (attributes[0] as ColorAttribute).Color;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Object[] targets)
{
// Get the target object.
if (Event.current.type == EventType.Layout)
{
try
{
_Value = _Getter(_IsStatic ? null : targets[0]);
_LabelValue = _Value != null ? _Value.ToString() : "null";
_Exception = null;
}
catch (Exception exception)
{
_LabelValue = exception.GetBaseException().GetType().Name;
_Exception = exception;
}
}
if (Attribute.HideWhenNull && _Value == null)
return;
// Draw the label.
var oldColor = GUI.color;
if (_Color != null) GUI.color = _Color.Value;
Rect area;
if (Attribute.LargeMode)
{
area = EditorGUILayout.BeginVertical();
{
GUILayout.Label(Attribute.LabelContent, LabelStyle);
GUILayout.Label(_LabelValue, EditorStyles.wordWrappedLabel);
}
EditorGUILayout.EndVertical();
}
else
{
area = PrefixLabel(Attribute.LabelContent);
GUI.Label(area, _LabelValue, EditorStyles.wordWrappedLabel);
area = GUILayoutUtility.GetLastRect();
}
GUI.color = oldColor;
CheckContextMenu(area, targets);
if (Attribute.ConstantlyRepaint)
InternalEditorUtility.RepaintAllViews();
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void PopulateContextMenu(GenericMenu menu, Object[] targets)
{
menu.AddItem(new GUIContent("Copy to Clipboard"), false, () =>
{
EditorGUIUtility.systemCopyBuffer = _LabelValue;
});
menu.AddItem(new GUIContent("Log Value"), false, () =>
{
if (_Exception == null)
{
Debug.Log($"{Member.GetNameCS()}: {_LabelValue}", targets[0]);
}
else
{
Debug.LogException(_Exception, targets[0]);
}
});
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8ea2501412c66fd4094813f168559170
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 20585076ec3f36046904439993255c32
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,38 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="ColorAttribute"/>.</summary>
[UnityEditor.CustomPropertyDrawer(typeof(ColorAttribute))]
public sealed class ColorAttributeDrawer : ObjectDrawer
{
/************************************************************************************************************************/
public override void OnGUI(Rect area, UnityEditor.SerializedProperty property, GUIContent label)
{
var oldColor = GUI.color;
GUI.color = (attribute as ColorAttribute).Color;
{
base.OnGUI(area, property, label);
}
GUI.color = oldColor;
}
/************************************************************************************************************************/
public override float GetPropertyHeight(UnityEditor.SerializedProperty property, GUIContent label)
{
return UnityEditor.EditorGUI.GetPropertyHeight(property, label, true);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d51c640b686c8a40915056f9ddff2ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="EulerAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(EulerAttribute))]
public sealed class EulerAttributeDrawer : PropertyDrawer
{
/************************************************************************************************************************/
private sealed class Data
{
public Vector3 eulerAngles, oldEulerAngles;
}
private static readonly Dictionary<string, Data>
CachedData = new Dictionary<string, Data>();
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.Quaternion)
{
EditorGUI.PropertyField(area, property, label);
var accessor = property.GetAccessor();
Debug.LogError("Invalid [Euler] Attribute: " +
(accessor != null ? accessor.ToString() : property.propertyPath) +
" is not a Quaternion.");
return;
}
if (!CachedData.TryGetValue(property.propertyPath, out var data))
{
data = new Data();
data.eulerAngles = data.oldEulerAngles = property.quaternionValue.eulerAngles;
CachedData.Add(property.propertyPath, data);
}
var wideMode = EditorGUIUtility.wideMode;
var showMixedValue = EditorGUI.showMixedValue;
EditorGUIUtility.wideMode = true;
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
label = EditorGUI.BeginProperty(area, label, property);
if (EditorGUI.showMixedValue)
{
data.eulerAngles = data.oldEulerAngles = default;
}
else
{
var euler = property.quaternionValue.eulerAngles;
if (data.oldEulerAngles != euler)
{
data.eulerAngles = euler;
}
}
EditorGUI.BeginChangeCheck();
data.eulerAngles = EditorGUI.Vector3Field(area, label, data.eulerAngles);
if (EditorGUI.EndChangeCheck())
{
var quaternion = Quaternion.Euler(data.eulerAngles);
property.quaternionValue = quaternion;
data.oldEulerAngles = quaternion.eulerAngles;
}
EditorGUI.EndProperty();
EditorGUI.showMixedValue = showMixedValue;
EditorGUIUtility.wideMode = wideMode;
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7af0f370a1ee83049ac34148b66ecd0d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,53 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with an <see cref="InfoBoxAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(InfoBoxAttribute))]
public sealed class InfoBoxAttributeDrawer : DecoratorDrawer
{
/************************************************************************************************************************/
public InfoBoxAttribute Attribute
=> (InfoBoxAttribute)attribute;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetHeight()
{
var width = EditorGUIUtility.currentViewWidth -
EditorStyles.inspectorDefaultMargins.padding.horizontal -
EditorGUI.IndentedRect(default).x -
20;// Extra padding in case there's a scroll bar.
var attribute = Attribute;
var content = EditorGUIUtility.TrTextContentWithIcon(attribute.Text, (MessageType)attribute.Type);
return
EditorStyles.helpBox.CalcHeight(content, width) +
IGEditorUtils.Spacing;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area)
{
area = EditorGUI.IndentedRect(area);
area.height -= IGEditorUtils.Spacing;
var attribute = Attribute;
EditorGUI.HelpBox(area, attribute.Text, (MessageType)attribute.Type);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 47345018c846f1a43a77c3bde9ea154c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,151 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="LabelledCollectionAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(LabelledCollectionAttribute))]
public sealed class LabelledCollectionAttributeDrawer : ObjectDrawer
{
/************************************************************************************************************************/
private readonly Dictionary<string, int>
PropertyIndices = new Dictionary<string, int>();
/************************************************************************************************************************/
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
if (!string.IsNullOrEmpty(label.text) ||
!string.IsNullOrEmpty(label.tooltip))
{
var path = property.propertyPath;
if (!PropertyIndices.TryGetValue(path, out var index))
{
index = path.LastIndexOf('[');
if (index >= 0)
{
index++;
var close = path.IndexOf(']', index);
if (index >= 0)
{
var textIndex = path.Substring(index, close - index);
if (!int.TryParse(textIndex, out index))
index = -1;
}
}
PropertyIndices.Add(path, index);
}
if (index >= 0)
{
var attribute = (LabelledCollectionAttribute)this.attribute;
Initialize(attribute, fieldInfo, property);
var name = attribute.GetLabel(index);
if (!string.IsNullOrEmpty(name))
label = new GUIContent(name, $"{label.text}: {name}");
}
}
base.OnGUI(area, property, label);
}
/************************************************************************************************************************/
private static readonly Type[] MethodParameterTypes = { typeof(int) };
private static void Initialize(
LabelledCollectionAttribute attribute,
FieldInfo attributedField,
SerializedProperty property)
{
if (attribute._MemberName == null)
return;
var memberName = attribute._MemberName;
attribute._MemberName = null;
var accessor = property.GetAccessor();
if (accessor != null)
{
var type = accessor.GetField(property).FieldType;
if (type.IsArray)// Array Field.
{
attribute._GetLabel = index =>
{
var array = accessor.GetValue(property.serializedObject.targetObject) as Array;
if (array != null && index >= 0 && index < array.Length)
{
var value = array.GetValue(index);
return value?.ToString();
}
else
{
return index.ToString();
}
};
}
else if (typeof(IList).IsAssignableFrom(type))// List Field.
{
attribute._GetLabel = index =>
{
var list = accessor.GetValue(property.serializedObject.targetObject) as IList;
if (list != null && index >= 0 && index < list.Count)
{
var value = list[index];
return value?.ToString();
}
else
{
return index.ToString();
}
};
}
return;
}
var method = attributedField.DeclaringType.GetMethod(
memberName,
IGEditorUtils.AnyAccessBindings,
null,
MethodParameterTypes,
null);
if (method != null)// Named Method returns labels.
{
if (method.ReturnType != typeof(void))
{
accessor = accessor.Parent;
var parameters = new object[1];
attribute._GetLabel = index =>
{
parameters[0] = index;
var name = method.Invoke(accessor.GetValue(property.serializedObject.targetObject), parameters);
return name?.ToString();
};
}
return;
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dfb9e8bedfb1b8e4b9a6431befbecb57
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="LabelledCollectionAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(LayerAttribute))]
public sealed class LayerAttributeDrawer : PropertyDrawer
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
property.intValue = EditorGUI.LayerField(area, label, property.intValue);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa6a81e5126acac498b33675e5b897b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,47 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="ReadonlyAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(ReadonlyAttribute))]
public sealed class ReadonlyAttributeDrawer : ObjectDrawer
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
var attribute = (ReadonlyAttribute)this.attribute;
var enabled = GUI.enabled;
GUI.enabled = !attribute.When.ValueOrDefault().IsNow();
base.OnGUI(area, property, label);
var currentEvent = Event.current;
if (currentEvent.rawType == EventType.ContextClick &&
area.Contains(currentEvent.mousePosition))
SerializedPropertyContextMenu.OpenMenu(property);
GUI.enabled = enabled;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property, label, true);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e35b3ffd6bf43114792cfd43eaeec762
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,187 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="RequiredAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(RequiredAttribute))]
public sealed class RequiredAttributeDrawer : ObjectDrawer
{
/************************************************************************************************************************/
private RequiredAttribute _Attribute;
private SerializedProperty _SerializedProperty;
private Serialization.PropertyAccessor _SerializedPropertyAccessor;
private FieldInfo _Field;
private PropertyInfo _Property;
private Type _Type;
private bool _IsReferenceOrNullable;
private object _DefaultValue;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> EditorGUI.GetPropertyHeight(property, label);
/************************************************************************************************************************/
private static readonly Color
ErrorFieldColor = new Color(1, 0.65f, 0.65f);
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
var color = GUI.color;
if (Event.current.type == EventType.Repaint)
{
Initialize(property);
if (_Attribute != null && IsInitialized)
{
var accessor = property.GetAccessor();
try
{
var targets = property.serializedObject.targetObjects;
foreach (var target in targets)
{
if (IsDefaultValue(accessor, target))
{
GUI.color = ErrorFieldColor;
break;
}
}
}
catch (Exception exception)
{
Debug.LogException(exception);
GUI.color = ErrorFieldColor;
}
}
}
base.OnGUI(area, property, label);
GUI.color = color;
}
/************************************************************************************************************************/
private bool IsInitialized
=> _SerializedProperty != null
|| _Field != null
|| _Property != null;
/************************************************************************************************************************/
/// <summary>Initializes this attribute to check the value of the specified `property`.</summary>
private void Initialize(SerializedProperty property)
{
if (_Attribute != null)
return;
_Attribute = attribute as RequiredAttribute;
_SerializedProperty = property;
_SerializedPropertyAccessor = property.GetAccessor();
if (_SerializedPropertyAccessor != null)
{
var field = _SerializedPropertyAccessor.GetField(property);
if (field != null)
Initialize(field.FieldType);
}
}
/// <summary>Initializes this attribute to check the value of the specified `property`.</summary>
public void Initialize(FieldInfo field)
{
_Field = field;
Initialize(field.FieldType);
}
/// <summary>Initializes this attribute to check the value of the specified `property`.</summary>
public void Initialize(PropertyInfo property)
{
_Property = property;
Initialize(property.PropertyType);
}
/************************************************************************************************************************/
private void Initialize(Type type)
{
_Type = type;
if (!type.IsValueType || Nullable.GetUnderlyingType(type) != null)
_IsReferenceOrNullable = true;
else
_DefaultValue = Activator.CreateInstance(type);
}
/************************************************************************************************************************/
/// <summary>Does the attributed member on the specified `obj` still have its default value?</summary>
public bool IsDefaultValue(object obj)
=> IsDefaultValue(_SerializedPropertyAccessor, obj);
/// <summary>Does the attributed member on the specified `obj` still have its default value?</summary>
public bool IsDefaultValue(Serialization.PropertyAccessor accessor, object obj)
{
object value;
if (accessor != null)
value = accessor.GetValue(obj);
else if (_Field != null)
value = _Field.GetValue(obj);
else if (_Property != null)
value = _Property.GetValue(obj, null);
else
return false;
if (value == null)
return true;
if (_Type == typeof(string))
return value.Equals("");
// Non-null reference or nullable is not default.
if (_IsReferenceOrNullable)
{
return
(obj as Object) != null &&
value is Object &&
(value as Object) == null;
}
// Otherwise it is a value type, so we need to compare it to the default value.
return value.Equals(_DefaultValue);
}
/************************************************************************************************************************/
/// <summary>Returns the full name of the attributed field or property.</summary>
public override string ToString()
{
if (_Field != null)
return $"{_Field.DeclaringType.FullName}.{_Field.Name}";
else if (_Property != null)
return $"{_Property.DeclaringType.FullName}.{_Property.Name}";
else
return base.ToString();
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 50cbcd2ee58e2fd479df04ac86b27da1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,196 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="SceneAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(SceneAttribute))]
public sealed class SceneAttributeDrawer : PropertyDrawer
{
/************************************************************************************************************************/
private static bool _IsListeningForSceneListChange;
private static EditorBuildSettingsScene[] _AllScenes;
private static readonly List<string> ActiveScenes = new List<string>();
private static readonly GUIContent ButtonContent = new GUIContent();
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
GatherScenes();
area = EditorGUI.IndentedRect(area);
var width = area.width;
var labelWidth = EditorGUIUtility.labelWidth + 20;
area.xMax = labelWidth;
GUI.Label(area, label);
area.width = width;
area.xMin = labelWidth;
string buttonLabel;
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
var value = property.intValue;
if (value >= 0 && value < ActiveScenes.Count)
buttonLabel = ActiveScenes[value];
else
buttonLabel = "None";
ButtonContent.text = value + ": " + buttonLabel;
ButtonContent.tooltip = buttonLabel;
if (DoButtonGUI(area))
ShowIndexPopup(buttonLabel, property);
break;
case SerializedPropertyType.String:
buttonLabel = property.stringValue;
ButtonContent.text = buttonLabel;
ButtonContent.tooltip = null;
if (DoButtonGUI(area))
ShowPathPopup(buttonLabel, property);
break;
default:
GUI.Label(area, "Invalid [Scene] attribute");
break;
}
}
/************************************************************************************************************************/
private void GatherScenes()
{
if (_AllScenes != null)
return;
if (!_IsListeningForSceneListChange)
{
_IsListeningForSceneListChange = true;
EditorBuildSettings.sceneListChanged += () =>
{
_AllScenes = null;
ActiveScenes.Clear();
};
}
_AllScenes = EditorBuildSettings.scenes;
for (int i = 0; i < _AllScenes.Length; i++)
{
var scene = _AllScenes[i];
if (scene.enabled)
ActiveScenes.Add(scene.path);
}
}
/************************************************************************************************************************/
private bool DoButtonGUI(Rect area)
{
return GUI.Button(area, ButtonContent, EditorStyles.popup);
}
/************************************************************************************************************************/
private void ShowIndexPopup(string selectedLabel, SerializedProperty property)
{
var menu = new GenericMenu();
AddMenuItem(menu, "None", selectedLabel, () => property.intValue = -1, property);
var sceneIndex = 0;
for (int i = 0; i < _AllScenes.Length; i++)
{
var scene = _AllScenes[i];
if (scene.enabled)
{
var currentSceneIndex = sceneIndex;
AddMenuItem(menu, currentSceneIndex + ": " + scene.path.AllBackslashes(), selectedLabel,
() => property.intValue = currentSceneIndex, property);
sceneIndex++;
}
else
{
menu.AddDisabledItem(new GUIContent(scene.path.AllBackslashes()));
}
}
menu.ShowAsContext();
}
/************************************************************************************************************************/
private void ShowPathPopup(string selectedLabel, SerializedProperty property)
{
var menu = new GenericMenu();
AddMenuItem(menu, "None", selectedLabel, () => property.stringValue = "", property);
var sceneIndex = 0;
for (int i = 0; i < _AllScenes.Length; i++)
{
var scene = _AllScenes[i];
var name = Path.GetFileNameWithoutExtension(scene.path);
if (scene.enabled)
{
AddMenuItem(menu, name, selectedLabel, () => property.stringValue = name, property);
sceneIndex++;
}
else
{
menu.AddDisabledItem(new GUIContent(name));
}
}
menu.ShowAsContext();
}
/************************************************************************************************************************/
private static void AddMenuItem(GenericMenu menu, string label, string selectedLabel, Action method, SerializedProperty property)
{
menu.AddItem(new GUIContent(label), label == selectedLabel, () =>
{
method();
property.serializedObject.ApplyModifiedProperties();
});
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f1c338e81e558947be4990b1c2174a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,98 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="ShowPreviewAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(ShowPreviewAttribute))]
public sealed class ShowPreviewAttributeDrawer : ObjectDrawer
{
/************************************************************************************************************************/
private const float Padding = 1;
private float _BaseHeight;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var height = _BaseHeight = EditorGUI.GetPropertyHeight(property, label);
if (property.propertyType == SerializedPropertyType.ObjectReference)
{
if (property.objectReferenceValue != null)
{
var previewTexture = AssetPreview.GetAssetPreview(property.objectReferenceValue);
if (previewTexture != null)
{
var attribute = (ShowPreviewAttribute)this.attribute;
height += Mathf.Min(attribute.MaxHeight, previewTexture.height) + Padding;
}
}
}
return height;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
area.height = _BaseHeight;
base.OnGUI(area, property, label);
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
DoWarningBoxGUI($"{property.name} is not an object reference type", property);
return;
}
if (property.objectReferenceValue == null)
return;
var preview = AssetPreview.GetAssetPreview(property.objectReferenceValue);
if (preview == null)
{
DoWarningBoxGUI($"{property.name} doesn't have an asset preview", property);
return;
}
var attribute = (ShowPreviewAttribute)this.attribute;
var xMax = area.xMax;
area.y += area.height + Padding;
area.height = Mathf.Min(attribute.MaxHeight, preview.height);
area.width = area.height * preview.height / preview.width;
area.x = xMax - area.width;
if (area.width <= 0 || area.height <= 0)
return;
GUI.DrawTexture(area, preview);
}
/************************************************************************************************************************/
private void DoWarningBoxGUI(string warningText, SerializedProperty property)
{
EditorGUILayout.HelpBox(warningText, MessageType.Warning);
//Debug.LogWarning(warningText, property.serializedObject.targetObject);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec84a027b80a78a4e928c3008b3386fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,477 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="ToolbarAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(ToolbarAttribute))]
public sealed class ToolbarAttributeDrawer : PropertyDrawer
{
/************************************************************************************************************************/
private static readonly GUIStyle
LeftButtonStyle,
MidButtonStyle,
RightButtonStyle;
static ToolbarAttributeDrawer()
{
LeftButtonStyle = new GUIStyle(EditorStyles.miniButtonLeft);
MidButtonStyle = new GUIStyle(EditorStyles.miniButtonMid);
RightButtonStyle = new GUIStyle(EditorStyles.miniButtonRight);
LeftButtonStyle.padding = MidButtonStyle.padding = RightButtonStyle.padding = new RectOffset(4, 4, 2, 2);
}
/************************************************************************************************************************/
private static readonly int
DragControlHint = "ToolbarDragControl".GetHashCode();
private ToolbarAttribute _Attribute;
private Type _FieldType;
private bool _IsFlags;
private int _Count;
private GUIContent[] _Labels;
private long[] _Values;
private float[] _Widths;
private float _TotalWidth;
private float _FieldWidth;
private float _Scrollarea;
/************************************************************************************************************************/
private void ClampScrollarea()
{
_Scrollarea = Mathf.Clamp(_Scrollarea, 0, _TotalWidth - _FieldWidth);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
var currentEvent = Event.current;
if (currentEvent.type == EventType.Layout)
return;
Initialize(property, label);
if (_Labels == null || _Labels.Length == 0)
{
EditorGUI.PropertyField(area, property, label);
return;
}
DoToolbar(area, property, label);
// Handle scroll events if you are too wide.
if (currentEvent.type == EventType.ScrollWheel &&
_TotalWidth > _FieldWidth &&
area.Contains(currentEvent.mousePosition))
{
_Scrollarea += (currentEvent.delta.y + currentEvent.delta.x) * 10;
ClampScrollarea();
currentEvent.Use();
}
}
/************************************************************************************************************************/
private void Initialize(SerializedProperty property, GUIContent label)
{
// Already initialized.
if (_Labels != null)
return;
DetermineFieldType();
_Attribute = attribute as ToolbarAttribute;
if (_FieldType == typeof(bool))// Bool.
{
_Labels = _Attribute.Labels;
if (_Labels == null || _Labels.Length != 2)
{
if (_Labels == null)
{
_Labels = new GUIContent[]
{
new GUIContent("False"),
new GUIContent("True"),
};
}
else if (_Labels.Length <= 1)
{
Debug.LogWarning(GetImproperAttributeWarningPrefix() +
" must specify exactly two labels to be used on a bool field.");
_Labels = Array.Empty<GUIContent>();
return;
}
else
{
Array.Resize(ref _Labels, 2);
}
}
_Count = 2;
return;
}
else if (_FieldType == typeof(string))// String.
{
_Labels = _Attribute.Labels;
if (_Labels == null)
{
Debug.LogWarning(GetImproperAttributeWarningPrefix() +
" must specify the labels you with to display.");
_Labels = Array.Empty<GUIContent>();
return;
}
_Count = _Labels.Length;
}
else if (_FieldType.IsEnum)// Enum.
{
if (_Attribute.Labels != null)
Debug.LogWarning(GetImproperAttributeWarningPrefix() +
" shound not specify any labels since it is an enum field.");
_IsFlags = _FieldType.IsDefined(typeof(FlagsAttribute), true);
var enumValues = Enum.GetValues(_FieldType);
var displayNames = property.enumDisplayNames;
var trueNames = property.enumNames;
_Labels = new GUIContent[enumValues.Length];
_Values = new long[enumValues.Length];
_Count = 0;
for (int i = 0; i < enumValues.Length; i++)
{
var value = Convert.ToInt64(enumValues.GetValue(i));
// Skip zero values in flags enums.
if (_IsFlags && value == 0)
continue;
var tooltip = trueNames[i];
var members = _FieldType.GetMember(tooltip);
for (int iMember = 0; iMember < members.Length; iMember++)
{
var member = members[iMember];
var tooltipAttribute = member.GetCustomAttribute<TooltipAttribute>(true);
if (tooltipAttribute != null)
{
tooltip = tooltipAttribute.tooltip;
break;
}
}
_Labels[_Count] = new GUIContent(displayNames[i], tooltip);
_Values[_Count] = value;
_Count++;
}
}
else// Invalid field type.
{
_Labels = Array.Empty<GUIContent>();
Debug.LogWarning(GetImproperAttributeWarningPrefix() +
" will do nothing because it is only valid on bool, string, and enum fields.");
return;
}
// Initialize the button widths.
_Widths = new float[_Count];
_TotalWidth = 0;
for (int i = 0; i < _Count; i++)
{
var width = MidButtonStyle.CalcSize(_Labels[i]).x;
_Widths[i] = width;
_TotalWidth += width;
}
}
/************************************************************************************************************************/
private string GetImproperAttributeWarningPrefix()
=> $"The [Toolbar] attribute applied to {fieldInfo.DeclaringType.FullName}.{fieldInfo.Name}";
/************************************************************************************************************************/
private void DetermineFieldType()
{
_FieldType = fieldInfo.FieldType;
while (_FieldType.IsGenericType)
{
var genericArguments = _FieldType.GetGenericArguments();
if (genericArguments.Length == 1)
_FieldType = genericArguments[0];
else
break;
}
while (_FieldType.IsArray)
{
_FieldType = _FieldType.GetElementType();
}
}
/************************************************************************************************************************/
private void DoToolbar(Rect area, SerializedProperty property, GUIContent label)
{
label = EditorGUI.BeginProperty(area, label, property);
_FieldWidth = area.width - EditorGUIUtility.labelWidth;
var right = area.xMax;
area.width = EditorGUIUtility.labelWidth;
// Drag.
var controlID = GUIUtility.GetControlID(DragControlHint, FocusType.Passive, area);
float spareSpace;
if (_TotalWidth > _FieldWidth)
{
DragNumberValue(area, controlID, ref _Scrollarea);
ClampScrollarea();
_FieldWidth -= 2;
//right -= 1;
spareSpace = 0;
}
else
{
spareSpace = (_FieldWidth - _TotalWidth) / _Count;
}
// Label.
if (_Attribute.Label != null)
label.text = _Attribute.Label;
EditorGUI.PrefixLabel(area, label);
if (_Labels.Length == 0)
return;
// Buttons.
area.x += area.width;
area.xMax = right;
GUI.BeginGroup(area);
area.x = -_Scrollarea;
area.y = 0;
if (_FieldType == typeof(bool))
DoBoolButtons(area, property, spareSpace);
else if (_FieldType == typeof(string))
DoStringButtons(area, property, spareSpace);
if (_FieldType.IsEnum)
DoEnumButtons(area, property, spareSpace);
GUI.EndGroup();
EditorGUI.EndProperty();
}
/************************************************************************************************************************/
private void DoBoolButtons(Rect area, SerializedProperty property, float spareSpace)
{
const float ToggleWidth = 16;
area.width -= ToggleWidth;
var width = area.width;
area.width = ToggleWidth;
var value = property.boolValue;
if (value != GUI.Toggle(area, value, GUIContent.none))
property.boolValue = !value;
area.x += area.width;
area.width = Mathf.Floor(width * 0.5f);
if (GUI.Toggle(area, !property.boolValue, _Labels[0], LeftButtonStyle))
property.boolValue = false;
area.x += area.width;
area.width = width - area.width;
if (GUI.Toggle(area, property.boolValue, _Labels[1], RightButtonStyle))
property.boolValue = true;
}
/************************************************************************************************************************/
private void DoEnumButtons(Rect area, SerializedProperty property, float spareSpace)
{
var right = area.width;
var propertyValue = property.longValue;
for (int i = 0; i < _Count; i++)
{
var value = _Values[i];
area.width = _Widths[i] + spareSpace;
// Make sure the last button goes all the way to the end.
if (i == _Count - 1 && area.xMax < right)
area.xMax = right;
if (_IsFlags)// Flags Enum.
{
var wasPressed = (property.intValue & value) == value;
var isPressed = GUI.Toggle(area, wasPressed, _Labels[i], GetStyle(i));
if (isPressed != wasPressed)
{
if (isPressed)
propertyValue |= value;
else
propertyValue &= ~value;
}
}
else// Standard Enum.
{
var pressed = propertyValue == value;
pressed = GUI.Toggle(area, pressed, _Labels[i], GetStyle(i));
if (pressed) propertyValue = value;
}
area.x += area.width;
}
if (property.longValue != propertyValue)
property.longValue = propertyValue;
}
/************************************************************************************************************************/
private void DoStringButtons(Rect area, SerializedProperty property, float spareSpace)
{
var right = area.width;
var propertyValue = property.stringValue;
for (int i = 0; i < _Count; i++)
{
var label = _Labels[i];
area.width = _Widths[i] + spareSpace;
// Make sure the last button goes all the way to the end.
if (i == _Count - 1 && area.xMax < right)
area.xMax = right;
var pressed = propertyValue == label.text;
pressed = GUI.Toggle(area, pressed, label, GetStyle(i));
if (pressed) propertyValue = label.text;
area.x += area.width;
}
if (property.stringValue != propertyValue)
property.stringValue = propertyValue;
}
/************************************************************************************************************************/
private static float _DragStartValue;
private static void DragNumberValue(Rect area, int id, ref float value)
{
var current = Event.current;
switch (current.GetTypeForControl(id))
{
case EventType.MouseDown:// Begin.
if (current.button == 0 && area.Contains(current.mousePosition))
{
EditorGUIUtility.editingTextField = false;
GUIUtility.hotControl = id;
GUIUtility.keyboardControl = id;
Undo.IncrementCurrentGroup();
current.Use();
_DragStartValue = value;
EditorGUIUtility.SetWantsMouseJumping(1);
}
break;
case EventType.MouseUp:// End.
if (GUIUtility.hotControl == id)// && EditorGUI.s_DragCandidateState != 0)
{
GUIUtility.hotControl = 0;
current.Use();
EditorGUIUtility.SetWantsMouseJumping(0);
}
break;
case EventType.MouseDrag:// Move.
if (GUIUtility.hotControl == id)
{
value -= HandleUtility.niceMouseDelta;
GUI.changed = true;
current.Use();
}
break;
case EventType.KeyDown:// Cancel.
if (GUIUtility.hotControl == id && current.keyCode == KeyCode.Escape)
{
value = _DragStartValue;
GUI.changed = true;
GUIUtility.hotControl = 0;
current.Use();
}
break;
case EventType.Repaint:// Repaint.
EditorGUIUtility.AddCursorRect(area, MouseCursor.SlideArrow);
break;
default:
break;
}
}
/************************************************************************************************************************/
private GUIStyle GetStyle(int index)
{
if (index <= 0)
return LeftButtonStyle;
if (index >= _Count - 1)
return RightButtonStyle;
return MidButtonStyle;
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bc58f5a52d383c241adcc5cf32e8191c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for fields with a <see cref="UniqueCollectionAttribute"/>.</summary>
[CustomPropertyDrawer(typeof(UniqueCollectionAttribute))]
public sealed class UniqueCollectionAttributeDrawer : ObjectDrawer
{
/************************************************************************************************************************/
private static readonly Color
WarningColor = new Color(1, 0.65f, 0.65f);
private const string
NonUniqueMessage = "This element is not unique";
private readonly List<object> Elements = new List<object>();
private string _FirstProperty;
private int _CurrentIndex;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> base.GetPropertyHeight(property, label);
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
var color = GUI.color;
var tooltip = label.tooltip;
switch (Event.current.type)
{
case EventType.Layout:
if (_FirstProperty == null)
{
_FirstProperty = property.propertyPath;
}
else if (property.propertyPath == _FirstProperty)
{
Elements.Clear();
_CurrentIndex = 0;
}
Elements.Add(property.GetValue());
break;
case EventType.Repaint:
var value = Elements[_CurrentIndex];
for (int i = 0; i < Elements.Count; i++)
{
if (i != _CurrentIndex && Equals(value, Elements[i]))
{
GUI.color = WarningColor;
if (!string.IsNullOrEmpty(label.tooltip))
label.tooltip = $"{NonUniqueMessage}\n{label.tooltip}";
else
label.tooltip = NonUniqueMessage;
break;
}
}
_CurrentIndex++;
break;
default:
break;
}
base.OnGUI(area, property, label);
label.tooltip = tooltip;
GUI.color = color;
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0f62c105531ff43468d35b78c03012f4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 438335b91aa0e114abef632bbc47dc6b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Pro-Only]
/// Allows [TexDefine(KEYWORD)] to be used on texture properties in shaders.
/// </summary>
internal sealed class TexDefineDrawer : MaterialPropertyDrawer
{
/************************************************************************************************************************/
private readonly string Keyword;
private readonly bool Log;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TexDefineDrawer"/>.</summary>
public TexDefineDrawer(string keyword)
{
Keyword = keyword;
}
/// <summary>Creates a new <see cref="TexDefineDrawer"/>.</summary>
public TexDefineDrawer(string keyword, string log)
{
Keyword = keyword;
Log = bool.Parse(log);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(MaterialProperty prop, string label, MaterialEditor editor)
=> MaterialEditor.GetDefaultPropertyHeight(prop);
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, MaterialProperty prop, string label, MaterialEditor editor)
{
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = prop.hasMixedValue;
var texture = editor.TextureProperty(area, prop, label);
EditorGUI.showMixedValue = false;
if (EditorGUI.EndChangeCheck())
{
prop.textureValue = texture;
var material = editor.target as Material;
if (texture != null)
material.EnableKeyword(Keyword);
else
material.DisableKeyword(Keyword);
if (Log)
Debug.Log($"{Keyword} = {texture != null}");
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8afef6da8c2da4f4ca39a9a390a3f870
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1ab3337ee3781d8428cf3a8cbf90fe96
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,85 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
[CustomPropertyDrawer(typeof(ValidatorAttribute), true)]
internal sealed class ValidatorAttributeDrawer : ObjectDrawer
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> base.GetPropertyHeight(property, label);
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
double previousDoubleValue = 0;
long previousLongValue = 0;
Object previousObjectReference = null;
switch (property.propertyType)
{
case SerializedPropertyType.Float:
previousDoubleValue = property.doubleValue;
break;
case SerializedPropertyType.Integer:
previousLongValue = property.longValue;
break;
case SerializedPropertyType.ObjectReference:
previousObjectReference = property.objectReferenceValue;
break;
}
EditorGUI.BeginChangeCheck();
base.OnGUI(area, property, label);
if (EditorGUI.EndChangeCheck())
{
var attribute = this.attribute as ValidatorAttribute;
if (attribute == null)
return;
switch (property.propertyType)
{
case SerializedPropertyType.Float:
var doubleValue = property.doubleValue;
if (attribute.TryValidate(ref doubleValue))
property.doubleValue = doubleValue;
else
property.doubleValue = previousDoubleValue;
break;
case SerializedPropertyType.Integer:
var longValue = property.longValue;
if (attribute.TryValidate(ref longValue))
property.longValue = longValue;
else
property.longValue = previousLongValue;
break;
case SerializedPropertyType.ObjectReference:
var objectReference = property.objectReferenceValue;
if (attribute.TryValidate(ref objectReference))
property.objectReferenceValue = objectReference;
else
property.objectReferenceValue = previousObjectReference;
break;
}
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7cadb0cdb4e3a0b42bc42defc520504e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4a26e541f987f5749aabe576000a28ea
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,95 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using UnityEngine;
namespace InspectorGadgets.Editor
{
/// <summary>A <see cref="CommentEditor"/> for <see cref="CommentAsset"/>.</summary>
[UnityEditor.CustomEditor(typeof(CommentAsset))]
internal sealed class CommentAssetEditor : CommentEditor { }
/// <summary>A <see cref="CommentEditor"/> for <see cref="CommentComponent"/>.</summary>
[UnityEditor.CustomEditor(typeof(CommentComponent))]
internal sealed class CommentComponentEditor : CommentEditor { }
/// <summary>[Editor-Only] Custom Inspector for <see cref="IComment"/>s.</summary>
[UnityEditor.CanEditMultipleObjects]
public class CommentEditor : UnityEditor.Editor
{
/************************************************************************************************************************/
private UnityEditor.SerializedProperty _Text;
/************************************************************************************************************************/
private void OnEnable()
{
_Text = serializedObject.FindProperty(((IComment)target).TextFieldName);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnInspectorGUI()
{
serializedObject.Update();
var style = IGEditorUtils.GetCachedStyle(() => new GUIStyle(UnityEditor.EditorStyles.textArea)
{
wordWrap = true
});
GUILayout.BeginHorizontal();
GUILayout.Space(-10);
UnityEditor.EditorGUI.BeginChangeCheck();
var text = UnityEditor.EditorGUILayout.TextArea(_Text.stringValue, style, GUILayout.ExpandHeight(true));
if (UnityEditor.EditorGUI.EndChangeCheck())
_Text.stringValue = IGUtils.TruncateForLabel(text);
GUILayout.EndHorizontal();
// Include in Build.
var includeSome = false;
var excludeSome = false;
foreach (var target in targets)
{
if ((target as IComment).IncludeInBuild)
includeSome = true;
else
excludeSome = true;
}
if (includeSome)
{
if (excludeSome)
UnityEditor.EditorGUI.showMixedValue = true;
UnityEditor.EditorGUI.BeginChangeCheck();
includeSome = UnityEditor.EditorGUILayout.Toggle("Include in Build?", true);
if (UnityEditor.EditorGUI.EndChangeCheck())
foreach (var target in targets)
(target as IComment).IncludeInBuild = includeSome;
UnityEditor.EditorGUI.showMixedValue = false;
}
serializedObject.ApplyModifiedProperties();
}
/************************************************************************************************************************/
[UnityEditor.MenuItem(EditorStrings.CommentAssetIncludeInBuild)]
[UnityEditor.MenuItem(EditorStrings.CommentComponentIncludeInBuild)]
private static void IncludeInBuild(UnityEditor.MenuCommand command)
{
(command.context as IComment).IncludeInBuild = true;
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c9a7bc3a0004ebd4b83cce2c97e8f510
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,257 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2025 Kybernetik //
// Inspector Gadgets // https://kybernetik.com.au/animancer // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Globalization;
using UnityEditor;
//namespace Animancer.Editor
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only]
/// A system for formatting floats as strings that fit into a limited area and storing the results so they can be
/// reused to minimise the need for garbage collection, particularly for string construction.
/// </summary>
/// <example>
/// With <c>"x"</c> as the suffix:
/// <list type="bullet">
/// <item><c>1.111111</c> could instead show <c>1.111~x</c>.</item>
/// <item><c>0.00001234567</c> would normally show <c>1.234567e-05</c>, but with this it instead shows <c>0~x</c>
/// because very small values generally aren't useful.</item>
/// <item><c>99999999</c> shows <c>1e+08x</c> because very large values are already approximations and trying to
/// format them correctly would be very difficult.</item>
/// </list>
/// This system only affects the display value. Once you select a field, it shows its actual value.
/// </example>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CompactUnitConversionCache
/// https://kybernetik.com.au/inspector-gadgets/api/InspectorGadgets.Editor/CompactUnitConversionCache
///
public class CompactUnitConversionCache
{
/************************************************************************************************************************/
/// <summary>Should the fields show approximations if the value is too long for the GUI?</summary>
public static bool ShowApproximations
// => AnimancerSettings.AnimationTimeFields.showApproximations;
=> PropertyDrawers.TransformPropertyDrawer.ShowApproximations;
/************************************************************************************************************************/
/// <summary>The suffix added to the end of each value.</summary>
public readonly string Suffix;
/// <summary>The <see cref="Suffix"/> with a <c>~</c> before it to indicate an approximation.</summary>
public readonly string ApproximateSuffix;
/// <summary>The value <c>0</c> with the <see cref="Suffix"/>.</summary>
public readonly string ConvertedZero;
/// <summary>The value <c>0</c> with the <see cref="ApproximateSuffix"/>.</summary>
public readonly string ConvertedSmallPositive;
/// <summary>The value <c>-0</c> with the <see cref="ApproximateSuffix"/>.</summary>
public readonly string ConvertedSmallNegative;
/// <summary>The pixel width of the <see cref="Suffix"/> when drawn by <see cref="EditorStyles.numberField"/>.</summary>
public float _SuffixWidth;
/// <summary>The caches for each character count.</summary>
/// <remarks><c>this[x]</c> is a cache that outputs strings with <c>x</c> characters.</remarks>
private List<ConversionCache<float, string>>
Caches = new List<ConversionCache<float, string>>();
/************************************************************************************************************************/
/// <summary>Strings mapped to the width they would require for a <see cref="EditorStyles.numberField"/>.</summary>
private static ConversionCache<string, float> _WidthCache;
/// <summary>Padding around the text in a <see cref="EditorStyles.numberField"/>.</summary>
public static float _FieldPadding;
/// <summary>The pixel width of the <c>~</c> character when drawn by <see cref="EditorStyles.numberField"/>.</summary>
public static float _ApproximateSymbolWidth;
/// <summary>The character(s) used to separate decimal values in the current OS language.</summary>
public static string _DecimalSeparator;
/// <summary>Values smaller than this become <c>0~</c> or <c>-0~</c>.</summary>
public const float
SmallExponentialThreshold = 0.0001f;
/// <summary>Values larger than this can't be approximated.</summary>
public const float
LargeExponentialThreshold = 9999999f;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="CompactUnitConversionCache"/>.</summary>
public CompactUnitConversionCache(string suffix)
{
Suffix = suffix;
ApproximateSuffix = "~" + Suffix;
ConvertedZero = "0" + Suffix;
ConvertedSmallPositive = "0" + ApproximateSuffix;
ConvertedSmallNegative = "-0" + ApproximateSuffix;
}
/************************************************************************************************************************/
/// <summary>
/// Returns a cached string representing the `value` trimmed to fit within the `width` (if necessary) and with
/// the <see cref="Suffix"/> added on the end.
/// </summary>
public string Convert(float value, float width)
{
if (value == 0)
return ConvertedZero;
if (!ShowApproximations)
return GetCache(0).Convert(value);
if (value < SmallExponentialThreshold &&
value > -SmallExponentialThreshold)
return value > 0 ? ConvertedSmallPositive : ConvertedSmallNegative;
var index = CalculateCacheIndex(value, width);
return GetCache(index).Convert(value);
}
/************************************************************************************************************************/
/// <summary>Calculate the index of the cache to use for the given parameters.</summary>
private int CalculateCacheIndex(float value, float width)
{
//if (value > LargeExponentialThreshold ||
// value < -LargeExponentialThreshold)
// return 0;
var valueString = value.ToStringCached();
// It the approximated string wouldn't be shorter than the original, don't approximate.
if (valueString.Length < 2 + ApproximateSuffix.Length)
return 0;
if (_SuffixWidth == 0)
{
if (_WidthCache == null)
{
_WidthCache = ConversionCache.CreateWidthCache(EditorStyles.numberField);
_FieldPadding = EditorStyles.numberField.padding.horizontal;
_ApproximateSymbolWidth = _WidthCache.Convert("~") - _FieldPadding;
}
if (!string.IsNullOrEmpty(Suffix))
_SuffixWidth = _WidthCache.Convert(Suffix);
}
// If the field is wide enough to fit the full value, don't approximate.
width -= _FieldPadding + _ApproximateSymbolWidth * 0.75f;
var valueWidth = _WidthCache.Convert(valueString) + _SuffixWidth;
if (valueWidth <= width)
return 0;
// If the number of allowed characters would include the full value, don't approximate.
var suffixedLength = valueString.Length + Suffix.Length;
var allowedCharacters = (int)(suffixedLength * width / valueWidth);
if (allowedCharacters + 2 >= suffixedLength)
return 0;
return allowedCharacters;
}
/************************************************************************************************************************/
/// <summary>Creates and returns a cache for the specified `characterCount`.</summary>
private ConversionCache<float, string> GetCache(int characterCount)
{
if (characterCount < 0)
characterCount = 0;
while (Caches.Count <= characterCount)
Caches.Add(null);
var cache = Caches[characterCount];
if (cache == null)
{
if (characterCount == 0)
{
cache = new ConversionCache<float, string>((value) =>
{
return value.ToStringCached() + Suffix;
});
}
else
{
cache = new ConversionCache<float, string>((value) =>
{
var valueString = value.ToStringCached();
if (value > LargeExponentialThreshold ||
value < -LargeExponentialThreshold)
goto IsExponential;
if (_DecimalSeparator == null)
_DecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
var decimalIndex = valueString.IndexOf(_DecimalSeparator);
if (decimalIndex < 0 || decimalIndex > characterCount)
goto IsExponential;
// Not exponential.
return valueString[..characterCount] + ApproximateSuffix;
IsExponential:
var digits = Math.Max(0, characterCount - ApproximateSuffix.Length - 1);
var format = GetExponentialFormat(digits);
valueString = value.ToString(format);
TrimExponential(ref valueString);
return valueString + Suffix;
});
}
Caches[characterCount] = cache;
}
return cache;
}
/************************************************************************************************************************/
private static List<string> _ExponentialFormats;
/// <summary>Returns a format string to include the specified number of `digits` in an exponential number.</summary>
public static string GetExponentialFormat(int digits)
{
if (_ExponentialFormats == null)
_ExponentialFormats = new List<string>();
while (_ExponentialFormats.Count <= digits)
_ExponentialFormats.Add("g" + _ExponentialFormats.Count);
return _ExponentialFormats[digits];
}
/************************************************************************************************************************/
private static void TrimExponential(ref string valueString)
{
var length = valueString.Length;
if (length <= 4 ||
valueString[length - 4] != 'e' ||
valueString[length - 2] != '0')
return;
valueString =
valueString[..(length - 2)] +
valueString[length - 1];
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1053dcd7e215b242a49baa598d052a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,135 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
//#define LOG_CONVERSION_CACHE
using System;
using System.Collections.Generic;
using UnityEngine;
//namespace Animancer.Editor
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only]
/// A simple system for converting objects and storing the results so they can be reused to minimise the need for
/// garbage collection, particularly for string construction.
/// </summary>
/// <remarks>This class doesn't use any Editor-Only functionality, but it's unlikely to be useful at runtime.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ConversionCache_2
/// https://kybernetik.com.au/animancer/api/InspectorGadgets.Editor/ConversionCache_2
///
public class ConversionCache<TKey, TValue>
{
/************************************************************************************************************************/
private class CachedValue
{
public int lastFrameAccessed;
public TValue value;
}
/************************************************************************************************************************/
private readonly Dictionary<TKey, CachedValue>
Cache = new Dictionary<TKey, CachedValue>();
private readonly List<TKey>
Keys = new List<TKey>();
private readonly Func<TKey, TValue>
Converter;
private int _LastCleanupFrame;
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="ConversionCache{TKey, TValue}"/> which uses the specified delegate to convert values.
/// </summary>
public ConversionCache(Func<TKey, TValue> converter) => Converter = converter;
/************************************************************************************************************************/
/// <summary>
/// If a value has already been cached for the specified `key`, return it. Otherwise create a new one using
/// the delegate provided in the constructor and cache it.
/// <para></para>
/// If the `key` is <c>null</c>, this method returns the default <typeparamref name="TValue"/>.
/// </summary>
/// <remarks>This method also periodically removes values that have not been used recently.</remarks>
public TValue Convert(TKey key)
{
if (key == null)
return default;
CachedValue cached;
// The next time a value is retrieved after at least 100 frames, clear out any old ones.
var frame = Time.frameCount;
if (_LastCleanupFrame + 100 < frame)
{
for (int i = Keys.Count - 1; i >= 0; i--)
{
var checkKey = Keys[i];
if (!Cache.TryGetValue(checkKey, out cached) ||
cached.lastFrameAccessed <= _LastCleanupFrame)
{
Cache.Remove(checkKey);
Keys.RemoveAt(i);
}
}
_LastCleanupFrame = frame;
}
if (!Cache.TryGetValue(key, out cached))
{
Cache.Add(key, cached = new CachedValue { value = Converter(key) });
Keys.Add(key);
}
cached.lastFrameAccessed = frame;
return cached.value;
}
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] Utilities for <see cref="ConversionCache{TKey, TValue}"/>.</summary>
/// <remarks>This class doesn't use any Editor-Only functionality, but it's unlikely to be useful at runtime.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ConversionCache
/// https://kybernetik.com.au/animancer/api/InspectorGadgets.Editor/ConversionCache
///
public static class ConversionCache
{
/************************************************************************************************************************/
/// <summary>
/// Creates a <see cref="ConversionCache{TKey, TValue}"/> for calculating the GUI width occupied by text using
/// the specified `style`.
/// </summary>
public static ConversionCache<string, float> CreateWidthCache(GUIStyle style)
=> new ConversionCache<string, float>((text) => style.CalculateWidth(text));
/************************************************************************************************************************/
// The "g" format gives a lower case 'e' for exponentials instead of upper case 'E'.
private static readonly ConversionCache<float, string>
FloatToString = new ConversionCache<float, string>((value) => $"{value:g}");
/// <summary>[Animancer Extension]
/// Calls <see cref="float.ToString(string)"/> using <c>"g"</c> as the format and caches the result.
/// </summary>
public static string ToStringCached(this float value)
=> FloatToString.Convert(value);
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 97c89c786ac86704ba5916cf1e186019
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,374 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Attributes;
using InspectorGadgets.Editor.PropertyDrawers;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor
{
#if ! DISABLE_IG_COMPONENT_EDITOR
[CustomEditor(typeof(Component), true, isFallback = true), CanEditMultipleObjects]
#endif
internal sealed class ComponentEditor : Editor<Component>
{
// This is here because having it in the generic base class would make a new instance for each generic subtype.
public static readonly AutoPrefs.EditorBool
HideScriptProperty = new AutoPrefs.EditorBool(EditorStrings.PrefsKeyPrefix + "HideScriptProperty", true);
}
#if ! DISABLE_IG_SCRIPTABLE_OBJECT_EDITOR
[CustomEditor(typeof(ScriptableObject), true, isFallback = true), CanEditMultipleObjects]
#endif
internal sealed class ScriptableObjectEditor : Editor<ScriptableObject>
{
/************************************************************************************************************************/
/// <summary>
/// Called by the Unity editor to draw the custom inspector GUI elements.
/// <para></para>
/// Draws the regular inspector then adds a message explaining that changes in Play Mode will persist.
/// </summary>
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (target != null &&
EditorApplication.isPlayingOrWillChangePlaymode &&
EditorUtility.IsPersistent(target))
{
EditorGUILayout.HelpBox("This is an asset, not a scene object," +
" which means that any changes you make to it are permanent" +
" and will NOT be undone when you exit Play Mode.", MessageType.Warning);
}
}
/************************************************************************************************************************/
}
#if UNITY_ANIMATION && ! DISABLE_IG_STATE_MACHINE_BEHAVIOUR_EDITOR
[CustomEditor(typeof(StateMachineBehaviour), true, isFallback = true), CanEditMultipleObjects]
internal sealed class StateMachineBehaviourEditor : Editor<StateMachineBehaviour> { }
#endif
/************************************************************************************************************************/
/// <summary>[Pro-Only] [Editor-Only]
/// Base class to derive custom editors from, with a bunch of additional features on top of Unity's base
/// <see cref="UnityEditor.Editor"/> class.
/// </summary>
/// <remarks>
/// Doesn't draw the target's "Script" field to save inspector space and reduce clutter.
/// <para></para>
/// You can Middle Click anywhere in the inspector area to open the script in your script editor
/// or Ctrl + Middle Click to open its editor script (or create one if none exists already).
/// <para></para>
/// Provides type-casted versions of <see cref="UnityEditor.Editor.target"/> and
/// <see cref="UnityEditor.Editor.targets"/> so you don't always have to do it yourself
/// (<see cref="Target"/> and <see cref="Targets"/> respectively).
/// </remarks>
public abstract class Editor<T> : MissingScriptEditor where T : class
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[NonSerialized] private bool _HasReflectedTarget;
[NonSerialized] private List<BaseInspectableAttributeDrawer> _Inspectables;
[NonSerialized] private Action _OnInspectorGUI, _AfterInspectorGUI;
[NonSerialized] private MethodCache.OverridePropertyGUIMethod _OverridePropertyGUI;
/************************************************************************************************************************/
[NonSerialized] private bool _HasTarget;
[NonSerialized] private T _Target;
[NonSerialized] private T[] _Targets;
/// <summary>The object being inspected (<see cref="UnityEditor.Editor.target"/> casted to T).</summary>
public T Target
{
get
{
if (!_HasTarget)
{
_HasTarget = true;
_Target = target as T;
}
return _Target;
}
}
/// <summary>An array of all the objects being inspected (<see cref="UnityEditor.Editor.targets"/> casted to T).</summary>
public T[] Targets
{
get
{
if (_Targets == null)
{
var count = targets.Length;
_Targets = new T[count];
_Targets[0] = Target;
var i = 1;
for (; i < count; i++)
_Targets[i] = targets[i] as T;
}
return _Targets;
}
}
/************************************************************************************************************************/
/// <summary>The editor currently being drawn.</summary>
public static Editor<T> Current { get; private set; }
/// <summary>
/// The object being inspected (<see cref="UnityEditor.Editor.target"/> casted to T) by the editor currently
/// being drawn.
/// </summary>
public static T CurrentTarget => Current.Target;
/// <summary>
/// An array of all the objects being inspected (<see cref="UnityEditor.Editor.targets"/> casted to T) by the
/// editor currently being drawn.
/// </summary>
public static T[] CurrentTargets => Current.Targets;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/// <summary>
/// Gathers all of the target's inspectables (<see cref="ButtonAttribute"/> and <see cref="LabelAttribute"/>)
/// and checks for inspector GUI events in the script.
/// </summary>
protected override void OnEnable()
{
base.OnEnable();
if (target != null && !_HasReflectedTarget)
{
_HasReflectedTarget = true;
_Inspectables = Inspectables.Gather(target.GetType());
_OnInspectorGUI = MethodCache.OnInspectorGUI.GetDelegate(target);
if (_OnInspectorGUI == null)
{
_AfterInspectorGUI = MethodCache.AfterInspectorGUI.GetDelegate(target);
_OverridePropertyGUI = MethodCache.OverridePropertyGUI.GetDelegate(target);
}
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Execution
/************************************************************************************************************************/
/// <summary>
/// Draws the target's regular inspector followed by any extra inspectables
/// (<see cref="ButtonAttribute"/> and <see cref="LabelAttribute"/>), and responds to Middle
/// Click events.
/// <para></para>
/// To modify or replace just the regular inspector and keep the extra features of <see cref="Editor{T}"/>,
/// override <see cref="DoPropertiesAndInspectables()"/> instead of <see cref="OnInspectorGUI"/>.
/// </summary>
public override void OnInspectorGUI()
{
if (target == null || serializedObject.targetObject == null)
{
base.OnInspectorGUI();
return;
}
Current = this;
if (_OnInspectorGUI != null)
_OnInspectorGUI();
else
DoInspectorGUI();
}
/************************************************************************************************************************/
/// <summary>Draws the inspector GUI of the <see cref="Current"/> editor.</summary>
public static void DoInspectorGUI()
{
var current = Current;
var rect = EditorGUILayout.BeginVertical();
current.serializedObject.Update();
current.DoPropertiesAndInspectables();
current._AfterInspectorGUI?.Invoke();
if (current.serializedObject.ApplyModifiedProperties())
current.OnPropertyModified();
EditorGUILayout.EndVertical();
current.CheckMiddleClick(rect);
}
/************************************************************************************************************************/
/// <summary>Draws all of the target's serialized properties and inspectables.</summary>
public virtual void DoPropertiesAndInspectables()
{
//DrawDefaultInspector(); return;
if (!ComponentEditor.HideScriptProperty && ScriptProperty != null)
DoPropertyGUI(ScriptProperty);
var inspectableIndex = 0;
var inspectableCount = _Inspectables != null ? _Inspectables.Count : 0;
for (int i = 0; i < OtherProperties.Count; i++)
{
// Draw any inspectables with the current index.
while (inspectableIndex < inspectableCount)
{
var inspectable = _Inspectables[inspectableIndex];
if (inspectable.DisplayIndex <= i)
{
if (inspectable.When.IsNow())
inspectable.OnGUI(targets);
inspectableIndex++;
}
else break;
}
DoPropertyGUI(OtherProperties[i]);
}
// Draw any remaining inspectables.
while (inspectableIndex < inspectableCount)
{
var inspectable = _Inspectables[inspectableIndex];
if (inspectable.When.IsNow())
inspectable.OnGUI(targets);
inspectableIndex++;
}
}
/************************************************************************************************************************/
private static readonly GUIContent PropertyLabel = new GUIContent();
private void DoPropertyGUI(SerializedProperty property)
{
PropertyLabel.text = property.displayName;
PropertyLabel.tooltip = property.tooltip;
if (_OverridePropertyGUI != null)
{
if (_OverridePropertyGUI(property, PropertyLabel))
return;
}
EditorGUILayout.PropertyField(property, PropertyLabel, true);
}
/************************************************************************************************************************/
/// <summary>
/// This method is called if any of the target's serialized members are modified during
/// <see cref="DoInspectorGUI()"/>.
/// </summary>
/// <remarks>This method does nothing unless overridden.</remarks>
public virtual void OnPropertyModified() { }
/************************************************************************************************************************/
/// <summary>
/// Checks if the current event is a Middle Click to open the script in the user's script editor application,
/// or Ctrl + Middle Click to open or create its custom inspector script.
/// </summary>
public virtual void CheckMiddleClick(Rect area)
{
area.yMin -= 16;// Move the top of the rect up to include the component's title bar.
var currentEvent = Event.current;
if (currentEvent.type == EventType.MouseUp &&
currentEvent.button == 2 &&
area.Contains(currentEvent.mousePosition))
{
currentEvent.Use();
EditorApplication.delayCall += () =>
{
if (currentEvent.control)// Ctrl + Middle click to open or create the editor script.
{
var script = MonoScript.FromScriptableObject(this);
if (script != null && GetType().Assembly != typeof(Editor<>).Assembly)
AssetDatabase.OpenAsset(script);
else
IGEditorUtils.CreateEditorScript(target);
}
else if (currentEvent.shift)// Shift + Middle click to ping the script asset.
{
IGEditorUtils.PingScriptAsset(target);
}
else// Middle click to open the script.
{
var behaviour = target as MonoBehaviour;
if (behaviour != null)
{
AssetDatabase.OpenAsset(MonoScript.FromMonoBehaviour(behaviour));
}
else
{
var scriptable = target as ScriptableObject;
if (scriptable != null)
AssetDatabase.OpenAsset(MonoScript.FromScriptableObject(scriptable));
}
}
};
}
}
/************************************************************************************************************************/
/// <summary>[Pro-Only] Draw all <see cref="InspectableAttribute"/> members.</summary>
public void DoAllInspectables()
{
for (int i = 0; i < _Inspectables.Count; i++)
{
var inspectable = _Inspectables[i];
if (inspectable.When.IsNow())
inspectable.OnGUI(targets);
}
}
/************************************************************************************************************************/
/// <summary>
/// Records the current state of the <see cref="Targets"/> so that any subsequent changes can be undone
/// (reverted back to the recorded state).
/// </summary>
public void RecordTargetUndo(string undoName)
{
Undo.RecordObjects(targets, undoName);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e34974defec2cb458721eb2ea92b54c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor
{
/// <summary>[Pro-Only] [Editor-Only]
/// A cache for manually created <see cref="UnityEditor.Editor"/>s which handles their cleanup automatically.
/// </summary>
internal static class Editors
{
/************************************************************************************************************************/
private static readonly HashSet<UnityEditor.Editor>
AllEditors = new HashSet<UnityEditor.Editor>();
/************************************************************************************************************************/
public static UnityEditor.Editor Create(Object target)
{
var editor = UnityEditor.Editor.CreateEditor(target);
AllEditors.Add(editor);
return editor;
}
public static UnityEditor.Editor Create(Object[] targets)
{
var editor = UnityEditor.Editor.CreateEditor(targets);
AllEditors.Add(editor);
return editor;
}
/************************************************************************************************************************/
public static void Destroy(UnityEditor.Editor editor)
{
if (!(editor is null))
AllEditors.Remove(editor);
Object.DestroyImmediate(editor);
}
/************************************************************************************************************************/
static Editors()
{
Selection.selectionChanged += DestroyOldEditors;
AssemblyReloadEvents.beforeAssemblyReload += DestroyOldEditors;
}
/************************************************************************************************************************/
private static void DestroyOldEditors()
{
foreach (var editor in AllEditors)
{
Object.DestroyImmediate(editor);
}
AllEditors.Clear();
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 14c7041aa6c571340877eb622c52ee19
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,361 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only]
/// A custom Inspector for <see cref="MonoBehaviour"/> but not its children, so it should only ever get used on
/// missing scripts.
/// </summary>
#if !DISABLE_IG_MONOBEHAVIOUR_INSPECTOR
[CustomEditor(typeof(MonoBehaviour), isFallback = true)]
#endif
public class MissingScriptEditor : UnityEditor.Editor
{
/************************************************************************************************************************/
[NonSerialized]
private SerializedProperty _ScriptProperty;
/// <summary>The target's "m_Script" property.</summary>
protected SerializedProperty ScriptProperty
{
get
{
GatherSerializedProperties();
return _ScriptProperty;
}
}
/************************************************************************************************************************/
[NonSerialized]
private List<SerializedProperty> _OtherProperties;
/// <summary>All of the target's properties other than "m_Script".</summary>
protected List<SerializedProperty> OtherProperties
{
get
{
GatherSerializedProperties();
return _OtherProperties;
}
}
/************************************************************************************************************************/
#region Property Gathering and Analysis
/************************************************************************************************************************/
/// <summary>
/// Gathers the target's properties. If its script is missing, this method tries to find other similar scripts.
/// </summary>
protected virtual void OnEnable()
{
if (target != null ||
MissingScriptWindow.Instance == null ||
ScriptProperty == null)
return;
if (MissingScriptWindow.Instance.AutoDestroy)
{
DestroyProperly(targets, false);
return;
}
if (MissingScriptWindow.Instance.HasTarget)
return;
// Get the referenced script's name (if possible).
var scriptReference = _ScriptProperty.objectReferenceValue;
var scriptName = scriptReference != null ? scriptReference.name : null;
var fields = new List<FieldInfo>();
var scripts = MissingScriptWindow.AllScripts;
var similarityRatings = new int[scripts.Length];
// Go through each script and calculate its similarity to the missing script.
for (int i = 0; i < scripts.Length; i++)
{
var script = scripts[i];
var scriptType = script.GetClass();
if (scriptType == null)
continue;
// Scripts with the same name have max similarity.
if (script.name == scriptName)
{
similarityRatings[i] = int.MaxValue;
continue;
}
// Otherwise count the number of fields with names and types matching the serialized properties.
fields.Clear();
GatherAllFields(scriptType, fields);
similarityRatings[i] = CountSimilarFields(fields);
}
var hasSerializedProperties = scriptName != null || _OtherProperties.Count > 0;
MissingScriptWindow.Instance.SetScripts(target, _ScriptProperty, hasSerializedProperties, similarityRatings);
}
/************************************************************************************************************************/
private Object[] _GatheredPropertiesFrom;
private void GatherSerializedProperties()
{
var targets = this.targets;
if (_OtherProperties != null)
{
if (_GatheredPropertiesFrom == null ||
_GatheredPropertiesFrom.Length != targets.Length)
goto Gather;
for (int i = 0; i < _GatheredPropertiesFrom.Length; i++)
if (!ReferenceEquals(_GatheredPropertiesFrom[i], targets[i]))
goto Gather;
return;
}
Gather:
if (_GatheredPropertiesFrom == null || _GatheredPropertiesFrom.Length != targets.Length)
_GatheredPropertiesFrom = new Object[targets.Length];
for (int i = 0; i < _GatheredPropertiesFrom.Length; i++)
{
_GatheredPropertiesFrom[i] = targets[i];
}
_OtherProperties = new List<SerializedProperty>();
try
{
var iterator = serializedObject.GetIterator();
if (!iterator.NextVisible(true))
return;
do
{
var property = iterator.Copy();
if (property.propertyPath == "m_Script")
_ScriptProperty = property;
else
_OtherProperties.Add(property);
}
while (iterator.NextVisible(false));
}
catch { }
if (_ScriptProperty == null && target is MonoBehaviour)
Debug.Log("Script property was not found on " + target, target);
}
/************************************************************************************************************************/
private static void GatherAllFields(Type type, List<FieldInfo> fields)
{
var typeFields = type.GetFields(IGEditorUtils.InstanceBindings);
for (int i = 0; i < typeFields.Length; i++)
{
var field = typeFields[i];
if (field.IsDefined(typeof(NonSerializedAttribute), true) ||
field.IsDefined(typeof(HideInInspector), true))
continue;
if (!field.IsPublic && !field.IsDefined(typeof(SerializeField), true))
continue;
fields.Add(field);
}
}
/************************************************************************************************************************/
private int CountSimilarFields(List<FieldInfo> fields)
{
var count = 0;
for (int i = 0; i < _OtherProperties.Count; i++)
{
var property = _OtherProperties[i];
for (int j = 0; j < fields.Count; j++)
{
var field = fields[j];
if (IsMatch(property, field))
{
count++;
break;
}
}
}
return count;
}
/************************************************************************************************************************/
private static bool IsMatch(SerializedProperty property, FieldInfo field)
{
if (field.Name != property.propertyPath)
return false;
switch (property.propertyType)
{
case SerializedPropertyType.Integer: return typeof(int).IsAssignableFrom(field.FieldType) || typeof(uint).IsAssignableFrom(field.FieldType);
case SerializedPropertyType.Boolean: return field.FieldType == typeof(bool);
case SerializedPropertyType.Float: return typeof(float).IsAssignableFrom(field.FieldType);
case SerializedPropertyType.String: return field.FieldType == typeof(string);
case SerializedPropertyType.Color: return field.FieldType == typeof(Color) || field.FieldType == typeof(Color32);
case SerializedPropertyType.ObjectReference: return typeof(Object).IsAssignableFrom(field.FieldType);
case SerializedPropertyType.LayerMask: return field.FieldType == typeof(LayerMask);
case SerializedPropertyType.Enum: return field.FieldType.IsEnum;// This doesn't confirm that the enum type actually matches the field. Would need to check property.enumDisplayNames.
case SerializedPropertyType.Vector2: return field.FieldType == typeof(Vector2);
case SerializedPropertyType.Vector3: return field.FieldType == typeof(Vector3);
case SerializedPropertyType.Vector4: return field.FieldType == typeof(Vector4);
case SerializedPropertyType.Rect: return field.FieldType == typeof(Rect);
case SerializedPropertyType.Generic: return field.FieldType.HasElementType || typeof(IEnumerable).IsAssignableFrom(field.FieldType);
case SerializedPropertyType.Character: return field.FieldType == typeof(char);
case SerializedPropertyType.AnimationCurve: return field.FieldType == typeof(AnimationCurve);
case SerializedPropertyType.Bounds: return field.FieldType == typeof(Bounds);
case SerializedPropertyType.Gradient: return field.FieldType == typeof(Gradient);
case SerializedPropertyType.Quaternion: return field.FieldType == typeof(Quaternion);
case SerializedPropertyType.Vector2Int: return field.FieldType == typeof(Vector2Int);
case SerializedPropertyType.Vector3Int: return field.FieldType == typeof(Vector3Int);
case SerializedPropertyType.RectInt: return field.FieldType == typeof(RectInt);
case SerializedPropertyType.BoundsInt: return field.FieldType == typeof(BoundsInt);
default:
case SerializedPropertyType.ArraySize:
case SerializedPropertyType.ExposedReference:
case SerializedPropertyType.FixedBufferSize:
break;
}
return false;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>
/// Draws the target's inspector with a message indicating that the script is missing.
/// </summary>
public override void OnInspectorGUI()
{
if (target != null)
{
base.OnInspectorGUI();
return;
}
serializedObject.Update();
// Draw the script field and a help box.
GUI.enabled = true;
EditorGUILayout.PropertyField(ScriptProperty, true);
EditorGUILayout.HelpBox(EditorUtility.scriptCompilationFailed ?
"The associated script can not be loaded. Please fix any compile errors and assign a valid script." :
"The associated script can not be loaded. Please assign a valid script.", MessageType.Warning);
// Draw all other serialized properties disabled.
GUI.enabled = false;
for (int i = 0; i < _OtherProperties.Count; i++)
{
EditorGUILayout.PropertyField(_OtherProperties[i], true);
}
GUI.enabled = true;
// Apply modifications in case the user dragged a new script into the script field.
serializedObject.ApplyModifiedProperties();
if (GUILayout.Button(target is Component ? "Remove Component" : "Destroy This"))
{
EditorApplication.delayCall += () => DestroyProperly(targets, true);
}
if (GUILayout.Button("Find and Fix Missing Scripts"))
{
EditorWindow.GetWindow<MissingScriptWindow>();
OnEnable();
}
}
/************************************************************************************************************************/
/// <summary>
/// Indicates to the <see cref="MissingScriptWindow"/> that the target has been deselected.
/// </summary>
protected virtual void OnDisable()
{
if (MissingScriptWindow.Instance != null)
MissingScriptWindow.Instance.ClearTarget();
}
/************************************************************************************************************************/
/// <summary>Destroys the `targets`. Shows a confirmation dialog before destroying assets.</summary>
public static void DestroyProperly(Object[] targets, bool confirmAssetDeletion)
{
for (int i = 0; i < targets.Length; i++)
DestroyProperly(targets[i], confirmAssetDeletion);
}
/// <summary>Destroys the `target`. Shows a confirmation dialog before destroying assets.</summary>
public static void DestroyProperly(Object target, bool confirmAssetDeletion)
{
var assetPath = AssetDatabase.GetAssetPath(target);
if (!string.IsNullOrEmpty(assetPath))
{
var isMainAsset = AssetDatabase.IsMainAsset(target);
var message = isMainAsset ?
assetPath :
$"{target.name} (sub asset of '{assetPath}')";
message += "\n\nYou cannot undo this action.";
if (!confirmAssetDeletion || EditorUtility.DisplayDialog("Delete selected asset?", message, "Delete", "Cancel"))
{
if (isMainAsset)
{
AssetDatabase.DeleteAsset(assetPath);
}
else
{
DestroyImmediate(target, true);
AssetDatabase.ImportAsset(assetPath);
}
}
}
else
{
Undo.DestroyObjectImmediate(target);
}
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dc671b99ba021504cbfef69a041c060b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,577 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
// The only way to currently determine the name and serialized fields of a missing script is via a custom inspector
// for MonoBehaviour scripts (but not other classes that inherit from it) which needs each object to be selected in
// turn. If anyone knows of a better way to do it, please email mail@kybernetik.com.au.
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only]
/// A utility for tracking down and fixing missing script references.
/// </summary>
internal sealed class MissingScriptWindow : EditorWindow, IHasCustomMenu
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
public static MissingScriptWindow Instance { get; private set; }
public bool AutoDestroy { get; private set; }
// Gathered Assets.
[NonSerialized] private SceneSetup[] _OriginalSceneSetup;
[NonSerialized] private readonly List<string> MissingAssetPaths = new List<string>();
[NonSerialized] private readonly List<GameObject> MissingPrefabs = new List<GameObject>();
[NonSerialized] private readonly List<string> ScenePaths = new List<string>();
[NonSerialized] private readonly List<GameObject> SceneObjects = new List<GameObject>();
[NonSerialized] private int _SceneObjectIndex;
// Selected Target Details.
[NonSerialized] private Object _Target;
[NonSerialized] private SerializedProperty _ScriptProperty;
[NonSerialized] private bool _HasSerializedProperties;
[NonSerialized] private int[] _SimilarityRatings;
[NonSerialized] private Vector2 _ScrollPosition;
[NonSerialized] private bool _HasLoadedAnotherScene;
[NonSerialized] private bool _ConfirmResetScene;
/************************************************************************************************************************/
public bool HasTarget => _ScriptProperty != null;
/************************************************************************************************************************/
private static MonoScript[] _AllScripts;
public static MonoScript[] AllScripts
{
get
{
if (_AllScripts == null)
_AllScripts = MonoImporter.GetAllRuntimeMonoScripts();
return _AllScripts;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region EditorWindow Methods
/************************************************************************************************************************/
private void OnEnable()
{
titleContent = new GUIContent("Missing Script Window");
Instance = this;
Selection.selectionChanged += OnSelectionChanged;
}
/************************************************************************************************************************/
private void OnGUI()
{
const float MinWidth = 300;
var fieldWidth = position.width < MinWidth ?
position.width * 0.5f :
MinWidth * 0.5f + Mathf.Pow(position.width - MinWidth, 0.65f);
EditorGUIUtility.labelWidth = position.width - fieldWidth;
if (_OriginalSceneSetup == null)
GatherScenesAndMissingAsssets();
if (EditorUtility.scriptCompilationFailed)
EditorGUILayout.HelpBox("The project currently has compile errors which should be fixed" +
" before trying to fix missing script references.", MessageType.Warning);
if (DoSelectNextButtonGUI())
return;
if (_ScriptProperty == null)
{
DoGatheredAssetsGUI();
return;
}
_ScrollPosition = GUILayout.BeginScrollView(_ScrollPosition);
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(_ScriptProperty, true);
if (EditorGUI.EndChangeCheck())
{
var script = _ScriptProperty.objectReferenceValue as MonoScript;
if (script != null && script.GetClass() != null)
_ScriptProperty.serializedObject.ApplyModifiedProperties();
else
_ScriptProperty.serializedObject.Update();
}
if (!(_Target is null) && GUILayout.Button("Destroy Component"))
DestroyTargetAndSelectNext();
DoMissingScriptButtons();
GUILayout.EndScrollView();
}
/************************************************************************************************************************/
private void Update()
{
if (AutoDestroy)
{
if (_Target is null)
{
AutoDestroy = false;
return;
}
DestroyTargetAndSelectNext();
}
}
/************************************************************************************************************************/
private void OnDestroy()
{
Selection.selectionChanged -= OnSelectionChanged;
ResetSceneSetup();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
void IHasCustomMenu.AddItemsToMenu(GenericMenu menu)
{
menu.AddItem(new GUIContent("Destroy All Missing Scripts"), false, DestroyAllMissingScripts);
}
/************************************************************************************************************************/
private void DestroyAllMissingScripts()
{
const string Message = "Are you sure you want to destroy all missing scripts" +
" in scenes and assets everywhere in your project?" +
"\n\nYou cannot undo this action.";
if (!EditorUtility.DisplayDialog("Destroy All Missing Scripts?", Message, "Destroy", "Cancel"))
return;
AutoDestroy = true;
DeleteAllMissingAssets();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
private void OnSelectionChanged()
{
if (_HasLoadedAnotherScene)
_ConfirmResetScene = true;
Repaint();
}
/************************************************************************************************************************/
private void GatherScenesAndMissingAsssets()
{
_OriginalSceneSetup = EditorSceneManager.GetSceneManagerSetup();
_HasLoadedAnotherScene = false;
_ConfirmResetScene = false;
MissingAssetPaths.Clear();
MissingPrefabs.Clear();
ScenePaths.Clear();
var guids = AssetDatabase.FindAssets($"t:{nameof(Object)}");
for (int i = 0; i < guids.Length; i++)
{
var path = AssetDatabase.GUIDToAssetPath(guids[i]);
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
if (asset == null)
{
MissingAssetPaths.Add(path);
}
}
guids = AssetDatabase.FindAssets($"t:{nameof(GameObject)}");
for (int i = 0; i < guids.Length; i++)
{
var path = AssetDatabase.GUIDToAssetPath(guids[i]);
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (HasAnyMissingComponents(prefab))
{
MissingPrefabs.Add(prefab);
}
}
guids = AssetDatabase.FindAssets($"t:{nameof(SceneAsset)}");
for (int i = 0; i < guids.Length; i++)
{
var path = AssetDatabase.GUIDToAssetPath(guids[i]);
ScenePaths.Add(path);
}
}
/************************************************************************************************************************/
private void DoGatheredAssetsGUI()
{
_ScrollPosition = GUILayout.BeginScrollView(_ScrollPosition);
DoMissingAssetsGUI();
GUILayout.Label("Prefabs with missing scripts: " + MissingPrefabs.Count, EditorStyles.boldLabel);
for (int i = 0; i < MissingPrefabs.Count; i++)
{
var prefab = MissingPrefabs[i];
EditorGUILayout.ObjectField(AssetDatabase.GetAssetPath(prefab), prefab, typeof(GameObject), true);
}
GUILayout.Label("Scenes: " + ScenePaths.Count, EditorStyles.boldLabel);
GUILayout.Label("We won't know if they contain any missing scripts until we open them.", EditorStyles.wordWrappedLabel);
for (int i = 0; i < ScenePaths.Count; i++)
{
var scene = ScenePaths[i];
EditorGUILayout.ObjectField(scene, AssetDatabase.LoadAssetAtPath<SceneAsset>(scene), typeof(SceneAsset), true);
}
GUILayout.EndScrollView();
GUILayout.Label(
"Assets are removed from these lists as they are selected." +
"\nTo begin again, simply click this button:",
EditorStyles.wordWrappedLabel);
if (GUILayout.Button("Re-Gather Assets"))
{
GatherScenesAndMissingAsssets();
}
}
/************************************************************************************************************************/
private void DoMissingAssetsGUI()
{
GUILayout.Label("Assets with missing scripts: " + MissingAssetPaths.Count, EditorStyles.boldLabel);
if (MissingAssetPaths.Count == 0)
return;
GUILayout.BeginHorizontal();
GUILayout.Label("These assets can't be automatically selected" +
" because Unity doesn't give us any way to reference them.", EditorStyles.wordWrappedLabel);
if (GUILayout.Button("Delete All", EditorStyles.miniButton, Editor.IGEditorUtils.DontExpandWidth))
{
const string Message = "Are you sure you want to delete all assets with missing scripts?" +
"\n\nYou cannot undo this action.";
if (EditorUtility.DisplayDialog("Delete all assets with missing scripts?", Message, "Delete", "Cancel"))
{
DeleteAllMissingAssets();
GUIUtility.ExitGUI();
}
}
GUILayout.EndHorizontal();
for (int i = 0; i < MissingAssetPaths.Count; i++)
{
GUILayout.BeginHorizontal();
var path = MissingAssetPaths[i];
GUILayout.Label(MissingAssetPaths[i], GUI.skin.textArea);
var area = GUILayoutUtility.GetLastRect();
var currentEvent = Event.current;
if (currentEvent.type == EventType.MouseUp &&
area.Contains(currentEvent.mousePosition))
{
path = Path.GetDirectoryName(path);
var asset = AssetDatabase.LoadAssetAtPath(path, typeof(Object));
SelectObject(asset);
}
if (GUILayout.Button("Delete", EditorStyles.miniButton, Editor.IGEditorUtils.DontExpandWidth))
{
var message = path + "\n\nYou cannot undo this action.";
if (EditorUtility.DisplayDialog("Delete asset?", message, "Delete", "Cancel"))
{
AssetDatabase.DeleteAsset(path);
MissingAssetPaths.RemoveAt(i);
GUIUtility.ExitGUI();
}
}
GUILayout.EndHorizontal();
}
}
/************************************************************************************************************************/
private void DeleteAllMissingAssets()
{
for (int i = 0; i < MissingAssetPaths.Count; i++)
{
AssetDatabase.DeleteAsset(MissingAssetPaths[i]);
}
MissingAssetPaths.Clear();
}
/************************************************************************************************************************/
private void DoMissingScriptButtons()
{
if (_SimilarityRatings == null)
return;
var scripts = AllScripts;
for (int i = scripts.Length - 1; i >= 0; i--)
{
var similarity = _SimilarityRatings[i];
if (similarity <= 0)
{
if (i == scripts.Length - 1)
{
GUILayout.Label(_HasSerializedProperties
? "No similar scripts found."
: "Unable to determine the missing script's name or find any serialized properties to compare other scripts against.",
EditorStyles.wordWrappedLabel);
}
continue;
}
var script = scripts[i];
var prefix = similarity == int.MaxValue ?
"[Name Match] " :
$"[{similarity}] ";
if (GUILayout.Button(prefix + script.name))
{
_ScriptProperty.objectReferenceValue = script;
_ScriptProperty.serializedObject.ApplyModifiedProperties();
SelectNextMissingScript();
return;
}
}
}
/************************************************************************************************************************/
private void SelectNextMissingScript()
{
_ScriptProperty = null;
// Next Prefab.
for (int i = MissingPrefabs.Count - 1; i >= 0; i--)
{
var prefab = MissingPrefabs[i];
MissingPrefabs.RemoveAt(i);
if (HasAnyMissingComponents(prefab))
{
SelectObject(prefab);
return;
}
}
NextScene:
if (_SceneObjectIndex >= SceneObjects.Count)
{
if (ScenePaths.Count > 0)
{
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
return;
var scene = ScenePaths[ScenePaths.Count - 1];
ScenePaths.RemoveAt(ScenePaths.Count - 1);
EditorSceneManager.OpenScene(scene);
_HasLoadedAnotherScene = true;
GatherSceneObjects();
_SceneObjectIndex = -1;
// Continue on to pick a scene object.
}
else
{
// No scenes left, so we're done.
ResetSceneSetup();
return;
}
}
NextSceneObject:
_SceneObjectIndex++;
if (_SceneObjectIndex < SceneObjects.Count)
{
var sceneObject = SceneObjects[_SceneObjectIndex];
if (HasAnyMissingComponents(sceneObject))
{
SelectObject(sceneObject);
return;
}
else
{
goto NextSceneObject;
}
}
else
{
SceneObjects.Clear();
goto NextScene;
}
}
private void GatherSceneObjects()
{
SceneObjects.Clear();
foreach (var gameObject in Resources.FindObjectsOfTypeAll<GameObject>())
{
if ((gameObject.hideFlags & HideFlags.HideAndDontSave) != 0)
continue;
if (EditorUtility.IsPersistent(gameObject.transform.root.gameObject))
continue;
SceneObjects.Add(gameObject);
}
}
/************************************************************************************************************************/
private void DestroyTargetAndSelectNext()
{
MissingScriptEditor.DestroyProperly(_Target, true);
ClearTarget();
SelectNextMissingScript();
}
/************************************************************************************************************************/
private static void SelectObject(Object target)
{
Selection.activeObject = null;
EditorApplication.delayCall += () =>
Selection.activeObject = target;
}
/************************************************************************************************************************/
public void SetScripts(Object target, SerializedProperty scriptProperty, bool hasSerializedProperties, int[] similarityRatings)
{
_Target = target;
_ScriptProperty = scriptProperty;
_HasSerializedProperties = hasSerializedProperties;
_SimilarityRatings = similarityRatings;
Array.Sort(similarityRatings, AllScripts);
GetWindow<MissingScriptWindow>();
}
/************************************************************************************************************************/
public void ClearTarget()
{
_Target = null;
_ScriptProperty = null;
_SimilarityRatings = null;
}
/************************************************************************************************************************/
private bool DoSelectNextButtonGUI()
{
if (MissingPrefabs.Count > 0 || ScenePaths.Count > 0 || _SceneObjectIndex < SceneObjects.Count)
{
if (GUILayout.Button("Select Next Missing Script"))
{
SelectNextMissingScript();
return true;
}
}
else
{
if (GUILayout.Button("Close"))
{
Close();
return true;
}
}
return false;
}
/************************************************************************************************************************/
private static readonly List<MonoBehaviour> Components = new List<MonoBehaviour>();
private static bool HasAnyMissingComponents(GameObject gameObject)
{
gameObject.GetComponents(Components);
for (int i = 0; i < Components.Count; i++)
{
if (Components[i] == null)
return true;
}
return false;
}
/************************************************************************************************************************/
private void ResetSceneSetup()
{
_HasLoadedAnotherScene = false;
// If the current scene setup has changed, ask if the user wants to return to what they started with.
var sceneSetup = EditorSceneManager.GetSceneManagerSetup();
if (sceneSetup.Length != _OriginalSceneSetup.Length)
goto ReturnToOriginalSetup;
for (int i = 0; i < sceneSetup.Length; i++)
{
if (sceneSetup[i].path != _OriginalSceneSetup[i].path)
goto ReturnToOriginalSetup;
}
return;
ReturnToOriginalSetup:
if (_ConfirmResetScene && !EditorUtility.DisplayDialog("Restore Original Scene Setup",
"Would you like to return to the scene you had open when you opened the Scripty Hunter?", "Restore", "Do Nothing"))
return;
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
return;
EditorSceneManager.RestoreSceneManagerSetup(_OriginalSceneSetup);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 45a7f1d7eb9b94540bd7643a4a40189f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,669 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Text;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor.PropertyDrawers
{
/// <summary>[Editor-Only] [Pro-Only] A custom drawer for <see cref="Object"/> fields.</summary>
#if !DISABLE_IG_OBJECT_DRAWER
[CustomPropertyDrawer(typeof(Object), true)]
#endif
public class ObjectDrawer : PropertyDrawer
{
/************************************************************************************************************************/
/// <summary>The number of nesting levels allowed.</summary>
public static readonly AutoPrefs.EditorInt
ObjectEditorNestLimit = new(
EditorStrings.PrefsKeyPrefix + nameof(ObjectEditorNestLimit), 3);
/// <summary>Should references to components on the same object be italicised?</summary>
public static readonly AutoPrefs.EditorBool
ItaliciseSelfReferences = new(
EditorStrings.PrefsKeyPrefix + nameof(ItaliciseSelfReferences), true);
/// <summary>Should the get button always be visible?</summary>
public static readonly AutoPrefs.EditorBool
AlwaysShowGet = new(
EditorStrings.PrefsKeyPrefix + nameof(AlwaysShowGet), false);
/************************************************************************************************************************/
private static bool _HasRegisteredOnModifierKeysChanged;
private static void RegisterOnModifierKeysChanged()
{
if (_HasRegisteredOnModifierKeysChanged)
return;
_HasRegisteredOnModifierKeysChanged = true;
EditorApplication.modifierKeysChanged += () =>
{
if (!AlwaysShowGet)
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
};
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> EditorGUI.GetPropertyHeight(property, label, property.isExpanded);
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
EditorGUI.PropertyField(area, property, label, property.isExpanded);
return;
}
GetReference(property, out var reference, out var isSelf);
DoGetButtonGUI(ref area, property, reference);
HandleDragAndDrop(area, property, reference);
// Property Field.
var originalStyle = EditorStyles.objectField.fontStyle;
var labelWidth = EditorGUIUtility.labelWidth;
try
{
var fieldArea = area;
if (isSelf)
{
EditorStyles.objectField.fontStyle = FontStyle.Italic;
}
else
{
isSelf = !NestingIsEnabled(property, reference);
if (!isSelf && !EditorGUIUtility.hierarchyMode)
{
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 1;
var x = fieldArea.x;
fieldArea = EditorGUI.IndentedRect(fieldArea);
EditorGUIUtility.labelWidth -= fieldArea.x - x;
EditorGUI.indentLevel = indentLevel;
}
}
EditorGUI.PropertyField(fieldArea, property, label, property.isExpanded);
}
finally
{
EditorStyles.objectField.fontStyle = originalStyle;
EditorGUIUtility.labelWidth = labelWidth;
}
// Nested Inspector.
if (!isSelf)
DoNestedInspectorGUI(area, property, reference);
}
/************************************************************************************************************************/
private void GetReference(SerializedProperty property, out Object reference, out bool isSelf)
{
isSelf = false;
if (!property.hasMultipleDifferentValues && property.propertyType == SerializedPropertyType.ObjectReference)
{
reference = property.objectReferenceValue;
if (!ItaliciseSelfReferences ||
reference == null)
return;
if (property.serializedObject.targetObject is Component targetComponent)
{
if (reference is Component component)
{
if (component.gameObject == targetComponent.gameObject)
{
isSelf = true;
}
}
else
{
if (reference is GameObject gameObject)
{
if (gameObject == targetComponent.gameObject)
{
isSelf = true;
}
}
}
}
else if (reference == property.serializedObject.targetObject)
{
isSelf = true;
}
}
else
{
reference = null;
}
}
/************************************************************************************************************************/
private static GUIContent _GetIcon;
private static GUIStyle _GetIconStyle;
private static void DoGetButtonGUI(ref Rect area, SerializedProperty property, Object reference)
{
if (reference != null && !AlwaysShowGet)
{
RegisterOnModifierKeysChanged();
if (!Event.current.alt)
return;
}
var accessor = property.GetAccessor();
var type = accessor.GetFieldElementType(property);
if (type == null || type == typeof(Object))
return;
var canSearchComponents =
typeof(Component).IsAssignableFrom(type) &&
property.serializedObject.targetObject is Component;
_GetIcon ??= EditorGUIUtility.IconContent("ViewToolZoom");
_GetIconStyle ??= new GUIStyle(InternalGUI.SmallButtonStyle)
{
padding = new RectOffset(0, 0, 0, 0),
};
if (canSearchComponents)
_GetIcon.tooltip = "Click to search for an appropriate component in this object's" +
" parents, or children, or the rest of the scene (in that order)." +
"\n\nCtrl + Click to search for an appropriate component in all prefabs in the project.";
else
_GetIcon.tooltip = "Click to search for an appropriate asset in the project.";
var width = _GetIconStyle.CalculateWidth(_GetIcon);
var buttonArea = IGEditorUtils.StealFromRight(
ref area, width, new RectOffset((int)EditorGUIUtility.standardVerticalSpacing, 0, 0, 0));
if (GUI.Button(buttonArea, _GetIcon, _GetIconStyle))
{
property.ForEachTarget((prop) =>
{
if (canSearchComponents &&
!Event.current.control)
{
var component = prop.serializedObject.targetObject as Component;
component = IGUtils.ProgressiveSearch(component.gameObject, type, prop.displayName);
if (component != null)
prop.objectReferenceValue = component;
}
else
{
var asset = IGEditorUtils.FindAssetOfType(type, prop.displayName);
if (asset != null)
prop.objectReferenceValue = asset;
}
});
}
}
/************************************************************************************************************************/
private static readonly int DragHint = nameof(DragHint).GetHashCode();
private static void HandleDragAndDrop(Rect area, SerializedProperty property, Object reference)
{
var id = GUIUtility.GetControlID(DragHint, FocusType.Passive, area);
var currentEvent = Event.current;
switch (currentEvent.type)
{
// Drag out of object field.
case EventType.MouseDrag:
if (GUIUtility.keyboardControl == id + 1 &&
currentEvent.button == 0 &&
area.Contains(currentEvent.mousePosition) &&
reference != null)
{
DragAndDrop.PrepareStartDrag();
DragAndDrop.objectReferences = Serialization.GetValues<Object>(property);
DragAndDrop.StartDrag("Objects");
currentEvent.Use();
}
break;
// Drop into Component field, show menu if there are multiple valid components.
case EventType.DragPerform:
if (area.Contains(currentEvent.mousePosition))
{
var accessor = property.GetAccessor();
var fieldType = accessor.GetFieldElementType(property);
if (fieldType == typeof(Object))
HandleDragAndDropIntoObjectField(currentEvent, property, reference);
else if (typeof(Component).IsAssignableFrom(fieldType))
HandleDragAndDropIntoComponentField(currentEvent, fieldType, property, reference);
}
break;
}
}
/************************************************************************************************************************/
private static void HandleDragAndDropIntoObjectField(
Event currentEvent,
SerializedProperty property,
Object reference)
{
var droppingReferences = DragAndDrop.objectReferences;
if (droppingReferences.Length == 0)
return;
var objects = new List<Object>();
for (int i = 0; i < droppingReferences.Length; i++)
{
var dropping = droppingReferences[i];
objects.Add(dropping);
if (dropping is GameObject gameObject)
objects.AddRange(gameObject.GetComponents<Component>());
}
if (objects.Count == 0)
return;
DragAndDrop.AcceptDrag();
currentEvent.Use();
if (objects.Count == 1)
{
property.objectReferenceValue = objects[0];
return;
}
property = property.Copy();
var menu = new GenericMenu();
foreach (var item in objects)
{
menu.AddItem(new GUIContent(item.ToString()), reference == item, () =>
{
Undo.RecordObjects(property.serializedObject.targetObjects, "Inspector");
property.objectReferenceValue = item;
property.serializedObject.ApplyModifiedProperties();
});
}
menu.ShowAsContext();
}
/************************************************************************************************************************/
private static void HandleDragAndDropIntoComponentField(
Event currentEvent,
Type fieldType,
SerializedProperty property,
Object reference)
{
var droppingReferences = DragAndDrop.objectReferences;
if (droppingReferences.Length == 0)
return;
var componentSet = new HashSet<Component>();
var components = new List<(string, Component)>();// (path, component)
for (int i = 0; i < droppingReferences.Length; i++)
{
var dropping = droppingReferences[i];
if (fieldType.IsAssignableFrom(dropping.GetType()))
{
var droppingComponent = (Component)dropping;
components.Add((dropping.name, droppingComponent));
componentSet.Add(droppingComponent);
}
else if (dropping is GameObject gameObject)
{
foreach (var component in gameObject.GetComponents(fieldType))
{
components.Add((component.name, component));
componentSet.Add(component);
}
}
}
List<(string, Component)>// (path, component)
childComponents = new(),
parentComponents = new();
// If the dropped objects have no valid components or the user is holding Alt, check in parents and children.
if (components.Count != 1 || currentEvent.alt)
{
var text = new StringBuilder();
for (int i = 0; i < droppingReferences.Length; i++)
{
var dropping = IGUtils.GetTransform(droppingReferences[i]);
if (dropping == null)
continue;
var newComponents = dropping.GetComponentsInChildren(fieldType);
foreach (var component in newComponents)
{
if (componentSet.Contains(component))
continue;
text.Length = 0;
AppendRelativeHierarchyPath(text, component.transform, dropping, true, "/");
text.Append("/");
text.Append(component.name);
text.Insert(0, "Children");
childComponents.Add((text.ToString(), component));
}
newComponents = dropping.GetComponentsInParent(fieldType);
foreach (var component in newComponents)
{
if (componentSet.Contains(component))
continue;
text.Length = 0;
AppendRelativeHierarchyPath(text, dropping, component.transform, false, "/");
text.Insert(0, "Parents");
childComponents.Add((text.ToString(), component));
}
}
}
var foundAnyInFamily =
childComponents.Count != 0 ||
parentComponents.Count != 0;
if (components.Count == 0 &&
!foundAnyInFamily)
return;
DragAndDrop.AcceptDrag();
currentEvent.Use();
if (components.Count == 1 &&
!foundAnyInFamily)
{
property.objectReferenceValue = components[0].Item2;
return;
}
property = property.Copy();
var menu = new GenericMenu();
AddObjectAssignmentMenuItems(menu, childComponents, reference, property);
AddObjectAssignmentMenuItems(menu, parentComponents, reference, property);
AddObjectAssignmentMenuItems(menu, components, reference, property);
menu.ShowAsContext();
}
/************************************************************************************************************************/
private static void AppendRelativeHierarchyPath(
StringBuilder text,
Transform child,
Transform parent,
bool reverse,
string separator)
{
while (child != parent && child != null)
{
child = child.parent;
if (child == null)
return;
if (reverse)
{
if (child == parent)
return;
text.Insert(0, child.name);
text.Insert(0, separator);
}
else
{
text.Append(separator);
text.Append(child.name);
}
}
}
/************************************************************************************************************************/
private static void AddObjectAssignmentMenuItems(
GenericMenu menu,
List<(string, Component)> components,
Object currentReference,
SerializedProperty property)
{
GameObject previousGameObject = null;
var index = 0;
for (int i = 0; i < components.Count; i++)
{
var component = components[i];
if (previousGameObject != component.Item2.gameObject)
{
previousGameObject = component.Item2.gameObject;
index = 0;
}
else
{
index++;
}
var label = $"{component.Item1} [{index}] -> {component.Item2.GetType().GetNameCS()}";
menu.AddItem(new GUIContent(label), currentReference == component.Item2, () =>
{
Undo.RecordObjects(property.serializedObject.targetObjects, "Inspector");
property.objectReferenceValue = component.Item2;
property.serializedObject.ApplyModifiedProperties();
});
}
}
/************************************************************************************************************************/
#region Nesting
/************************************************************************************************************************/
private static readonly HashSet<Object>
CurrentReferences = new();
private static GUIStyle _NestAreaStyle;
private bool _IsInitialized;
private bool _AllowNestedEditors;
private UnityEditor.Editor _TargetEditor;
/************************************************************************************************************************/
private bool NestingIsEnabled(SerializedProperty property, Object reference)
{
return
reference != null &&
AllowNesting(reference.GetType()) &&
CurrentReferences.Count < ObjectEditorNestLimit &&
!property.hasMultipleDifferentValues &&
AllowNesting(property) &&
!CurrentReferences.Contains(reference);
}
/************************************************************************************************************************/
private void DoNestedInspectorGUI(Rect area, SerializedProperty property, Object reference)
{
// Disable the GUI if HideFlags.NotEditable is set.
var enabled = GUI.enabled;
try
{
GUI.enabled = (reference.hideFlags & HideFlags.NotEditable) != HideFlags.NotEditable;
CurrentReferences.Add(reference);
property.isExpanded = EditorGUI.Foldout(area, property.isExpanded, GUIContent.none, true);
if (property.isExpanded)
{
const float NegativePadding = 4;
EditorGUIUtility.labelWidth -= NegativePadding;
if (_NestAreaStyle == null)
{
_NestAreaStyle = new GUIStyle(GUI.skin.box);
var rect = _NestAreaStyle.margin;
rect.bottom = rect.top = 0;
_NestAreaStyle.margin = rect;
}
EditorGUI.indentLevel++;
GUILayout.BeginVertical(_NestAreaStyle);
try
{
// If the target has changed, destroy the old editor and make a new one.
if (_TargetEditor == null || _TargetEditor.target != reference)
{
Editors.Destroy(_TargetEditor);
_TargetEditor = Editors.Create(reference);
}
// Draw the target editor.
_TargetEditor.OnInspectorGUI();
}
catch (ExitGUIException)
{
}
catch (Exception exception)
{
Debug.LogException(exception);
}
GUILayout.EndVertical();
EditorGUI.indentLevel--;
EditorGUIUtility.labelWidth += NegativePadding;
}
}
finally
{
CurrentReferences.Remove(reference);
GUI.enabled = enabled;
}
}
/************************************************************************************************************************/
private bool AllowNesting(SerializedProperty property)
{
if (!_IsInitialized)
{
_IsInitialized = true;
_AllowNestedEditors =
Event.current.type != EventType.Repaint &&
AllowNesting(property.GetAccessor(), property);
}
return _AllowNestedEditors;
}
/************************************************************************************************************************/
private static readonly Dictionary<Serialization.PropertyAccessor, bool>
AllowNestingAccessorCache = new();
private static bool AllowNesting(Serialization.PropertyAccessor accessor, SerializedProperty property)
{
if (accessor == null)
return true;
if (!AllowNestingAccessorCache.TryGetValue(accessor, out var allow))
{
var type = accessor.GetFieldElementType(property);
if (!AllowNesting(type))
{
allow = false;
}
else
{
allow = AllowNesting(accessor.Parent, property);
}
AllowNestingAccessorCache.Add(accessor, allow);
}
return allow;
}
/************************************************************************************************************************/
private static readonly Dictionary<Type, bool>
AllowNestingTypeCache = new()
{
#if UNITY_AUDIO
{ typeof(AudioClip), false },
#endif
{ typeof(DefaultAsset), false },
{ typeof(GameObject), false },
{ typeof(Material), false },
#if UNITY_ANIMATION
{ typeof(RuntimeAnimatorController), false },
#endif
};
/************************************************************************************************************************/
private static bool AllowNesting(Type type)
{
if (type == null)
return true;
if (!AllowNestingTypeCache.TryGetValue(type, out var allow))
{
var field = type.GetField("NestedObjectDrawers", IGEditorUtils.StaticBindings);
if (field != null && field.IsLiteral && field.FieldType == typeof(bool))
{
allow = (bool)field.GetValue(null);
}
else
{
allow = AllowNesting(type.BaseType);
}
AllowNestingTypeCache.Add(type, allow);
}
return allow;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a5685d794cb016488046f98a4dd1344
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5fca52d770a24e4459f859648af54484
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,152 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only] Settings which control the custom Transform Gizmo.</summary>
public static class CustomTransformGizmo
{
/************************************************************************************************************************/
private static readonly AutoPrefs.EditorString
StandardGizmoComponents;
private static readonly HashSet<string>
StandardGizmoComponentsSet = new();
static CustomTransformGizmo()
{
StandardGizmoComponents = new(
EditorStrings.PrefsKeyPrefix + nameof(StandardGizmoComponents),
"UnityEngine.ProBuilder.ProBuilderMesh," +
"UnityEngine.Splines.SplineContainer",
value => ParseTypes());
ParseTypes();
}
/************************************************************************************************************************/
private static readonly List<Component>
Components = new();
/// <summary>Should the gizmo currently be shown?</summary>
public static bool ShouldShow()
{
if (StandardGizmoComponentsSet.Count == 0)
return true;
foreach (var selected in Selection.gameObjects)
{
Components.Clear();
selected.GetComponents(Components);
foreach (var component in Components)
if (component != null &&
StandardGizmoComponentsSet.Contains(component.GetType().FullName))
return false;
}
return true;
}
/************************************************************************************************************************/
private static readonly char[] SplitChars = { ' ', ',', ';' };
private static void ParseTypes()
{
StandardGizmoComponentsSet.Clear();
var types = StandardGizmoComponents.Value;
var start = 0;
while (start < types.Length)
{
var end = types.IndexOfAny(SplitChars, start);
if (end < 0)
break;
if (end > start + 1)
StandardGizmoComponentsSet.Add(types[start..end]);
start = end + 1;
}
StandardGizmoComponentsSet.Add(types[start..]);
}
/************************************************************************************************************************/
private static readonly GUIContent
ComponentTypesToDisableGizmoLabel = new("Standard Gizmo Components",
"A comma separated list containing the full name of any component types" +
" that should disable the custom gizmo."),
LogLabel = new("Log",
"Log the full name of all Component types on the currently selected objects.");
/// <summary>Draws the GUI for this system's preferences.</summary>
public static void DoPreferencesGUI()
{
GUILayout.BeginHorizontal();
StandardGizmoComponents.DoGUI(
ComponentTypesToDisableGizmoLabel,
GUI.skin.textField,
(area, label, value, style) =>
{
return EditorGUI.DelayedTextField(area, label, value, style);
});
if (GUILayout.Button(LogLabel, EditorStyles.miniButton, IGEditorUtils.DontExpandWidth))
LogSelectedComponents();
GUILayout.EndHorizontal();
}
/************************************************************************************************************************/
private static void LogSelectedComponents()
{
var types = new HashSet<Type>();
foreach (var selected in Selection.gameObjects)
{
Components.Clear();
selected.GetComponents(Components);
foreach (var component in Components)
if (component != null)
types.Add(component.GetType());
}
if (types.Count == 0)
{
Debug.Log("No GameObjects are currently selected.");
}
else
{
var text = new StringBuilder();
text.Append("The selected GameObjects have Components of the following types: ");
foreach (var type in types)
text.Append(type.ToString()).Append(", ");
text.Length -= 2;
Debug.Log(text);
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 60a000d388aa48b40ac228365048bf31
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,641 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
internal sealed class PositionDrawer : TransformPropertyDrawer
{
/************************************************************************************************************************/
public PositionDrawer(TransformEditor parentEditor)
: base(parentEditor,
"Position",
"The local position of this Game Object relative to the parent.",
"The world position of this Game Object.")
{ }
/************************************************************************************************************************/
public override void OnEnable(SerializedObject transform)
{
base.OnEnable(transform);
_MainSerializedProperty = transform.FindProperty("m_LocalPosition");
_XSerializedProperty = _MainSerializedProperty.FindPropertyRelative("x");
_YSerializedProperty = _MainSerializedProperty.FindPropertyRelative("y");
_ZSerializedProperty = _MainSerializedProperty.FindPropertyRelative("z");
}
/************************************************************************************************************************/
public override Vector3 GetLocalValue(Transform target) => target.localPosition;
public override Vector3 GetWorldValue(Transform target) => target.position;
public override void SetLocalValue(Transform target, Vector3 localPosition)
{
if (ParentEditor.CurrentFreezeChildTransforms)
CacheChildPositions(target);
target.localPosition = localPosition;
if (ParentEditor.CurrentFreezeChildTransforms)
RevertChildPositions(target);
}
public override void SetWorldValue(Transform target, Vector3 position)
{
if (ParentEditor.CurrentFreezeChildTransforms)
CacheChildPositions(target);
target.position = position;
if (ParentEditor.CurrentFreezeChildTransforms)
RevertChildPositions(target);
}
public override string UndoName => "Move";
public override Vector3 DefaultValue => default;
public override Vector3 SnapValues => IGEditorUtils.MoveSnapVector;
public override Vector3 SnapValue(Vector3 value) => IGEditorUtils.SnapPosition(value);
/************************************************************************************************************************/
protected override Vector3 GetCurrentDragValue(Transform target)
=> GetWorldValue(target);
protected override void HandleDragValue(
Transform target, Vector3 startingValue, Vector3 dragDelta, Vector3 dragOffset)
{
IGEditorUtils.GetCurrentCameraTransform(out var cameraPosition, out var cameraRotation);
dragOffset *= Vector3.Distance(cameraPosition, startingValue) * 0.25f / Screen.dpi;
var right = cameraRotation * Vector3.right;
var up = cameraRotation * Vector3.up;
var forward = cameraRotation * Vector3.forward;
startingValue +=
dragOffset.x * right +
-dragOffset.y * up +
-dragOffset.z * forward;
SetWorldValue(target, startingValue);
}
/************************************************************************************************************************/
private static Vector3[] _CachedChildPoistions;
public static void CacheChildPositions(Transform parent)
{
Array.Resize(ref _CachedChildPoistions, parent.childCount);
for (int i = 0; i < _CachedChildPoistions.Length; i++)
{
_CachedChildPoistions[i] = parent.GetChild(i).position;
}
}
public static void RevertChildPositions(Transform parent)
{
for (int i = 0; i < _CachedChildPoistions.Length; i++)
{
parent.GetChild(i).position = _CachedChildPoistions[i];
}
}
/************************************************************************************************************************/
private static readonly GUIContent PasteContentValue = new GUIContent("P");
public override GUIContent PasteContent => PasteContentValue;
private static readonly GUIContent SnapContentValue = new GUIContent("S");
public override GUIContent SnapContent => SnapContentValue;
protected override string GetSnapTooltip() => SnapValues.ToString();
/************************************************************************************************************************/
private static readonly NullableVector4 ClipboardValue = new NullableVector4(default(Vector3));
public override NullableVector4 PrivateClipboard => ClipboardValue;
/************************************************************************************************************************/
public override void OnPropertyContextMenu(GenericMenu menu, SerializedProperty property)
{
AddPropertyNameItem(menu, property);
var axis = CurrentVectorAxis;
if (axis < 0)// Vector Label.
{
AddVectorClipboardFunctions(menu);
menu.AddSeparator("");
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Negate (*= -1)",
(targetProperty) => targetProperty.vector3Value *= -1);
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Snap to Grid {IGEditorUtils.MoveSnapVector}",
(targetProperty) => targetProperty.vector3Value = IGEditorUtils.SnapPosition(targetProperty.vector3Value));
AddSnapToGroundFunctions(menu);
SerializedPropertyContextMenu.Vector3MenuHandler.AddLogFunction(menu, property);
}
else// X, Y, Z.
{
AddFloatClipboardFunctions(menu, axis);
menu.AddSeparator("");
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Negate (*= -1)",
(targetProperty) => targetProperty.floatValue *= -1);
AddSnapFloatToGridItem(menu, property, axis);
if (axis == 1)// Y.
AddSnapToGroundFunctions(menu);
menu.AddSeparator("");
SerializedPropertyContextMenu.AddLogValueFunction(menu, property, (targetProperty) => targetProperty.floatValue);
}
}
/************************************************************************************************************************/
public static void AddSnapFloatToGridItem(GenericMenu menu, SerializedProperty property, int axis)
{
var snap = IGEditorUtils.MoveSnapVector[axis];
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Snap to Grid ({snap})",
(targetProperty) => targetProperty.floatValue = IGEditorUtils.Snap(property.floatValue, snap));
}
/************************************************************************************************************************/
protected override string GetCurrentModePropertyPrefix()
=> ParentEditor.CurrentIsLocalMode ? "localPosition" : "position";
/************************************************************************************************************************/
#region Snap to Ground
/************************************************************************************************************************/
private delegate void GroundPointSelector(float target, ref float best, float current);
public static readonly AutoPrefs.EditorFloat
SnapToGroundDistance = new AutoPrefs.EditorFloat(EditorStrings.PrefsKeyPrefix + "SnapToGroundDistance", 10);
public static readonly AutoPrefs.EditorInt
SnapToGroundLayers = new AutoPrefs.EditorInt(EditorStrings.PrefsKeyPrefix + "SnapToGroundLayers",
#if UNITY_PHYSICS_3D
Physics.DefaultRaycastLayers &
#endif
~AutoHideUI.UILayerMask.Value);
/************************************************************************************************************************/
private void AddSnapToGroundFunctions(GenericMenu menu)
{
AddSnapToGroundFunction(menu, "Snap to Ground (Closest)", GetClosest);
AddSnapToGroundFunction(menu, "Snap to Ground (Highest)", GetHighest);
}
private void AddSnapToGroundFunction(GenericMenu menu, string label, GroundPointSelector selectGroundPoint)
{
menu.AddItem(new GUIContent(label), false, () =>
{
var targets = _MainSerializedProperty.serializedObject.targetObjects;
Undo.RecordObjects(targets, "Snap to Ground");
// Ignore all selected objects and their children.
var ignore = new HashSet<object>();
for (int i = 0; i < targets.Length; i++)
{
var target = targets[i] as Transform;
#if UNITY_PHYSICS_3D
ignore.UnionWith(target.GetComponentsInChildren<Collider>());
#endif
#if UNITY_PHYSICS_2D
ignore.UnionWith(target.GetComponentsInChildren<Collider2D>());
#endif
}
for (int iTarget = 0; iTarget < targets.Length; iTarget++)
{
var target = targets[iTarget] as Transform;
// Raycast down from above the target and find the closest hit to them which is not one of their children.
var position = GetBasePosition(target);
var basePositionOffset = target.position.y - position.y;
var origin = position;
origin.y += SnapToGroundDistance;
var bestValue = float.PositiveInfinity;
var layers =
#if UNITY_PHYSICS_3D
Physics.DefaultRaycastLayers &
#endif
~AutoHideUI.UILayerMask.Value;
#if UNITY_PHYSICS_2D
var hits2D = Physics2D.RaycastAll(origin, Vector3.down, SnapToGroundDistance.Value * 2, layers);
for (int iHit = 0; iHit < hits2D.Length; iHit++)
{
var hit = hits2D[iHit];
if (ignore.Contains(hit.collider))
continue;
if (bestValue == float.PositiveInfinity)
bestValue = hit.point.y;
else
selectGroundPoint(position.y, ref bestValue, hit.point.y);
}
#endif
#if UNITY_PHYSICS_3D
var hits3D = Physics.RaycastAll(origin, Vector3.down, SnapToGroundDistance.Value * 2, layers);
for (int iHit = 0; iHit < hits3D.Length; iHit++)
{
var hit = hits3D[iHit];
if (ignore.Contains(hit.collider))
continue;
if (bestValue == float.PositiveInfinity)
bestValue = hit.point.y;
else
selectGroundPoint(position.y, ref bestValue, hit.point.y);
}
#endif
if (bestValue == float.PositiveInfinity)
return;
position.y = bestValue + basePositionOffset;
target.position = position;
}
});
}
/************************************************************************************************************************/
private static Vector3 GetBasePosition(Transform target)
{
var position = target.position;
// If we have any Colliders or Renderers, find the lowest bounds and snap that point to the ground instead of the transform's position.
#if UNITY_PHYSICS_2D
var colliders2D = target.GetComponentsInChildren<Collider2D>();
for (int i = 0; i < colliders2D.Length; i++)
{
var boundsMin = colliders2D[i].bounds.min.y;
if (position.y > boundsMin)
position.y = boundsMin;
}
#endif
#if UNITY_PHYSICS_3D
var colliders = target.GetComponentsInChildren<Collider>();
for (int i = 0; i < colliders.Length; i++)
{
var boundsMin = colliders[i].bounds.min.y;
if (position.y > boundsMin)
position.y = boundsMin;
}
#endif
var renderers = target.GetComponentsInChildren<Renderer>();
for (int i = 0; i < renderers.Length; i++)
{
var boundsMin = renderers[i].bounds.min.y;
if (position.y > boundsMin)
position.y = boundsMin;
}
return position;
}
/************************************************************************************************************************/
private static void GetClosest(float target, ref float best, float current)
{
var bestDistance = Math.Abs(target - best);
var distance = Math.Abs(target - current);
if (distance < bestDistance)
best = current;
}
private static void GetHighest(float target, ref float best, float current)
{
if (best < current)
best = current;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Custom Handles
/************************************************************************************************************************/
private static readonly Dictionary<Transform, Vector3>
MovingFromPositions = new Dictionary<Transform, Vector3>();
private static readonly Dictionary<Transform, Quaternion>
MovingFromRotations = new Dictionary<Transform, Quaternion>();
public static readonly AutoPrefs.EditorBool
ShowMovementGuides = new AutoPrefs.EditorBool(EditorStrings.PrefsKeyPrefix + nameof(ShowMovementGuides), true),
ShowMovementDistance = new AutoPrefs.EditorBool(EditorStrings.PrefsKeyPrefix + nameof(ShowMovementDistance), true),
ShowMovementDistancePerAxis = new AutoPrefs.EditorBool(EditorStrings.PrefsKeyPrefix + nameof(ShowMovementDistancePerAxis), true),
ShowPositionLabels = new AutoPrefs.EditorBool(EditorStrings.PrefsKeyPrefix + nameof(ShowPositionLabels), false);
private static bool
_HasInitializedHandles;
private static FieldInfo
_VertexDragging;
private static MethodInfo
_FindNearestVertex;
private static object[]
_FindNearestVertexParameters;
private static Vector3
_VertexSnapHandleOffset;
// UnityEditor.Handles.currentlyDragging.
private static bool CurrentlyDragging => GUIUtility.hotControl != 0;
/************************************************************************************************************************/
public override void DoToolGUI(Transform target, Vector3 handlePosition)
{
// Vertex Snapping.
{
if (!_HasInitializedHandles)
{
_HasInitializedHandles = true;
_VertexDragging = typeof(Tools).GetField("vertexDragging", IGEditorUtils.StaticBindings);
_FindNearestVertex = typeof(HandleUtility).GetMethod("FindNearestVertex", IGEditorUtils.StaticBindings, null,
new Type[] { typeof(Vector2), typeof(Transform[]) }, null);
}
if (_VertexDragging != null && _FindNearestVertex != null)
{
try
{
if ((bool)_VertexDragging.GetValue(null))
{
if (!CurrentlyDragging)
{
if (_FindNearestVertexParameters == null)
_FindNearestVertexParameters = new object[3];
_FindNearestVertexParameters[0] = Event.current.mousePosition;
_FindNearestVertexParameters[1] = Selection.GetTransforms(SelectionMode.Editable | SelectionMode.ExcludePrefab | SelectionMode.Deep);
_FindNearestVertex.Invoke(null, _FindNearestVertexParameters);
var position = (Vector3)_FindNearestVertexParameters[2];
_VertexSnapHandleOffset = position - GetWorldValue(target);
handlePosition = position;
}
else
{
handlePosition = GetWorldValue(target) + _VertexSnapHandleOffset;
}
}
}
catch { }
}
}
EditorGUI.BeginChangeCheck();
var movement = DoHandleGUI(target, handlePosition);
if (EditorGUI.EndChangeCheck() && GUI.enabled && movement != default)
{
RecordTransformForUndo(target, UndoName);
if (TransformEditor.DrawAllGizmos)
{
SetWorldValue(target, GetWorldValue(target) + movement);
}
else
{
for (int i = 0; i < ParentEditor.Targets.Length; i++)
{
target = ParentEditor.Targets[i];
if (!SelectionContainsParent(target))
SetWorldValue(target, GetWorldValue(target) + movement);
}
}
}
}
/************************************************************************************************************************/
private bool SelectionContainsParent(Transform target)
{
while (true)
{
target = target.parent;
if (target == null)
return false;
else if (Array.IndexOf(ParentEditor.Targets, target) >= 0)
return true;
}
}
/************************************************************************************************************************/
private static GUIStyle _DistanceLabelStyle, _AxisLabelStyle;
private static string _DistanceLabelFormat;
private static string[] _DistanceLabelArgs;
private Vector3 DoHandleGUI(Transform target, Vector3 handlePosition)
{
var rotation = Tools.pivotRotation == PivotRotation.Local ? target.rotation : Quaternion.identity;
if (ShowMovementGuides)
{
switch (Event.current.type)
{
case EventType.MouseDown:
MovingFromPositions[target] = handlePosition;
MovingFromRotations[target] = rotation;
break;
case EventType.MouseUp:
MovingFromPositions.Clear();
MovingFromRotations.Clear();
break;
case EventType.Repaint:
var cameraTransform = SceneView.currentDrawingSceneView.camera.transform;
var forward = cameraTransform.forward;
if (Vector3.Dot(forward, handlePosition) < Vector3.Dot(forward, cameraTransform.position))
break;
if (MovingFromPositions.TryGetValue(target, out var movingFrom))
{
var movingFromRotation = MovingFromRotations[target];
var xAxis = movingFromRotation * Vector3.right;
var yAxis = movingFromRotation * Vector3.up;
var zAxis = movingFromRotation * Vector3.forward;
Handles.DrawLine(movingFrom, handlePosition);
var size = HandleUtility.GetHandleSize(movingFrom) * 0.5f;
Handles.color = Handles.xAxisColor;
Handles.DrawLine(movingFrom, movingFrom + xAxis * size);
Handles.color = Handles.yAxisColor;
Handles.DrawLine(movingFrom, movingFrom + yAxis * size);
Handles.color = Handles.zAxisColor;
Handles.DrawLine(movingFrom, movingFrom + zAxis * size);
Handles.color = Color.white;
if (ShowMovementDistance)
{
if (_DistanceLabelStyle == null)
{
_DistanceLabelStyle = new GUIStyle(GUI.skin.label);
_DistanceLabelStyle.normal.textColor = Color.white;
var monoSpaceFont = EditorGUIUtility.Load("Fonts/RobotoMono/RobotoMono-Regular.ttf") as Font;
if (monoSpaceFont != null)
_DistanceLabelStyle.font = monoSpaceFont;
}
_DistanceLabelStyle.normal.background = InternalGUI.SceneLabelBackground;
var offset = movingFrom - handlePosition;
var distance = offset.sqrMagnitude;
if (distance > 0.00001f)
{
const string DistanceFormat = "0.00";
distance = Mathf.Sqrt(distance);
var label = distance.ToString(DistanceFormat);
if (ShowMovementDistancePerAxis)
{
if (_DistanceLabelFormat == null)
{
_DistanceLabelFormat =
$"(<color=#{IGEditorUtils.ColorToHex(FieldColorX)}>{{0}}</color>, " +
$"<color=#{IGEditorUtils.ColorToHex(FieldColorY)}>{{1}}</color>, " +
$"<color=#{IGEditorUtils.ColorToHex(FieldColorZ)}>{{2}}</color>)" +
$" => {{3}}";
_DistanceLabelArgs = new string[4];
}
_DistanceLabelArgs[0] = Math.Abs(offset.x).ToString(DistanceFormat);
_DistanceLabelArgs[1] = Math.Abs(offset.y).ToString(DistanceFormat);
_DistanceLabelArgs[2] = Math.Abs(offset.z).ToString(DistanceFormat);
_DistanceLabelArgs[3] = label;
label = string.Format(_DistanceLabelFormat, _DistanceLabelArgs);
_DistanceLabelStyle.richText = true;
}
else _DistanceLabelStyle.richText = false;
Handles.Label(movingFrom, label, _DistanceLabelStyle);
}
}
}
if (ShowPositionLabels)
{
if (_AxisLabelStyle == null)
{
_AxisLabelStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.MiddleLeft
};
}
_AxisLabelStyle.normal.background = InternalGUI.SceneLabelBackground;
var xAxis = rotation * Vector3.right;
var yAxis = rotation * Vector3.up;
var zAxis = rotation * Vector3.forward;
var size = HandleUtility.GetHandleSize(handlePosition) * 1.35f;
_AxisLabelStyle.normal.textColor = FieldColorX;
Handles.Label(handlePosition + xAxis * size, handlePosition.x.ToString(), _AxisLabelStyle);
_AxisLabelStyle.normal.textColor = FieldColorY;
Handles.Label(handlePosition + yAxis * size, handlePosition.y.ToString(), _AxisLabelStyle);
_AxisLabelStyle.normal.textColor = FieldColorZ;
Handles.Label(handlePosition + zAxis * size, handlePosition.z.ToString(), _AxisLabelStyle);
}
break;
default:
break;
}
}
return Handles.PositionHandle(handlePosition, rotation) - handlePosition;
}
/************************************************************************************************************************/
private static bool _IsInspectorDrag;
protected override void DoVectorField(Rect area, float labelRight)
{
if (ShowMovementGuides)
{
if (_IsInspectorDrag && GUIUtility.hotControl == 0)
{
_IsInspectorDrag = false;
MovingFromPositions.Clear();
MovingFromRotations.Clear();
SceneView.RepaintAll();
}
if (Event.current.type == EventType.MouseDown)
{
for (int i = 0; i < Targets.Length; i++)
{
var target = Targets[i];
if (target == null)
continue;
MovingFromPositions[target] = TransformEditor.GetHandlePosition(target);
MovingFromRotations[target] = target.rotation;
}
_IsInspectorDrag = true;
}
}
base.DoVectorField(area, labelRight);
}
/************************************************************************************************************************/
public override void OnDisable()
{
MovingFromPositions.Clear();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b1d180fa9369124aa95bdba3dd0c362
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,399 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
internal sealed class RotationDrawer : TransformPropertyDrawer
{
/************************************************************************************************************************/
private Vector3 _EulerAngles, _OldEulerAngles;
/************************************************************************************************************************/
public RotationDrawer(TransformEditor parentEditor)
: base(parentEditor,
"Rotation",
"The local rotation of this Game Object relative to the parent.",
"The world rotation of this Game Object.")
{ }
/************************************************************************************************************************/
public override void OnEnable(SerializedObject transform)
{
base.OnEnable(transform);
_MainSerializedProperty = transform.FindProperty("m_LocalRotation");
_XSerializedProperty = _YSerializedProperty = _ZSerializedProperty = _MainSerializedProperty;
CacheEulerAngles();
}
/************************************************************************************************************************/
public override Vector3 GetLocalValue(Transform target) => target.localEulerAngles;
public override Vector3 GetWorldValue(Transform target) => target.eulerAngles;
public override void SetLocalValue(Transform target, Vector3 localEulerAngles)
{
if (ParentEditor.CurrentFreezeChildTransforms)
{
PositionDrawer.CacheChildPositions(target);
CacheChildRotations(target);
}
target.localEulerAngles = localEulerAngles;
if (ParentEditor.CurrentFreezeChildTransforms)
{
PositionDrawer.RevertChildPositions(target);
RevertChildRotations(target);
}
}
public override void SetWorldValue(Transform target, Vector3 eulerAngles)
{
if (ParentEditor.CurrentFreezeChildTransforms)
{
PositionDrawer.CacheChildPositions(target);
CacheChildRotations(target);
}
target.eulerAngles = eulerAngles;
if (ParentEditor.CurrentFreezeChildTransforms)
{
PositionDrawer.RevertChildPositions(target);
RevertChildRotations(target);
}
}
public override string UndoName => "Rotate";
public override Vector3 DefaultValue => default;
public override Vector3 SnapValues => IGEditorUtils.RotationSnapVector;
public override Vector3 SnapValue(Vector3 value) => IGEditorUtils.SnapRotation(value);
/************************************************************************************************************************/
protected override Vector3 GetCurrentDragValue(Transform target)
=> default;
protected override void HandleDragValue(
Transform target, Vector3 startingValue, Vector3 dragDelta, Vector3 dragOffset)
{
IGEditorUtils.GetCurrentCameraTransform(out var cameraPosition, out var cameraRotation);
dragDelta *= 50 / Screen.dpi;
var right = cameraRotation * Vector3.right;
var up = cameraRotation * Vector3.up;
var forward = cameraRotation * Vector3.forward;
var result =
Quaternion.AngleAxis(-dragDelta.x, up) *
Quaternion.AngleAxis(-dragDelta.y, right) *
Quaternion.AngleAxis(dragDelta.z * -5, forward) *
target.rotation;
SetWorldValue(target, result.eulerAngles);
}
/************************************************************************************************************************/
private static Quaternion[] _CachedChildRotations;
public static void CacheChildRotations(Transform parent)
{
Array.Resize(ref _CachedChildRotations, parent.childCount);
for (int i = 0; i < _CachedChildRotations.Length; i++)
{
_CachedChildRotations[i] = parent.GetChild(i).rotation;
}
}
public static void RevertChildRotations(Transform parent)
{
for (int i = 0; i < _CachedChildRotations.Length; i++)
{
parent.GetChild(i).rotation = _CachedChildRotations[i];
}
}
/************************************************************************************************************************/
public void CacheEulerAngles()
{
_EulerAngles = _OldEulerAngles = GetCurrentValue(0);
}
/************************************************************************************************************************/
protected override void UpdateDisplayValues()
{
var firstValue = GetCurrentValue(0);
if (_OldEulerAngles != firstValue)
_EulerAngles = firstValue;
for (int i = 1; i < Targets.Length; i++)
{
var otherValue = GetCurrentValue(i);
if (otherValue != firstValue)
{
DisplayValues.SetAllNull();
return;
}
}
DisplayValues.CopyFrom(_EulerAngles);
}
/************************************************************************************************************************/
protected override void OnVectorFieldChanged(NullableVector4 values)
{
values.ZeroAllNulls();
_EulerAngles.x = values.x.Value % 360;
_EulerAngles.y = values.y.Value % 360;
_EulerAngles.z = values.z.Value % 360;
}
protected override void DoVectorField(Rect area, float labelRight)
{
base.DoVectorField(area, labelRight);
_OldEulerAngles = GetCurrentValue(0);
}
protected override void PasteValue(NullableVector4 clipboard)
{
base.PasteValue(clipboard);
CacheEulerAngles();
}
protected override void DoMiniButtonsGUI(ref Rect area)
{
EditorGUI.BeginChangeCheck();
base.DoMiniButtonsGUI(ref area);
if (EditorGUI.EndChangeCheck())
CacheEulerAngles();
}
/************************************************************************************************************************/
private static readonly GUIContent PasteContentValue = new GUIContent("P");
public override GUIContent PasteContent => PasteContentValue;
private static readonly GUIContent SnapContentValue = new GUIContent("S");
public override GUIContent SnapContent => SnapContentValue;
protected override string GetSnapTooltip() => $"({IGEditorUtils.RotationSnap})";
/************************************************************************************************************************/
private static readonly NullableVector4 ClipboardValue = new NullableVector4(default(Vector3));
public override NullableVector4 PrivateClipboard => ClipboardValue;
public override SerializedPropertyContextMenu.ContextMenuHandler ContextMenuHandler
=> SerializedPropertyContextMenu.QuaternionMenuHandler;
protected override object GetDisplayCopyValue()
=> Quaternion.Euler(DisplayValues.ToVector3());
/************************************************************************************************************************/
public bool IsArbitrarilyRotated { get; private set; }
public void CheckIfArbitrarilyRotated()
{
IsArbitrarilyRotated = false;
for (int i = 0; i < Targets.Length; i++)
{
var target = Targets[i];
if (target == null)
continue;
if (target.rotation != Quaternion.identity)
{
IsArbitrarilyRotated = true;
break;
}
}
}
/************************************************************************************************************************/
public override void OnPropertyContextMenu(GenericMenu menu, SerializedProperty property)
{
AddPropertyNameItem(menu, property);
var axis = CurrentVectorAxis;
if (axis < 0)// Vector Label.
{
AddVectorClipboardFunctions(menu);
SerializedPropertyContextMenu.QuaternionMenuHandler.AddCustomItems(menu, property);
AddSnapQuaternionToGridFunction(menu, property);
AddLookAtFunction(menu);
SerializedPropertyContextMenu.QuaternionMenuHandler.AddLogFunction(menu, property);
}
else// X, Y, Z.
{
AddFloatClipboardFunctions(menu, axis);
menu.AddSeparator("");
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Negate (*= -1)",
(targetProperty) =>
{
var euler = targetProperty.quaternionValue.eulerAngles;
euler[axis] *= -1;
targetProperty.quaternionValue = Quaternion.Euler(euler);
});
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Snap to Grid ({IGEditorUtils.RotationSnap})",
(targetProperty) => targetProperty.quaternionValue = IGEditorUtils.SnapRotation(property.quaternionValue, axis));
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Randomize}0-360",
(targetProperty) =>
{
var euler = targetProperty.quaternionValue.eulerAngles;
euler[axis] = UnityEngine.Random.Range(0f, 360f);
targetProperty.quaternionValue = Quaternion.Euler(euler);
});
menu.AddSeparator("");
SerializedPropertyContextMenu.AddLogValueFunction(menu, property,
(targetProperty) => targetProperty.quaternionValue.eulerAngles[axis]);
}
}
/************************************************************************************************************************/
public static void AddSnapQuaternionToGridFunction(GenericMenu menu, SerializedProperty property)
{
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Snap to Grid ({IGEditorUtils.RotationSnap})",
(targetProperty) => targetProperty.quaternionValue = IGEditorUtils.SnapRotation(property.quaternionValue));
}
/************************************************************************************************************************/
private void AddLookAtFunction(GenericMenu menu)
{
menu.AddItem(new GUIContent($"{SerializedPropertyContextMenu.Set}Look At Next Selected Object"), false, () =>
{
var currentSelection = Selection.objects;
Action onSelectionChanged = null;
onSelectionChanged = () =>
{
Selection.selectionChanged -= onSelectionChanged;
var lookAt = Selection.activeTransform;
if (lookAt != null)
{
var lookAtPoint = lookAt.position;
RecordTargetsForUndo("Look At");
for (int i = 0; i < Targets.Length; i++)
{
var target = Targets[i];
if (target == null)
continue;
SetWorldValue(target, Quaternion.LookRotation(lookAtPoint - target.position).eulerAngles);
}
EditorApplication.delayCall += () => Selection.objects = currentSelection;
}
};
Selection.selectionChanged += onSelectionChanged;
});
}
/************************************************************************************************************************/
protected override string GetCurrentModePropertyPrefix()
=> ParentEditor.CurrentIsLocalMode ? "localEulerAngles" : "eulerAngles";
/************************************************************************************************************************/
#region Custom Handles
/************************************************************************************************************************/
public override void DoToolGUI(Transform target, Vector3 handlePosition)
{
var rotation = Tools.pivotRotation == PivotRotation.Local ? target.rotation : Tools.handleRotation;
EditorGUI.BeginChangeCheck();
var newRotation = Handles.RotationHandle(rotation, handlePosition);
if (EditorGUI.EndChangeCheck() && GUI.enabled)
{
(Quaternion.Inverse(rotation) * newRotation).ToAngleAxis(out var angle, out var axis);
axis = rotation * axis;
var isIndividualSpace = Tools.pivotRotation == PivotRotation.Local && Tools.pivotMode == PivotMode.Pivot;
if (isIndividualSpace)
{
axis = target.rotation * Quaternion.Inverse(rotation) * axis;
}
RecordTransformForUndo(target, UndoName);
if (TransformEditor.DrawAllGizmos)
{
ApplyRotation(target, handlePosition, axis, angle);
}
else
{
for (int i = 0; i < Targets.Length; i++)
{
ApplyRotation(Targets[i], handlePosition, axis, angle);
}
}
Tools.handleRotation = newRotation;
}
}
/************************************************************************************************************************/
private void ApplyRotation(Transform target, Vector3 pivot, Vector3 axis, float angle)
{
if (ParentEditor.CurrentFreezeChildTransforms)
{
PositionDrawer.CacheChildPositions(target);
CacheChildRotations(target);
}
target.RotateAround(pivot, axis, angle);
if (ParentEditor.CurrentFreezeChildTransforms)
{
PositionDrawer.RevertChildPositions(target);
RevertChildRotations(target);
}
//if (transform.parent != null)
//{
// transform.SendTransformChangedScale();// Might need to call private.
//}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: de093c836a68a8046aa363e41c1bfdee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,382 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor.PropertyDrawers
{
internal sealed class ScaleDrawer : TransformPropertyDrawer
{
/************************************************************************************************************************/
public ScaleDrawer(TransformEditor parentEditor)
: base(parentEditor,
"Scale",
"The local scaling of this Game Object relative to the parent.",
"The world scale of this Game Object.")
{ }
/************************************************************************************************************************/
public override void OnEnable(SerializedObject transform)
{
base.OnEnable(transform);
_MainSerializedProperty = transform.FindProperty("m_LocalScale");
_XSerializedProperty = _MainSerializedProperty.FindPropertyRelative("x");
_YSerializedProperty = _MainSerializedProperty.FindPropertyRelative("y");
_ZSerializedProperty = _MainSerializedProperty.FindPropertyRelative("z");
}
/************************************************************************************************************************/
public override Vector3 GetWorldValue(Transform target)
{
var value = target.lossyScale;
// If the value is very close to (1,1,1), just show that value.
// The Vector3 equality operator is actually an approximation.
if (value == Vector3.one)
return Vector3.one;
return value;
}
public override Vector3 GetLocalValue(Transform target) => target.localScale;
public override void SetLocalValue(Transform target, Vector3 localScale)
{
if (ParentEditor.CurrentFreezeChildTransforms)
PositionDrawer.CacheChildPositions(target);
target.localScale = localScale;
if (ParentEditor.CurrentFreezeChildTransforms)
PositionDrawer.RevertChildPositions(target);
}
public override void SetWorldValue(Transform target, Vector3 worldScale)
{
if (ParentEditor.CurrentFreezeChildTransforms)
PositionDrawer.CacheChildPositions(target);
if (target.parent != null)
{
var parentWorldScale = target.parent.lossyScale;
worldScale.x /= parentWorldScale.x;
worldScale.y /= parentWorldScale.y;
worldScale.z /= parentWorldScale.z;
}
target.localScale = worldScale;
if (ParentEditor.CurrentFreezeChildTransforms)
PositionDrawer.RevertChildPositions(target);
}
public override string UndoName => "Scale";
public override Vector3 DefaultValue => Vector3.one;
public override Vector3 SnapValues => IGEditorUtils.ScaleSnapVector;
public override Vector3 SnapValue(Vector3 value) => IGEditorUtils.SnapScale(value);
/************************************************************************************************************************/
protected override Vector3 GetCurrentDragValue(Transform target)
{
var value = GetLocalValue(target);
return value != default
? value
: Vector3.one.normalized;
}
protected override void HandleDragValue(
Transform target, Vector3 startingValue, Vector3 dragDelta, Vector3 dragOffset)
{
dragOffset.z = 0;
if (dragOffset == default)
return;
dragOffset.y = -dragOffset.y;
var abs = new Vector2(
Mathf.Abs(dragOffset.x),
Mathf.Abs(dragOffset.y));
var axis = abs.x < abs.y ? 1 : 0;
startingValue *= 1 + Mathf.Sign(dragOffset[axis]) * dragOffset.magnitude / Screen.dpi;
SetLocalValue(target, startingValue);
}
/************************************************************************************************************************/
private static readonly GUIContent PasteContentValue = new GUIContent("P");
public override GUIContent PasteContent => PasteContentValue;
private static readonly GUIContent SnapContentValue = new GUIContent("S");
public override GUIContent SnapContent => SnapContentValue;
protected override string GetSnapTooltip() => $"({IGEditorUtils.ScaleSnap})";
/************************************************************************************************************************/
private static readonly NullableVector4 ClipboardValue = new NullableVector4(Vector3.one);
public override NullableVector4 PrivateClipboard => ClipboardValue;
/************************************************************************************************************************/
protected override void DoVectorField(Rect area, float labelRight)
{
DoUniformScaleToggleGUI(ref area);
if (!ParentEditor.UseUniformScale || !HasUniformScale())
{
ParentEditor.UseUniformScale = false;
base.DoVectorField(area, labelRight);
}
else
{
DoUniformScaleGUI(area);
}
}
/************************************************************************************************************************/
private void DoUniformScaleToggleGUI(ref Rect area)
{
var enabled = GUI.enabled;
if (!ParentEditor.UseUniformScale &&
!ParentEditor.CurrentIsLocalMode &&
ParentEditor.Rotation.IsArbitrarilyRotated)
GUI.enabled = false;
var padding = IGEditorUtils.Spacing;
var buttonArea = IGEditorUtils.StealFromLeft(ref area, InternalGUI.UniformScaleButtonStyle.fixedWidth + padding);
buttonArea.x += padding;
var content = ParentEditor.UseUniformScale ? EditorStrings.GUI.NonUniformScale : EditorStrings.GUI.UniformScale;
if (GUI.Button(buttonArea, content, InternalGUI.UniformScaleButtonStyle))
{
ParentEditor.UseUniformScale = !ParentEditor.UseUniformScale;
GUIUtility.hotControl = 0;
GUIUtility.keyboardControl = 0;
if (ParentEditor.UseUniformScale)
ApplyUniformScale();
}
GUI.enabled = enabled;
}
/************************************************************************************************************************/
private bool HasUniformScale()
{
return
DisplayValues.x == DisplayValues.y &&
DisplayValues.x == DisplayValues.z;
}
/************************************************************************************************************************/
private void ApplyUniformScale()
{
RecordTargetsForUndo(UndoName);
for (int i = 0; i < Targets.Length; i++)
{
var target = Targets[i];
var scale = GetCurrentValue(target);
scale.x = scale.y = scale.z = (scale.x + scale.y + scale.z) / 3f;
SetCurrentValue(target, scale);
}
UpdateDisplayValues();
}
/************************************************************************************************************************/
private void DoUniformScaleGUI(Rect area)
{
area.xMin -= 2;
area.width += 0.5f;// Not sure why, but this makes it line up properly.
EditorGUI.BeginChangeCheck();
var labelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = InternalGUI.NameLabelWidth - 5;
DisplayValues.x = MultiFloatField(
area, _MainSerializedProperty, Color.white, CurrentLabel, DisplayValues.x, IGEditorUtils.ScaleSnap, 1);
EditorGUIUtility.labelWidth = labelWidth;
if (EditorGUI.EndChangeCheck() && DisplayValues.x != null)
{
RecordTargetsForUndo(UndoName);
var x = DisplayValues.x.Value;
var scale = new Vector3(x, x, x);
for (int i = 0; i < Targets.Length; i++)
SetCurrentValue(i, scale);
}
}
/************************************************************************************************************************/
public override void OnPropertyContextMenu(GenericMenu menu, SerializedProperty property)
{
AddPropertyNameItem(menu, property);
var axis = CurrentVectorAxis;
if (axis < 0)// Vector Label.
{
AddVectorClipboardFunctions(menu);
menu.AddSeparator("");
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Negate (*= -1)",
(targetProperty) => targetProperty.vector3Value *= -1);
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Average",
(targetProperty) =>
{
var value = targetProperty.vector3Value;
value.x = value.y = value.z = (value.x + value.y + value.z) / 3;
targetProperty.vector3Value = value;
});
var currentEditor = ParentEditor;
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Randomize}0.5-1.5 (Uniform)",
(targetProperty) =>
{
var value = Random.value;
targetProperty.vector3Value = new Vector3(value, value, value);
currentEditor.UseUniformScale = true;
});
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Randomize}0.5-1.5 (Non-Uniform)",
(targetProperty) =>
{
targetProperty.vector3Value = new Vector3(Random.value, Random.value, Random.value);
});
AddSnapVectorToGridItem(menu, property);
SerializedPropertyContextMenu.Vector3MenuHandler.AddLogFunction(menu, property);
}
else// X, Y, Z.
{
AddFloatClipboardFunctions(menu, axis);
menu.AddSeparator("");
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Negate (*= -1)",
(targetProperty) => targetProperty.floatValue *= -1);
AddSnapFloatToGridItem(menu, property);
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Randomize}0.5-1.5",
(targetProperty) => targetProperty.floatValue = Random.value);
menu.AddSeparator("");
SerializedPropertyContextMenu.AddLogValueFunction(menu, property, (targetProperty) => targetProperty.floatValue);
}
}
/************************************************************************************************************************/
public static void AddSnapVectorToGridItem(GenericMenu menu, SerializedProperty property)
{
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Snap to Grid ({IGEditorUtils.ScaleSnap})",
(targetProperty) => targetProperty.vector3Value = IGEditorUtils.SnapScale(targetProperty.vector3Value));
}
public static void AddSnapFloatToGridItem(GenericMenu menu, SerializedProperty property)
{
menu.AddPropertyModifierFunction(property,
$"{SerializedPropertyContextMenu.Set}Snap to Grid ({IGEditorUtils.ScaleSnap})",
(targetProperty) => targetProperty.floatValue = IGEditorUtils.Snap(targetProperty.floatValue, IGEditorUtils.ScaleSnap));
}
/************************************************************************************************************************/
protected override string GetCurrentModePropertyPrefix()
{
return ParentEditor.CurrentIsLocalMode ? "localScale" : "lossyScale";
}
/************************************************************************************************************************/
#region Custom Handles
/************************************************************************************************************************/
public override void DoToolGUI(Transform target, Vector3 handlePosition)
{
EditorGUI.BeginChangeCheck();
var size = HandleUtility.GetHandleSize(handlePosition);
var newScale = Handles.ScaleHandle(target.localScale, handlePosition, target.rotation, size);
if (EditorGUI.EndChangeCheck() && GUI.enabled)
{
RecordTransformForUndo(target, UndoName);
var scaleOffset = target.localScale;
scaleOffset.x = newScale.x / scaleOffset.x;
scaleOffset.y = newScale.y / scaleOffset.y;
scaleOffset.z = newScale.z / scaleOffset.z;
if (TransformEditor.DrawAllGizmos)
{
ApplyScale(target, handlePosition, scaleOffset);
}
else
{
for (int i = 0; i < Targets.Length; i++)
{
ApplyScale(Targets[i], handlePosition, scaleOffset);
}
}
}
}
/************************************************************************************************************************/
private void ApplyScale(Transform target, Vector3 pivot, Vector3 scaleOffset)
{
if (ParentEditor.CurrentFreezeChildTransforms)
{
PositionDrawer.CacheChildPositions(target);
}
if (pivot == target.position)
{
target.localScale = Vector3.Scale(target.localScale, scaleOffset);
}
else
{
var positionOffset = target.position - pivot;
target.localScale = Vector3.Scale(target.localScale, scaleOffset);
target.position = pivot + Vector3.Scale(positionOffset, scaleOffset);
}
if (ParentEditor.CurrentFreezeChildTransforms)
{
PositionDrawer.RevertChildPositions(target);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5c0f294e9b1d7a14d9f3a8d060c392d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,418 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Editor.PropertyDrawers;
using System;
using System.Reflection;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only] A custom Inspector for <see cref="Transform"/> components.</summary>
#if !DISABLE_IG_TRANSFORM_INSPECTOR
[CustomEditor(typeof(Transform))]
[CanEditMultipleObjects]
#endif
public class TransformEditor : UnityEditor.Editor
{
/************************************************************************************************************************/
#region Fields
/************************************************************************************************************************/
internal PositionDrawer Position { get; private set; }
internal RotationDrawer Rotation { get; private set; }
internal ScaleDrawer Scale { get; private set; }
internal Transform[] Targets { get; private set; }
internal bool CurrentIsLocalMode { get; private set; }
internal bool CurrentFreezeChildTransforms { get; private set; }
internal bool UseUniformScale { get; set; }
/************************************************************************************************************************/
internal static readonly AutoPrefs.EditorBool
DefaultToUniformScale = new(
EditorStrings.PrefsKeyPrefix + nameof(DefaultToUniformScale), true),
IsLocalMode = new(
EditorStrings.PrefsKeyPrefix + nameof(IsLocalMode), true),
OverrideTransformGizmos = new(
EditorStrings.PrefsKeyPrefix + nameof(OverrideTransformGizmos), true),
FreezeChildTransforms = new(
EditorStrings.PrefsKeyPrefix + nameof(FreezeChildTransforms), false),
DrawAllGizmos = new(
EditorStrings.PrefsKeyPrefix + nameof(DrawAllGizmos), false);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Inspector
/************************************************************************************************************************/
private void OnEnable()
{
var targetCount = targets.Length;
if (Targets == null || Targets.Length != targetCount)
Targets = new Transform[targetCount];
for (int i = 0; i < targetCount; i++)
Targets[i] = targets[i] as Transform;
if (Position == null)
{
Position = new PositionDrawer(this);
Rotation = new RotationDrawer(this);
Scale = new ScaleDrawer(this);
}
Position.OnEnable(serializedObject);
Rotation.OnEnable(serializedObject);
Scale.OnEnable(serializedObject);
CurrentIsLocalMode = IsLocalMode;
CurrentFreezeChildTransforms = FreezeChildTransforms;
UseUniformScale = DefaultToUniformScale;
}
private void OnDisable()
{
Tools.hidden = false;
IsLocalMode.Value = CurrentIsLocalMode;
FreezeChildTransforms.Value = CurrentFreezeChildTransforms;
Position.OnDisable();
Rotation.OnDisable();
Scale.OnDisable();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnInspectorGUI()
{
if (Event.current.type == EventType.Layout)
{
Rotation.CheckIfArbitrarilyRotated();
if (!CurrentIsLocalMode && Rotation.IsArbitrarilyRotated)
UseUniformScale = false;
}
var height = Position.Height + Rotation.Height + Scale.Height + IGEditorUtils.Spacing;
var area = EditorGUILayout.GetControlRect(true, height);
DoModeButtons(ref area);
area.height = Position.Height;
Position.DoInspectorGUI(area);
area.y += area.height + IGEditorUtils.Spacing;
area.height = Rotation.Height;
Rotation.DoInspectorGUI(area);
area.y += area.height + IGEditorUtils.Spacing;
area.height = Scale.Height;
Scale.DoInspectorGUI(area);
DoWarnings();
}
/************************************************************************************************************************/
#region Mode Buttons
/************************************************************************************************************************/
private void DoModeButtons(ref Rect area)
{
var style = InternalGUI.MiniSquareButtonStyle;
var xMin = area.xMin;
xMin -= IGEditorUtils.IndentSize - 1;
if (xMin < IGEditorUtils.Spacing)
xMin = IGEditorUtils.Spacing;
area.xMin = xMin;
var buttonArea = area;
buttonArea.width = style.fixedWidth;
area.xMin += buttonArea.width;
var guiEnabled = GUI.enabled;
buttonArea.height = style.fixedHeight;
// Local / World.
var label = CurrentIsLocalMode ? EditorStrings.GUI.LocalMode : EditorStrings.GUI.WorldMode;
if (GUI.Button(buttonArea, label, style))
{
CurrentIsLocalMode = !CurrentIsLocalMode;
Rotation.CacheEulerAngles();
GUIUtility.hotControl = 0;
GUIUtility.keyboardControl = 0;
}
var height = style.fixedHeight + IGEditorUtils.Spacing;
buttonArea.y += height;
if (!OverrideTransformGizmos)
GUI.enabled = false;
// Freeze Child Transforms.
EditorGUI.BeginChangeCheck();
CurrentFreezeChildTransforms = GUI.Toggle(buttonArea, CurrentFreezeChildTransforms,
EditorStrings.GUI.FreezeChildTransforms, style);
if (EditorGUI.EndChangeCheck())
{
SceneView.RepaintAll();
}
// Draw All Gizmos.
buttonArea.y += height;
if (GUI.Toggle(buttonArea, DrawAllGizmos, EditorStrings.GUI.DrawAllGizmos, style) != DrawAllGizmos)
{
DrawAllGizmos.Invert();
SceneView.RepaintAll();
}
GUI.enabled = guiEnabled;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
private void DoWarnings()
{
if (!CurrentIsLocalMode && Rotation.IsArbitrarilyRotated)
{
GUILayout.Space(IGEditorUtils.Spacing);
EditorGUILayout.HelpBox(EditorStrings.GUI.ScaleSkewWarning, MessageType.Warning);
GUILayout.Space(-IGEditorUtils.Spacing);
}
const float PositionWarningThreshold = 100000f;
var position = Targets[0].position;
if (Math.Abs(position.x) > PositionWarningThreshold ||
Math.Abs(position.y) > PositionWarningThreshold ||
Math.Abs(position.z) > PositionWarningThreshold)
{
GUILayout.Space(IGEditorUtils.Spacing);
EditorGUILayout.HelpBox(EditorStrings.GUI.PrecisionWarning, MessageType.Warning);
GUILayout.Space(-IGEditorUtils.Spacing);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Scene GUI
/************************************************************************************************************************/
/// <summary>Draws custom Scene View controls.</summary>
protected virtual void OnSceneGUI()
{
if (!OverrideTransformGizmos)
return;
if (!CustomTransformGizmo.ShouldShow())
{
Tools.hidden = false;
return;
}
TransformPropertyDrawer currentTool;
switch (Tools.current)
{
case Tool.Move: currentTool = Position; break;
case Tool.Rotate: currentTool = Rotation; break;
case Tool.Scale: currentTool = Scale; break;
default:
Tools.hidden = false;
return;
}
// Ignore controls while holding alt so it can control the camera without clicking on handles.
if (Event.current.alt && Event.current.type != EventType.Repaint)
return;
Tools.hidden = true;
if (DrawAllGizmos || target == Selection.activeTransform)
{
DoHandlesGUI(target as Transform, currentTool);
}
}
/************************************************************************************************************************/
private static readonly Color
FrozenChildLineColor = new Color(1, 0, 0, 0.25f),
FrozenChildDotColor = new Color(1, 0, 0, 0.5f);
private void DoHandlesGUI(Transform target, TransformPropertyDrawer currentTool)
{
var handlePosition = GetHandlePosition(target);
EditorGUI.BeginDisabledGroup(ShouldDisableSceneTools());
{
currentTool.DoToolGUI(target, handlePosition);
if (CurrentFreezeChildTransforms && Event.current.type == EventType.Repaint)
{
var color = Handles.color;
var parentPosition = target.position;
for (int i = 0; i < target.childCount; i++)
{
var child = target.GetChild(i);
var childPosition = child.position;
Handles.color = FrozenChildLineColor;
Handles.DrawLine(parentPosition, childPosition);
Handles.color = FrozenChildDotColor;
Handles.SphereHandleCap(0, childPosition, child.rotation,
HandleUtility.GetHandleSize(childPosition) * 0.1f, EventType.Repaint);
}
Handles.color = color;
}
if (EditorApplication.isPlaying && target.gameObject.isStatic)
ShowStaticLabel(handlePosition);
}
EditorGUI.EndDisabledGroup();
}
/************************************************************************************************************************/
private static readonly FieldInfo
HandleOffset = typeof(Tools).GetField("handleOffset", IGEditorUtils.StaticBindings),
LocalHandleOffset = typeof(Tools).GetField("localHandleOffset", IGEditorUtils.StaticBindings);
internal static Vector3 GetHandlePosition(Transform target)
{
if (target == null)
return new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
Vector3 offset;
if (HandleOffset != null)
offset = (Vector3)HandleOffset.GetValue(null);
else
offset = default;
if (LocalHandleOffset != null)
offset += Tools.handleRotation * (Vector3)LocalHandleOffset.GetValue(null);
switch (Tools.pivotMode)
{
case PivotMode.Center:
if (DrawAllGizmos)
return CalculateBounds(target).center + offset;
return InternalEditorUtility.CalculateSelectionBounds(true, false).center + offset;
case PivotMode.Pivot:
default:
return target.position + offset;
}
}
/************************************************************************************************************************/
private static Bounds CalculateBounds(Transform target)
{
var bounds = new Bounds();
var hasOrigin = false;
#if UNITY_PHYSICS_3D
var colliders = target.GetComponentsInChildren<Collider>();
for (int i = 0; i < colliders.Length; i++)
EncapsulateBounds(ref bounds, colliders[i].bounds, ref hasOrigin);
#endif
#if UNITY_PHYSICS_2D
var colliders2D = target.GetComponentsInChildren<Collider2D>();
for (int i = 0; i < colliders2D.Length; i++)
EncapsulateBounds(ref bounds, colliders2D[i].bounds, ref hasOrigin);
#endif
var renderers = target.GetComponentsInChildren<Renderer>();
for (int i = 0; i < renderers.Length; i++)
EncapsulateBounds(ref bounds, renderers[i].bounds, ref hasOrigin);
if (!hasOrigin)
bounds.center = target.position;
return bounds;
}
private static void EncapsulateBounds(ref Bounds bounds, Bounds encapsulate, ref bool hasOrigin)
{
if (!hasOrigin)
{
hasOrigin = true;
bounds = encapsulate;
}
else
{
bounds.Encapsulate(encapsulate);
}
}
/************************************************************************************************************************/
/// <summary>
/// Returns true in Play Mode if any of the selected objects are marked as static.
/// </summary>
private bool ShouldDisableSceneTools()
{
if (!EditorApplication.isPlaying)
return false;
for (int i = 0; i < Targets.Length; i++)
{
if (Targets[i].gameObject.isStatic)
return true;
}
return false;
}
/************************************************************************************************************************/
private static GUIContent _StaticLabel;
private static GUIStyle _StaticLabelStyle;
// UnityEditor.Handles.ShowStaticLabel.
private static void ShowStaticLabel(Vector3 position)
{
if (_StaticLabel == null)
{
_StaticLabel = new GUIContent("Static");
_StaticLabelStyle = "SC ViewAxisLabel";
_StaticLabelStyle.alignment = TextAnchor.MiddleLeft;
_StaticLabelStyle.fixedWidth = 0f;
}
Handles.color = Color.white;
Handles.zTest = UnityEngine.Rendering.CompareFunction.Always;
Handles.BeginGUI();
var rect = HandleUtility.WorldPointToSizedRect(position, _StaticLabel, _StaticLabelStyle);
rect.x += 10f;
rect.y += 10f;
GUI.Label(rect, _StaticLabel, _StaticLabelStyle);
Handles.EndGUI();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9edf4968362d1c84b964f149978ccf62
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 58cf44485b662fd438585709c4b1a624
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
{
"name": "Kybernetik.InspectorGadgets.Editor",
"rootNamespace": "",
"references": [
"GUID:4516fe32c11b32b4c8eb0d5ed6f6d7bc"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [
{
"name": "com.unity.modules.animation",
"expression": "",
"define": "UNITY_ANIMATION"
},
{
"name": "com.unity.modules.audio",
"expression": "",
"define": "UNITY_AUDIO"
},
{
"name": "com.unity.modules.physics2d",
"expression": "",
"define": "UNITY_PHYSICS_2D"
},
{
"name": "com.unity.modules.physics",
"expression": "",
"define": "UNITY_PHYSICS_3D"
},
{
"name": "com.unity.test-framework",
"expression": "",
"define": "UNITY_TEST_FRAMEWORK"
},
{
"name": "com.unity.ugui",
"expression": "",
"define": "UNITY_UGUI"
}
],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: de90c74d1c031bf4c98cb03e4b33ce01
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a3f5a3f177efb4845ac6cb76662550e8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Kybernetik.InspectorGadgets.Editor")]
[assembly: AssemblyDescription("A variety of tools which enhance the Unity Editor.")]
[assembly: AssemblyCompany("Kybernetik")]
[assembly: AssemblyProduct("Inspector Gadgets")]
[assembly: AssemblyCopyright("Copyright © Kybernetik 2017-2025")]
[assembly: ComVisible(false)]
[assembly: AssemblyVersion(InspectorGadgets.Strings.InspectorGadgetsVersion + ".0")]
#if UNITY_EDITOR
[assembly: SuppressMessage("Style", "IDE0016:Use 'throw' expression",
Justification = "Not supported by older Unity versions.")]
[assembly: SuppressMessage("Style", "IDE0019:Use pattern matching",
Justification = "Not supported by older Unity versions.")]
[assembly: SuppressMessage("Style", "IDE0039:Use local function",
Justification = "Not supported by older Unity versions.")]
[assembly: SuppressMessage("Style", "IDE0044:Make field readonly",
Justification = "Using the [SerializeField] attribute on a private field means Unity will set it from serialized data.")]
[assembly: SuppressMessage("Code Quality", "IDE0051:Remove unused private members",
Justification = "Unity messages can be private, but the IDE will not know that Unity can still call them.")]
[assembly: SuppressMessage("Code Quality", "IDE0052:Remove unread private members",
Justification = "Unity messages can be private and don't need to be called manually.")]
[assembly: SuppressMessage("Style", "IDE0059:Value assigned to symbol is never used",
Justification = "Inline variable declarations are not supported by older Unity versions.")]
[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter",
Justification = "Unity messages sometimes need specific signatures, even if you don't use all the parameters.")]
[assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement",
Justification = "Not supported by older Unity versions.")]
[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression",
Justification = "Not supported by older Unity versions.")]
[assembly: SuppressMessage("Code Quality", "IDE0067:Dispose objects before losing scope",
Justification = "Not always relevant.")]
[assembly: SuppressMessage("Code Quality", "IDE0068:Use recommended dispose pattern",
Justification = "Not always relevant.")]
[assembly: SuppressMessage("Code Quality", "IDE0069:Disposable fields should be disposed",
Justification = "Not always relevant.")]
[assembly: SuppressMessage("Style", "IDE0074:Use compound assignment",
Justification = "Not supported by older Unity versions.")]
[assembly: SuppressMessage("Style", "IDE0083:Use pattern matching",
Justification = "Not supported by older Unity versions")]
[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'",
Justification = "Not supported by older Unity versions.")]
[assembly: SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression",
Justification = "Don't give code style advice in publically released code.")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles",
Justification = "Don't give code style advice in publically released code.")]
[assembly: SuppressMessage("Type Safety", "UNT0014:Invalid type for call to GetComponent",
Justification = "Doesn't account for generic constraints.")]
[assembly: SuppressMessage("Code Quality", "CS0649:Field is never assigned to, and will always have its default value",
Justification = "Using the [SerializeField] attribute on a private field means Unity will set it from serialized data.")]
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5dd39c7bbde1a6949b9fe1cf95d8c1c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,414 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only]
/// Automatically hides the UI layer inside the Editor so it doesn't get in the way of 3D objects in scene view.
///<para></para>
/// When any object is selected that is on the UI layer, the layer will be shown and the camera changed to 2D orthographic and zoomed to the current selection.
///<para></para>
/// When any object on another layer is selected, the UI layer will be hidden and the camera changed back to the previous state.
/// </summary>
/// <remarks>
/// The only reason this class inherits from <see cref="ScriptableObject"/> is so that
/// <see cref="MonoScript.FromScriptableObject"/> can be used to find its file path to determine whether it is in
/// the process of being deleted.
/// </remarks>
[InitializeOnLoad]
public sealed class AutoHideUI : ScriptableObject
{
/************************************************************************************************************************/
/// <summary>The inbuilt layer named "UI".</summary>
public const int
UiLayer = 5;
/// <summary>Is this system currently operating?</summary>
public static readonly AutoPrefs.EditorBool
IsEnabled = new(EditorStrings.PrefsKeyPrefix + nameof(AutoHideUI), false, OnEnabledChanged);
/// <summary>Should this system only activate for objects with a <see cref="Canvas"/> on a parent?</summary>
public static readonly AutoPrefs.EditorBool
RequireCanvas = new(EditorStrings.PrefsKeyPrefix + nameof(RequireCanvas), true);
/// <summary>Higher values cause the framed area to be larger on screen.</summary>
public static readonly AutoPrefs.EditorFloat
ZoomMultiplier = new(EditorStrings.PrefsKeyPrefix + nameof(ZoomMultiplier), 1);
/// <summary>Is the UI layer currently visible?</summary>
public static readonly AutoPrefs.EditorBool
IsShowingUI = EditorStrings.PrefsKeyPrefix + nameof(IsShowingUI);
/// <summary>
/// If true, selecting a UI object will focus the scene camera on that object. Otherwise it will focus on the
/// root <see cref="Canvas"/> of that object.
/// </summary>
public static readonly AutoPrefs.EditorBool
FocusOnSelection = EditorStrings.PrefsKeyPrefix + nameof(FocusOnSelection);
/// <summary>Was the scene camera in 2D mode before the UI was focussed?</summary>
public static readonly AutoPrefs.EditorBool
Previous2dMode = EditorStrings.PrefsKeyPrefix + nameof(Previous2dMode);
/// <summary>Was the scene camera in Orthographic mode before the UI was focussed?</summary>
public static readonly AutoPrefs.EditorBool
PreviousOrthographicMode = EditorStrings.PrefsKeyPrefix + nameof(PreviousOrthographicMode);
/// <summary>The bit mask of layers that are considered to contain UI objects.</summary>
public static readonly AutoPrefs.EditorInt
UILayerMask = new(EditorStrings.PrefsKeyPrefix + nameof(UILayerMask), 1 << UiLayer);
/// <summary>The pivot point of the scene camera from before the UI was focussed.</summary>
public static readonly AutoPrefs.EditorVector3
PreviousPivot = EditorStrings.PrefsKeyPrefix + nameof(PreviousPivot);
/// <summary>The rotation of the scene camera from before the UI was focussed.</summary>
public static readonly AutoPrefs.EditorQuaternion
PreviousRotation = EditorStrings.PrefsKeyPrefix + nameof(PreviousRotation);
/// <summary>The orthographic size of the scene camera from before the UI was focussed.</summary>
public static readonly AutoPrefs.EditorFloat
PreviousSize = new(EditorStrings.PrefsKeyPrefix + nameof(PreviousSize), 1);
/************************************************************************************************************************/
static AutoHideUI()
{
EditorApplication.delayCall += () =>
{
if (!IsEnabled.IsSaved())
{
Selection.selectionChanged += OnFirstRun;
}
else if (IsEnabled)
{
Selection.selectionChanged += OnSelectionChanged;
EditorApplication.playModeStateChanged += OnSelectionChanged;
if (!EditorApplication.isPlayingOrWillChangePlaymode && !ShouldShow())
{
Tools.visibleLayers &= ~UILayerMask;
}
}
EditorApplication.quitting += OnEditorQuit;
};
AssemblyReloadEvents.beforeAssemblyReload += DisableOnDelete;
}
/************************************************************************************************************************/
private static void OnFirstRun()
{
if (!ShouldShow())
return;
Selection.selectionChanged -= OnFirstRun;
IsEnabled.Value = false;
const string FirstRunMessage =
@"Would you like Inspector Gadgets to automatically show and hide the UI layer?
On UI selected: show UI layer, enter 2D orthographic mode, and focus the camera on the selected object.
On UI deselected: hide UI layer and return camera to the previous state.
You can turn this feature on/off via the Inspector Gadgets tab in the Edit/Preferences menu.";
if (EditorUtility.DisplayDialog("Auto Hide UI", FirstRunMessage, "Enable", "Do Nothing"))
{
IsEnabled.Value = true;
EditorApplication.delayCall += Enable;
}
}
/************************************************************************************************************************/
private static void Enable()
{
Selection.selectionChanged += OnSelectionChanged;
EditorApplication.playModeStateChanged += OnSelectionChanged;
OnSelectionChanged();
}
/************************************************************************************************************************/
private static bool ShouldShow()
{
var activeGameObject = Selection.activeGameObject;
if (activeGameObject == null ||
!activeGameObject.activeInHierarchy ||
EditorUtility.IsPersistent(activeGameObject) ||
GUIUtility.hotControl != 0 ||
activeGameObject.transform.rotation != Quaternion.identity)
return false;
#if UNITY_UGUI
var canvas = activeGameObject.GetComponentInParent<Canvas>();
if (canvas != null)
{
if (!canvas.enabled ||
canvas.renderMode == RenderMode.WorldSpace)
return false;
}
else
{
if (RequireCanvas)
return false;
}
#endif
if (activeGameObject.layer == UiLayer)
return true;
return false;
}
/************************************************************************************************************************/
private static void OnSelectionChanged()
{
if (ShouldShow())
ShowUI();
else
HideUI();
}
private static void OnSelectionChanged(PlayModeStateChange change)
=> OnSelectionChanged();
/************************************************************************************************************************/
private static Vector3[] _Corners;
private static void ShowUI()
{
if (IsShowingUI)
return;
var sceneView = SceneView.lastActiveSceneView;
if (sceneView == null)
return;
// Store the current scene view state.
Previous2dMode.Value = sceneView.in2DMode;
PreviousOrthographicMode.Value = sceneView.orthographic;
PreviousPivot.Value = sceneView.pivot;
PreviousRotation.Value = sceneView.rotation;
PreviousSize.Value = sceneView.size;
// Apply UI mode and show the UI layer.
sceneView.in2DMode = true;
sceneView.orthographic = true;
Tools.visibleLayers |= UILayerMask;
IsShowingUI.Value = true;
if (!FocusOnSelection)
{
var rootRect = Selection.activeTransform.root.GetComponentInChildren<RectTransform>();
if (rootRect != null)
{
_Corners ??= new Vector3[4];
rootRect.GetWorldCorners(_Corners);
var bounds = new Bounds(_Corners[0], default);
bounds.Encapsulate(_Corners[1]);
bounds.Encapsulate(_Corners[2]);
bounds.Encapsulate(_Corners[3]);
var size = Mathf.Max(bounds.size.x / sceneView.camera.aspect, bounds.size.y);
size *= 0.6f / ZoomMultiplier;
if (size > 0)
{
// This gives us a much better fit than sceneView.Frame(bounds, false);
sceneView.LookAt(bounds.center, rootRect.rotation, size, true, false);
return;
}
}
}
sceneView.FrameSelected();
}
/************************************************************************************************************************/
private static void HideUI()
{
if (!IsShowingUI)
return;
var previousPivot = PreviousPivot.Value;
var previousRotation = PreviousRotation.Value;
var previousSize = PreviousSize.Value;
var sceneView = SceneView.lastActiveSceneView;
if (sceneView == null)
return;
// Return to the stored scene view state and hide the UI layer.
sceneView.in2DMode = Previous2dMode;
// Only revert the camera state if all the values are actually valid.
if (IsValidNumber(previousPivot.x) &&
IsValidNumber(previousPivot.y) &&
IsValidNumber(previousPivot.z) &&
IsValidNumber(previousRotation.x) &&
IsValidNumber(previousRotation.y) &&
IsValidNumber(previousRotation.z) &&
IsValidNumber(previousRotation.w) &&
IsValidNumber(previousSize))
{
sceneView.LookAt(previousPivot, previousRotation, previousSize, PreviousOrthographicMode);
}
Tools.visibleLayers &= ~UILayerMask;
IsShowingUI.Value = false;
}
/************************************************************************************************************************/
private static bool IsValidNumber(float value)
=> !float.IsNaN(value)
&& !float.IsInfinity(value);
/************************************************************************************************************************/
private static void OnEditorQuit()
{
// If we are currently enabled when the editor closes, show the UI layer in case the next project the user
// opens doesn't have this script.
if (IsEnabled)
{
Tools.visibleLayers |= UILayerMask;
}
}
/************************************************************************************************************************/
private static void DisableOnDelete()
{
if (!IsEnabled)
return;
var instance = CreateInstance<AutoHideUI>();
var thisScript = MonoScript.FromScriptableObject(instance);
DestroyImmediate(instance);
if (thisScript == null)
OnEnabledChanged(false);
}
/************************************************************************************************************************/
#region Preferences
/************************************************************************************************************************/
internal static class Preferences
{
/************************************************************************************************************************/
public const string Headding = "Auto Hide UI";
private static readonly GUIContent
EnabledToggleContent = new("Enabled",
"If enabled, the UI layer will be automatically hidden until a UI object is selected."),
UILayerMaskContent = new("UI Layer Mask",
"Objects on these layers will trigger the Auto Hide feature."),
RequireCanvasContent = new("Require Canvas",
"Should this system only activate for objects with a Canvas on a parent?."),
ZoomMultiplierContent = new("Zoom Multiplier",
"Higher values cause the framed area to be larger on screen");
private static readonly GUIContent[] FocusModes =
{
new("Root",
"When a UI object is first selected, the scene camera will frame the root RectTransform of the selected object."),
new("Selection",
"When a UI object is first selected, the scene camera will frame the selected object."),
};
/************************************************************************************************************************/
internal static void DoGUI()
{
Editor.Preferences.DoSectionHeader(Headding);
// Enabled.
IsEnabled.DoGUI(EnabledToggleContent);
GUI.enabled = IsEnabled;
#if UNITY_UGUI
RequireCanvas.DoGUI(RequireCanvasContent);
#endif
ZoomMultiplier.DoGUI(ZoomMultiplierContent);
ZoomMultiplier.Value = Mathf.Clamp(ZoomMultiplier, 0.5f, 2);
// UI Layer Mask.
UILayerMask.DoGUI(UILayerMaskContent,
(area, label, value, style) => IGEditorUtils.DoLayerMaskField(area, label, value));
// Auto Focus Mode.
FocusOnSelection.DoGUI(IGEditorUtils.TempContent("UI Auto Focus Mode"),
(area, label, value, style) =>
{
EditorGUI.PrefixLabel(area, label);
area.xMin += EditorGUIUtility.labelWidth;
var previousMode = value ? 1 : 0;
return GUI.Toolbar(area, previousMode, FocusModes) != 0;
});
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
private static void OnEnabledChanged(bool value)
{
if (value)
{
Enable();
}
else
{
Selection.selectionChanged -= OnSelectionChanged;
EditorApplication.playModeStateChanged -= OnSelectionChanged;
HideUI();
Tools.visibleLayers |= UILayerMask;
}
}
/************************************************************************************************************************/
[MenuItem(EditorStrings.RectTransformToggleAutoHideUI)]
[MenuItem(EditorStrings.UIBehaviourToggleAutoHideUI)]
#if UNITY_UGUI
[MenuItem(EditorStrings.CanvasToggleAutoHideUI)]
#endif
private static void ToggleEnabled()
{
IsEnabled.Invert();
Debug.Log($"Inspector Gadgets Auto Hide UI is now {(IsEnabled.Value ? "Enabled" : "Disabled")}");
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 47bd92d356944e84dabbeab0fb6c3e55
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5ba8b94c0dcb1f64688ee2fd3c62af4c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,572 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEngine;
using UnityEditor;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only]
/// A collection of wrappers for EditorPrefs which simplify the way you can store and retrieve values.
/// </summary>
/// <remarks>PlayerPrefs versions can be found in <see cref="InspectorGadgets.AutoPrefs"/></remarks>
public static class AutoPrefs
{
/************************************************************************************************************************/
[InitializeOnLoadMethod]
private static void Initialize()
{
InspectorGadgets.AutoPrefs.OnAnyValueChanged +=
UnityEditorInternal.InternalEditorUtility.RepaintAllViews;
}
/************************************************************************************************************************/
#region GUI
/************************************************************************************************************************/
/// <summary>[Editor-Only] A delegate used to draw a GUI field and return its value.</summary>
public delegate T GUIFieldMethod<T>(Rect area, GUIContent label, T value, GUIStyle style);
/************************************************************************************************************************/
/// <summary>[Editor-Only] Draws GUI controls for this pref and returns true if the value was changed.</summary>
public static bool DoGUI<T>(
this InspectorGadgets.AutoPrefs.AutoPref<T> pref,
Rect area,
GUIContent label,
GUIStyle style,
GUIFieldMethod<T> doGUIField)
{
var resetStyle = InternalGUI.MiniSquareButtonStyle;
var isDefault = Equals(pref.Value, pref.DefaultValue);
if (!isDefault)
area.width -= resetStyle.fixedWidth;
EditorGUI.BeginChangeCheck();
var value = doGUIField(area, label, pref.Value, style);
if (EditorGUI.EndChangeCheck())
{
pref.Value = value;
return true;
}
if (!isDefault)
{
var resetPosition = new Rect(area.xMax, area.y, resetStyle.fixedWidth, resetStyle.fixedHeight);
if (GUI.Button(resetPosition, EditorStrings.GUI.Reset, resetStyle))
{
pref.Value = pref.DefaultValue;
return true;
}
}
return false;
}
/// <summary>[Editor-Only] Draws GUI controls for this pref and returns true if the value was changed.</summary>
public static bool DoGUI<T>(
this InspectorGadgets.AutoPrefs.AutoPref<T> pref,
GUIContent label,
GUIStyle style,
GUIFieldMethod<T> doGUIField)
=> pref.DoGUI(GetControlRect(), label, style, doGUIField);
/// <summary>[Editor-Only] Draws GUI controls for this pref and returns true if the value was changed.</summary>
public static bool DoGUI<T>(
this InspectorGadgets.AutoPrefs.AutoPref<T> pref,
GUIContent label,
GUIFieldMethod<T> doGUIField)
=> pref.DoGUI(GetControlRect(), label, null, doGUIField);
/************************************************************************************************************************/
/// <summary>
/// Uses <see cref="EditorGUILayout.GetControlRect(bool, float, GUIStyle, GUILayoutOption[])"/>
/// to allocate a <see cref="GUILayout"/> <see cref="Rect"/> for a control.
/// </summary>
public static Rect GetControlRect()
=> EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight);
/************************************************************************************************************************/
/// <summary>Draws a GUI field for an <see cref="InspectorGadgets.AutoPrefs.Bool"/>.</summary>
public static bool DoGUI(this InspectorGadgets.AutoPrefs.Bool pref, GUIContent label)
=> pref.DoGUI(label, EditorStyles.toggle, EditorGUI.Toggle);
/// <summary>Draws a GUI field for an <see cref="InspectorGadgets.AutoPrefs.Float"/>.</summary>
public static bool DoGUI(this InspectorGadgets.AutoPrefs.Float pref, GUIContent label)
=> pref.DoGUI(label, EditorStyles.numberField, EditorGUI.FloatField);
/// <summary>Draws a GUI field for an <see cref="InspectorGadgets.AutoPrefs.Int"/>.</summary>
public static bool DoGUI(this InspectorGadgets.AutoPrefs.Int pref, GUIContent label)
=> pref.DoGUI(label, EditorStyles.numberField, EditorGUI.IntField);
/************************************************************************************************************************/
/// <summary>Draws a GUI Color field for an <see cref="InspectorGadgets.AutoPrefs.Vector4"/>.</summary>
public static bool DoColorGUI(this InspectorGadgets.AutoPrefs.Vector4 pref, GUIContent label)
=> pref.DoGUI(label,
(area, content, value, style) => EditorGUI.ColorField(area, content, value.ToColor()).ToVector4());
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// An <see cref="InspectorGadgets.AutoPrefs.AutoPref{T}"/> which encapsulates a <see cref="bool"/> value
/// stored in <see cref="EditorPrefs"/>.
/// </summary>
public class EditorBool : InspectorGadgets.AutoPrefs.Bool
{
/************************************************************************************************************************/
/// <summary>Constructs an <see cref="EditorBool"/> pref with the specified `key` and `defaultValue`.</summary>
public EditorBool(string key, bool defaultValue = default, Action<bool> onValueChanged = null)
: base(key, defaultValue, onValueChanged)
{ }
/// <summary>Loads the value of this pref from <see cref="EditorPrefs"/>.</summary>
protected override bool Load()
=> EditorPrefs.GetBool(Key, DefaultValue);
/// <summary>Saves the value of this pref to <see cref="EditorPrefs"/>.</summary>
protected override void Save()
=> EditorPrefs.SetBool(Key, Value);
/************************************************************************************************************************/
/// <summary>Returns true if <see cref="EditorPrefs"/> currently contains a value for this pref.</summary>
public override bool IsSaved()
=> EditorPrefs.HasKey(Key);
/// <summary>Deletes the value of this pref from <see cref="EditorPrefs"/> and reverts to the default value.</summary>
public override void DeletePref()
{
EditorPrefs.DeleteKey(Key);
RevertToDefaultValue();
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EditorBool"/> pref using the specified string as the key.</summary>
public static implicit operator EditorBool(string key)
=> new EditorBool(key);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// An <see cref="InspectorGadgets.AutoPrefs.AutoPref{T}"/> which encapsulates a <see cref="float"/> value stored in
/// <see cref="EditorPrefs"/>.
/// </summary>
public class EditorFloat : InspectorGadgets.AutoPrefs.Float
{
/************************************************************************************************************************/
/// <summary>Constructs an <see cref="EditorFloat"/> pref with the specified `key` and `defaultValue`.</summary>
public EditorFloat(string key, float defaultValue = default, Action<float> onValueChanged = null)
: base(key, defaultValue, onValueChanged)
{ }
/// <summary>Loads the value of this pref from <see cref="EditorPrefs"/>.</summary>
protected override float Load()
=> EditorPrefs.GetFloat(Key, DefaultValue);
/// <summary>Saves the value of this pref to <see cref="EditorPrefs"/>.</summary>
protected override void Save()
=> EditorPrefs.SetFloat(Key, Value);
/************************************************************************************************************************/
/// <summary>Returns true if <see cref="EditorPrefs"/> currently contains a value for this pref.</summary>
public override bool IsSaved()
=> EditorPrefs.HasKey(Key);
/// <summary>Deletes the value of this pref from <see cref="EditorPrefs"/> and reverts to the default value.</summary>
public override void DeletePref()
{
EditorPrefs.DeleteKey(Key);
RevertToDefaultValue();
}
/************************************************************************************************************************/
#region Operators
/************************************************************************************************************************/
/// <summary>Checks if the value of this pref is greater then the specified `value`.</summary>
public static bool operator >(EditorFloat pref, float value)
=> pref.Value > value;
/// <summary>Checks if the value of this pref is less then the specified `value`.</summary>
public static bool operator <(EditorFloat pref, float value)
=> pref.Value < value;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EditorFloat"/> pref using the specified string as the key.</summary>
public static implicit operator EditorFloat(string key)
=> new EditorFloat(key);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// An <see cref="InspectorGadgets.AutoPrefs.AutoPref{T}"/> which encapsulates an <see cref="int"/> value stored in
/// <see cref="EditorPrefs"/>.
/// </summary>
public class EditorInt : InspectorGadgets.AutoPrefs.Int
{
/************************************************************************************************************************/
/// <summary>Constructs an <see cref="EditorInt"/> pref with the specified `key` and `defaultValue`.</summary>
public EditorInt(string key, int defaultValue = default, Action<int> onValueChanged = null)
: base(key, defaultValue, onValueChanged)
{ }
/// <summary>Loads the value of this pref from <see cref="EditorPrefs"/>.</summary>
protected override int Load()
=> EditorPrefs.GetInt(Key, DefaultValue);
/// <summary>Saves the value of this pref to <see cref="EditorPrefs"/>.</summary>
protected override void Save()
=> EditorPrefs.SetInt(Key, Value);
/************************************************************************************************************************/
/// <summary>Returns true if <see cref="EditorPrefs"/> currently contains a value for this pref.</summary>
public override bool IsSaved()
=> EditorPrefs.HasKey(Key);
/// <summary>Deletes the value of this pref from <see cref="EditorPrefs"/> and reverts to the default value.</summary>
public override void DeletePref()
{
EditorPrefs.DeleteKey(Key);
RevertToDefaultValue();
}
/************************************************************************************************************************/
#region Operators
/************************************************************************************************************************/
/// <summary>Checks if the value of this pref is greater then the specified `value`.</summary>
public static bool operator >(EditorInt pref, int value)
=> pref.Value > value;
/// <summary>Checks if the value of this pref is less then the specified `value`.</summary>
public static bool operator <(EditorInt pref, int value)
=> pref.Value < value;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EditorInt"/> pref using the specified string as the key.</summary>
public static implicit operator EditorInt(string key)
=> new EditorInt(key);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// An <see cref="InspectorGadgets.AutoPrefs.AutoPref{T}"/> which encapsulates a <see cref="string"/> value stored in
/// <see cref="EditorPrefs"/>.
/// </summary>
public class EditorString : InspectorGadgets.AutoPrefs.String
{
/************************************************************************************************************************/
/// <summary>Constructs an <see cref="EditorString"/> pref with the specified `key` and `defaultValue`.</summary>
public EditorString(string key, string defaultValue = default, Action<string> onValueChanged = null)
: base(key, defaultValue, onValueChanged)
{ }
/// <summary>Loads the value of this pref from <see cref="EditorPrefs"/>.</summary>
protected override string Load()
=> EditorPrefs.GetString(Key, DefaultValue);
/// <summary>Saves the value of this pref to <see cref="EditorPrefs"/>.</summary>
protected override void Save()
=> EditorPrefs.SetString(Key, Value);
/************************************************************************************************************************/
/// <summary>Returns true if <see cref="EditorPrefs"/> currently contains a value for this pref.</summary>
public override bool IsSaved()
=> EditorPrefs.HasKey(Key);
/// <summary>Deletes the value of this pref from <see cref="EditorPrefs"/> and reverts to the default value.</summary>
public override void DeletePref()
{
EditorPrefs.DeleteKey(Key);
RevertToDefaultValue();
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EditorString"/> pref using the specified string as the key.</summary>
public static implicit operator EditorString(string key)
=> new EditorString(key);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// An <see cref="InspectorGadgets.AutoPrefs.AutoPref{T}"/> which encapsulates a <see cref="Vector2"/> value stored in
/// <see cref="EditorPrefs"/>.
/// </summary>
public class EditorVector2 : InspectorGadgets.AutoPrefs.Vector2
{
/************************************************************************************************************************/
/// <summary>Constructs an <see cref="EditorString"/> pref with the specified `key` and `defaultValue`.</summary>
public EditorVector2(string key,
Vector2 defaultValue = default,
Action<Vector2> onValueChanged = null)
: base(key, defaultValue, onValueChanged)
{ }
/// <summary>Loads the value of this pref from <see cref="EditorPrefs"/>.</summary>
protected override Vector2 Load()
=> new Vector2(
EditorPrefs.GetFloat(Key, DefaultValue.x),
EditorPrefs.GetFloat(KeyY, DefaultValue.y));
/// <summary>Saves the value of this pref to <see cref="EditorPrefs"/>.</summary>
protected override void Save()
{
EditorPrefs.SetFloat(Key, Value.x);
EditorPrefs.SetFloat(KeyY, Value.y);
}
/************************************************************************************************************************/
/// <summary>Returns true if <see cref="EditorPrefs"/> currently contains a value for this pref.</summary>
public override bool IsSaved()
=> EditorPrefs.HasKey(Key)
&& EditorPrefs.HasKey(KeyY);
/// <summary>Deletes the value of this pref from <see cref="EditorPrefs"/> and reverts to the default value.</summary>
public override void DeletePref()
{
EditorPrefs.DeleteKey(Key);
EditorPrefs.DeleteKey(KeyY);
RevertToDefaultValue();
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EditorVector2"/> pref using the specified string as the key.</summary>
public static implicit operator EditorVector2(string key)
=> new EditorVector2(key);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// An <see cref="InspectorGadgets.AutoPrefs.AutoPref{T}"/> which encapsulates a <see cref="Vector3"/> value stored in
/// <see cref="EditorPrefs"/>.
/// </summary>
public class EditorVector3 : InspectorGadgets.AutoPrefs.Vector3
{
/************************************************************************************************************************/
/// <summary>Constructs an <see cref="EditorVector3"/> pref.</summary>
public EditorVector3(string key,
Vector3 defaultValue = default,
Action<Vector3> onValueChanged = null)
: base(key, defaultValue, onValueChanged)
{ }
/// <summary>Loads the value of this pref from <see cref="EditorPrefs"/>.</summary>
protected override Vector3 Load()
=> new Vector3(
EditorPrefs.GetFloat(Key, DefaultValue.x),
EditorPrefs.GetFloat(KeyY, DefaultValue.y),
EditorPrefs.GetFloat(KeyZ, DefaultValue.z));
/// <summary>Saves the value of this pref to <see cref="EditorPrefs"/>.</summary>
protected override void Save()
{
EditorPrefs.SetFloat(Key, Value.x);
EditorPrefs.SetFloat(KeyY, Value.y);
EditorPrefs.SetFloat(KeyZ, Value.z);
}
/************************************************************************************************************************/
/// <summary>Returns true if <see cref="EditorPrefs"/> currently contains a value for this pref.</summary>
public override bool IsSaved()
=> EditorPrefs.HasKey(Key)
&& EditorPrefs.HasKey(KeyY)
&& EditorPrefs.HasKey(KeyZ);
/// <summary>Deletes the value of this pref from <see cref="EditorPrefs"/> and reverts to the default value.</summary>
public override void DeletePref()
{
EditorPrefs.DeleteKey(Key);
EditorPrefs.DeleteKey(KeyY);
EditorPrefs.DeleteKey(KeyZ);
RevertToDefaultValue();
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EditorVector3"/> pref using the specified string as the key.</summary>
public static implicit operator EditorVector3(string key)
=> new EditorVector3(key);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// An <see cref="InspectorGadgets.AutoPrefs.AutoPref{T}"/> which encapsulates a <see cref="Vector4"/> value stored in
/// <see cref="EditorPrefs"/>.
/// </summary>
public class EditorVector4 : InspectorGadgets.AutoPrefs.Vector4
{
/************************************************************************************************************************/
/// <summary>Constructs an <see cref="EditorVector4"/> pref.</summary>
public EditorVector4(string key,
Vector4 defaultValue = default,
Action<Vector4> onValueChanged = null)
: base(key, defaultValue, onValueChanged)
{ }
/// <summary>Loads the value of this pref from <see cref="EditorPrefs"/>.</summary>
protected override Vector4 Load()
=> new Vector4(
EditorPrefs.GetFloat(Key, DefaultValue.x),
EditorPrefs.GetFloat(KeyY, DefaultValue.y),
EditorPrefs.GetFloat(KeyZ, DefaultValue.z),
EditorPrefs.GetFloat(KeyW, DefaultValue.w));
/// <summary>Saves the value of this pref to <see cref="EditorPrefs"/>.</summary>
protected override void Save()
{
EditorPrefs.SetFloat(Key, Value.x);
EditorPrefs.SetFloat(KeyY, Value.y);
EditorPrefs.SetFloat(KeyZ, Value.z);
EditorPrefs.SetFloat(KeyW, Value.w);
}
/************************************************************************************************************************/
/// <summary>Returns true if <see cref="EditorPrefs"/> currently contains a value for this pref.</summary>
public override bool IsSaved()
=> EditorPrefs.HasKey(Key)
&& EditorPrefs.HasKey(KeyY)
&& EditorPrefs.HasKey(KeyZ)
&& EditorPrefs.HasKey(KeyW);
/// <summary>Deletes the value of this pref from <see cref="EditorPrefs"/> and reverts to the default value.</summary>
public override void DeletePref()
{
EditorPrefs.DeleteKey(Key);
EditorPrefs.DeleteKey(KeyY);
EditorPrefs.DeleteKey(KeyZ);
EditorPrefs.DeleteKey(KeyW);
RevertToDefaultValue();
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EditorVector4"/> pref using the specified string as the key.</summary>
public static implicit operator EditorVector4(string key)
=> new EditorVector4(key);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// An <see cref="InspectorGadgets.AutoPrefs.AutoPref{T}"/> which encapsulates a <see cref="Quaternion"/> value stored in
/// <see cref="EditorPrefs"/>.
/// </summary>
public class EditorQuaternion : InspectorGadgets.AutoPrefs.Quaternion
{
/************************************************************************************************************************/
/// <summary>Constructs a <see cref="Quaternion"/> pref with the specified `key` and `defaultValue`.</summary>
public EditorQuaternion(string key,
Quaternion defaultValue = default,
Action<Quaternion> onValueChanged = null)
: base(key, defaultValue, onValueChanged)
{ }
/// <summary>Constructs a <see cref="Quaternion"/> pref with the specified `key` and <see cref="Quaternion.identity"/> as the default value.</summary>
public EditorQuaternion(string key)
: base(key, Quaternion.identity)
{ }
/// <summary>Loads the value of this pref from <see cref="EditorPrefs"/>.</summary>
protected override Quaternion Load()
=> new Quaternion(
EditorPrefs.GetFloat(Key, DefaultValue.x),
EditorPrefs.GetFloat(KeyY, DefaultValue.y),
EditorPrefs.GetFloat(KeyZ, DefaultValue.z),
EditorPrefs.GetFloat(KeyW, DefaultValue.w));
/// <summary>Saves the value of this pref to <see cref="EditorPrefs"/>.</summary>
protected override void Save()
{
EditorPrefs.SetFloat(Key, Value.x);
EditorPrefs.SetFloat(KeyY, Value.y);
EditorPrefs.SetFloat(KeyZ, Value.z);
EditorPrefs.SetFloat(KeyW, Value.w);
}
/************************************************************************************************************************/
/// <summary>Returns true if <see cref="EditorPrefs"/> currently contains a value for this pref.</summary>
public override bool IsSaved()
=> EditorPrefs.HasKey(Key)
&& EditorPrefs.HasKey(KeyY)
&& EditorPrefs.HasKey(KeyZ)
&& EditorPrefs.HasKey(KeyW);
/// <summary>Deletes the value of this pref from <see cref="EditorPrefs"/> and reverts to the default value.</summary>
public override void DeletePref()
{
EditorPrefs.DeleteKey(Key);
EditorPrefs.DeleteKey(KeyY);
EditorPrefs.DeleteKey(KeyZ);
EditorPrefs.DeleteKey(KeyW);
RevertToDefaultValue();
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EditorQuaternion"/> pref using the specified string as the key.</summary>
public static implicit operator EditorQuaternion(string key)
=> new EditorQuaternion(key);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 75276622ae7b5bc468c8b6fa41114acc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,111 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace InspectorGadgets.Editor
{
/// <summary>[Pro-Only] [Editor-Only]
/// <see cref="MethodCache{TDelegate}"/>s for custom event messages added by Inspector Gadgets.
/// </summary>
public static class MethodCache
{
/************************************************************************************************************************/
public static readonly MethodCache<Action> AfterInspectorGUI = nameof(AfterInspectorGUI);
public static readonly MethodCache<Action> OnInspectorGUI = nameof(OnInspectorGUI);
public delegate bool OverridePropertyGUIMethod(UnityEditor.SerializedProperty property, GUIContent label);
public static readonly MethodCache<OverridePropertyGUIMethod> OverridePropertyGUI = nameof(OverridePropertyGUI);
public delegate void OnPropertyContextMenuMethod(UnityEditor.GenericMenu menu, UnityEditor.SerializedProperty property);
public static readonly MethodCache<OnPropertyContextMenuMethod> OnPropertyContextMenu = nameof(OnPropertyContextMenu);
/************************************************************************************************************************/
}
/// <summary>[Pro-Only] [Editor-Only]
/// Retrieves and caches methods that match a specified signature.
/// </summary>
public sealed class MethodCache<TDelegate> where TDelegate : class
{
/************************************************************************************************************************/
public readonly string Name;
public readonly Type ReturnType;
public readonly Type[] ParameterTypes;
private readonly Dictionary<Type, MethodInfo>
TypeToMethod = new Dictionary<Type, MethodInfo>();
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="MethodCache{TDelegate}"/> targeting the specified method name.
/// </summary>
public MethodCache(string name)
{
Name = name;
var invoke = typeof(TDelegate).GetMethod("Invoke");
ReturnType = invoke.ReturnType;
var parameters = invoke.GetParameters();
ParameterTypes = new Type[parameters.Length];
for (int i = 0; i < ParameterTypes.Length; i++)
{
ParameterTypes[i] = parameters[i].ParameterType;
}
}
/// <summary>
/// Returns a new <see cref="MethodCache{TDelegate}"/> targeting the specified method name.
/// </summary>
public static implicit operator MethodCache<TDelegate>(string name)
=> new MethodCache<TDelegate>(name);
/************************************************************************************************************************/
/// <summary>Returns a delegate which calls the cached method on the `target`.</summary>
public TDelegate GetDelegate(object target)
{
if (target == null)
return null;
var method = GetMethod(target.GetType());
if (method == null)
return null;
return Delegate.CreateDelegate(typeof(TDelegate), target, method) as TDelegate;
}
/************************************************************************************************************************/
private MethodInfo GetMethod(Type type)
{
if (!TypeToMethod.TryGetValue(type, out var method))
{
method = type.GetMethod(Name, IGEditorUtils.AnyAccessBindings, null, ParameterTypes, null);
if (method != null && method.ReturnType != ReturnType)
method = null;
if (method == null && type.BaseType != null)
method = GetMethod(type.BaseType);
TypeToMethod.Add(type, method);
}
return method;
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 27fc1ac1a0fbeeb4686327ea5d5a590e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,79 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only] Flags for a combination of Alt, Ctrl, and Shift.</summary>
[Flags]
public enum ModifierKey
{
None = 0,
Alt = 1 << 0,
Ctrl = 1 << 1,
Shift = 1 << 2,
Disabled = 1 << 3,
}
/// <summary>[Editor-Only] An <see cref="AutoPrefs.EditorInt"/> which wraps <see cref="ModifierKey"/> flags.</summary>
public class ModifierKeysPref : AutoPrefs.EditorInt
{
/************************************************************************************************************************/
/// <inheritdoc/>
public ModifierKeysPref(
string key,
ModifierKey defaultValue = ModifierKey.None,
Action<int> onValueChanged = null)
: base(key, (int)defaultValue, onValueChanged)
{ }
/************************************************************************************************************************/
/// <summary>The current value of this pref.</summary>
public new ModifierKey Value
{
get => (ModifierKey)base.Value;
set => base.Value = (int)value;
}
/************************************************************************************************************************/
/// <summary>Does the <see cref="Value"/> contain the `modifier`?</summary>
public bool ValueContains(ModifierKey modifier)
=> (Value & modifier) == modifier;
/************************************************************************************************************************/
/// <summary>Are all modifiers in the <see cref="Value"/> currently being held down?</summary>
public bool AreKeysCurrentlyDown
=> AreKeysDown(Event.current);
/// <summary>Are all modifiers in the <see cref="Value"/> being held down in the `currentEvent`?</summary>
public bool AreKeysDown(Event currentEvent)
=> !ValueContains(ModifierKey.Disabled)
&& (!ValueContains(ModifierKey.Alt) || currentEvent.alt)
&& (!ValueContains(ModifierKey.Ctrl) || currentEvent.control)
&& (!ValueContains(ModifierKey.Shift) || currentEvent.shift);
/************************************************************************************************************************/
public bool DoGUI(GUIContent label)
=> this.DoGUI(label, (area, content, value, style)
=> (int)(ModifierKey)EditorGUI.EnumFlagsField(area, content, (ModifierKey)value));
/************************************************************************************************************************/
/// <summary>Returns the current value of this pref.</summary>
public static implicit operator ModifierKey(ModifierKeysPref pref)
=> pref.Value;
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f2e4bb5be20832429d7f7748b019921
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,517 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
using System.Globalization;
using System.Text;
using UnityEngine;
namespace InspectorGadgets
{
/// <summary>A <see cref="Vector4"/> which uses nullable floats.</summary>
public sealed class NullableVector4
{
/************************************************************************************************************************/
/// <summary>The X component of this vector.</summary>
public float? x;
/// <summary>The Y component of this vector.</summary>
public float? y;
/// <summary>The Z component of this vector.</summary>
public float? z;
/// <summary>The W component of this vector.</summary>
public float? w;
/************************************************************************************************************************/
/// <summary>The component of this vector at the specified index: 0 = x, 1 = y, 2 = z, 3 = w.</summary>
public float? this[int i]
{
get
{
switch (i)
{
case 0: return x;
case 1: return y;
case 2: return z;
case 3: return w;
default: return null;
}
}
set
{
switch (i)
{
case 0: x = value; break;
case 1: y = value; break;
case 2: z = value; break;
case 3: w = value; break;
default: throw new System.ArgumentOutOfRangeException(nameof(i));
}
}
}
/************************************************************************************************************************/
/// <summary>Returns true if any of the components of this vector are null.</summary>
public bool AnyNull()
{
return x == null || y == null || z == null || w == null;
}
/// <summary>Returns true if any of the components of this vector are null, ignoring components after `componentCount`.</summary>
public bool AnyNull(int componentCount)
{
if (componentCount >= 1)
{
if (x == null) return true;
if (componentCount >= 2)
{
if (y == null) return true;
if (componentCount >= 3)
{
if (z == null) return true;
if (componentCount >= 4)
{
if (w == null) return true;
}
}
}
}
return false;
}
/// <summary>Returns true if all of the components of this vector are null.</summary>
public bool AllNull()
{
return x == null && y == null && z == null && w == null;
}
/// <summary>Returns true if all of the components of this vector are null, ignoring components after `componentCount`.</summary>
public bool AllNull(int componentCount)
{
if (componentCount >= 1)
{
if (x != null) return false;
if (componentCount >= 2)
{
if (y != null) return false;
if (componentCount >= 3)
{
if (z != null) return false;
if (componentCount >= 4)
{
if (w != null) return false;
}
}
}
}
return true;
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="NullableVector4"/> with all components null.</summary>
public NullableVector4() { }
/// <summary>Creates a new <see cref="NullableVector4"/> with the specified components.</summary>
public NullableVector4(float? x = null, float? y = null, float? z = null, float? w = null)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
/// <summary>Creates a new <see cref="NullableVector4"/> with each of its components set the same as the specified `value`.</summary>
public NullableVector4(Vector2 value)
{
x = value.x;
y = value.y;
}
/// <summary>Creates a new <see cref="NullableVector4"/> with each of its components set the same as the specified `value`.</summary>
public NullableVector4(Vector3 value)
{
x = value.x;
y = value.y;
z = value.z;
}
/// <summary>Creates a new <see cref="NullableVector4"/> with each of its components set the same as the specified `value`.</summary>
public NullableVector4(Vector4 value)
{
x = value.x;
y = value.y;
z = value.z;
w = value.w;
}
/// <summary>Creates a new <see cref="NullableVector4"/> with each of its components set the same as the specified `value`.</summary>
public NullableVector4(NullableVector4 value)
{
x = value.x;
y = value.y;
z = value.z;
w = value.w;
}
/// <summary>Creates a new <see cref="NullableVector4"/> using the first 4 elements of the specified array as its components.</summary>
public NullableVector4(float?[] values)
{
if (values == null)
return;
if (values.Length >= 1)
{
x = values[0];
if (values.Length >= 2)
{
y = values[1];
if (values.Length >= 3)
{
z = values[2];
if (values.Length >= 4)
{
w = values[3];
}
}
}
}
}
/************************************************************************************************************************/
/// <summary>Sets all components of this vector to null.</summary>
public void SetAllNull()
{
x = y = z = w = null;
}
/************************************************************************************************************************/
/// <summary>Sets all components of this vector which are null to 0.</summary>
public void ZeroAllNulls()
{
if (x == null) x = 0;
if (y == null) y = 0;
if (z == null) z = 0;
if (w == null) w = 0;
}
/************************************************************************************************************************/
/// <summary>Sets each of the components of this vector to be the same as the specified `value`.</summary>
public void CopyFrom(NullableVector4 value)
{
x = value.x;
y = value.y;
z = value.z;
w = value.w;
}
/// <summary>Sets each of the components of this vector to be the same as the specified `value`.</summary>
public void CopyFrom(Vector2 value)
{
x = value.x;
y = value.y;
}
/// <summary>Sets each of the components of this vector to be the same as the specified `value`.</summary>
public void CopyFrom(Vector3 value)
{
x = value.x;
y = value.y;
z = value.z;
}
/// <summary>Sets each of the components of this vector to be the same as the specified `value`.</summary>
public void CopyFrom(Vector4 value)
{
x = value.x;
y = value.y;
z = value.z;
w = value.w;
}
/************************************************************************************************************************/
/// <summary>
/// Returns a new <see cref="Vector2"/> using components of this vector.
/// Any components which are null will be set to 0.
/// </summary>
public Vector2 ToVector2()
{
return new Vector2(
x != null ? x.Value : 0,
y != null ? y.Value : 0);
}
/// <summary>
/// Returns a new <see cref="Vector3"/> using components of this vector.
/// Any components which are null will be set to 0.
/// </summary>
public Vector3 ToVector3()
{
return new Vector3(
x != null ? x.Value : 0,
y != null ? y.Value : 0,
z != null ? z.Value : 0);
}
/// <summary>
/// Returns a new <see cref="Vector3"/> using components of this vector.
/// Any components which are null will be set to 0.
/// </summary>
public Vector4 ToVector4()
{
return new Vector4(
x != null ? x.Value : 0,
y != null ? y.Value : 0,
z != null ? z.Value : 0,
w != null ? w.Value : 0);
}
/************************************************************************************************************************/
/// <summary>
/// Returns a new <see cref="Vector3"/> using components of this vector.
/// Any components which are null will instead use the corresponding component of the specified `fallback`.
/// </summary>
public Vector2 ToVector3(Vector2 fallback)
{
if (x != null) fallback.x = x.Value;
if (y != null) fallback.y = y.Value;
return fallback;
}
/// <summary>
/// Returns a new <see cref="Vector3"/> using components of this vector.
/// Any components which are null will instead use the corresponding component of the specified `fallback`.
/// </summary>
public Vector3 ToVector3(Vector3 fallback)
{
if (x != null) fallback.x = x.Value;
if (y != null) fallback.y = y.Value;
if (z != null) fallback.z = z.Value;
return fallback;
}
/// <summary>
/// Returns a new <see cref="Vector4"/> using components of this vector.
/// Any components which are null will instead use the corresponding component of the specified `fallback`.
/// </summary>
public Vector4 ToVector4(Vector4 fallback)
{
if (x != null) fallback.x = x.Value;
if (y != null) fallback.y = y.Value;
if (z != null) fallback.z = z.Value;
if (w != null) fallback.w = w.Value;
return fallback;
}
/************************************************************************************************************************/
/// <summary>Returns a nicely formatted string for this vector using '-' to denote nulls.</summary>
public string ToString(int componentCount)
{
if (componentCount <= 0)
throw new System.ArgumentException(nameof(componentCount));
var str = new StringBuilder();
str.Append('(');
str.Append(x.ToDisplayString());
if (componentCount >= 2)
{
str.Append(", ");
str.Append(y.ToDisplayString());
if (componentCount >= 3)
{
str.Append(", ");
str.Append(z.ToDisplayString());
if (componentCount >= 4)
{
str.Append(", ");
str.Append(w.ToDisplayString());
}
}
}
str.Append(')');
return str.ToString();
}
/// <summary>Returns a nicely formatted string for this vector using '-' to denote nulls.</summary>
public override string ToString()
=> ToString(4);
/************************************************************************************************************************/
/// <summary>
/// Attempts to parse a series of floats from the given string and returns the index of the last successfully parsed value.
/// </summary>
public static int TryParse(string value, int componentCount, out NullableVector4 results)
{
results = new NullableVector4();
if (value == null)
return 0;
var start = value.IndexOf('(') + 1;
int end;
var componentsParsed = -1;
for (int i = 0; i < componentCount; i++)
{
end = value.IndexOf(',', start);
if (end < 0)
{
end = value.IndexOf(')', start);
if (end < 0)
end = value.Length;
}
var digitStart = start;
var digitEnd = end;
// Trim any non-digit prefix as long as it doesn't start with a '.'.
var separator = CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator;
while (digitStart < digitEnd)
{
if (digitStart + separator.Length < digitEnd &&
string.Compare(value, digitStart, separator, 0, separator.Length) == 0)
break;
var c = value[digitStart];
if (c == '-' || char.IsDigit(c))
break;
else
digitStart++;
}
// Trim any non-digit suffix.
while (digitEnd > digitStart)
{
var c = value[digitEnd - 1];
if (char.IsDigit(c))
break;
else
digitEnd--;
}
var substring = value.Substring(digitStart, digitEnd - digitStart);
if (float.TryParse(substring, NumberStyles.Float, CultureInfo.InvariantCulture, out var floatValue))
{
results[i] = floatValue;
componentsParsed = i;
}
start = end + 1;
if (start >= value.Length)
{
i++;
break;
}
}
return componentsParsed;
}
/************************************************************************************************************************/
#region Operators
/************************************************************************************************************************/
/// <summary>Returns true if all of the components of `a` and `b` are equal.</summary>
public static bool operator ==(NullableVector4 a, NullableVector4 b)
{
if (a is null) return b is null;
if (b is null) return false;
if (a.x != b.x) return false;
if (a.y != b.y) return false;
if (a.z != b.z) return false;
if (a.w != b.w) return false;
return true;
}
/// <summary>Returns true if any of the components of `a` and `b` are not equal.</summary>
public static bool operator !=(NullableVector4 a, NullableVector4 b)
=> !(a == b);
/************************************************************************************************************************/
/// <summary>Returns true if all of the components of `this` and `other` are equal, ignoring components after `componentCount`.</summary>
public bool Equals(NullableVector4 other, int componentCount)
{
if (other == null)
return false;
if (componentCount >= 1)
{
if (x != other.x)
return false;
if (componentCount >= 2)
{
if (y != other.y)
return false;
if (componentCount >= 3)
{
if (z != other.z)
return false;
if (componentCount >= 4)
{
if (w != other.w)
return false;
}
}
}
}
return true;
}
/************************************************************************************************************************/
/// <summary>Returns true if all of the components of `a` and `b` are equal.</summary>
public override bool Equals(object obj)
{
if (obj == null)
return false;
var vector = obj as NullableVector4;
if (vector != null)
return this == vector;
return false;
}
/// <summary>Uses the base method.</summary>
public override int GetHashCode() => base.GetHashCode();
/************************************************************************************************************************/
/// <summary>Returns a new <see cref="NullableVector4"/> with each of its components set the same as the specified `vector`.</summary>
public static implicit operator NullableVector4(Vector2 vector) => new NullableVector4(vector);
/// <summary>Returns a new <see cref="NullableVector4"/> with each of its components set the same as the specified `vector`.</summary>
public static implicit operator NullableVector4(Vector3 vector) => new NullableVector4(vector);
/// <summary>Returns a new <see cref="NullableVector4"/> with each of its components set the same as the specified `vector`.</summary>
public static implicit operator NullableVector4(Vector4 vector) => new NullableVector4(vector);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a63998aedaf1f6a47b1ccbe44970d59f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,329 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using InspectorGadgets.Editor.PropertyDrawers;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only] GUI for Inspector Gadgets settings.</summary>
/// <remarks>These settings are accessible via <c>Edit/Preferences/Inspector Gadgets</c>.</remarks>
public static class Preferences
{
/************************************************************************************************************************/
private const string
TransformInspector = "Transform Inspector",
SceneTools = "Scene Tools",
ScriptInspector = "Script Inspector",
AssetDragAndDrop = "Asset Drag and Drop";
/************************************************************************************************************************/
private static SettingsProvider _SettingsProvider;
/// <summary>Creates this preferences page.</summary>
[SettingsProvider]
public static SettingsProvider CreateMyCustomSettingsProvider()
{
if (_SettingsProvider == null)
{
var keywords = new HashSet<string>
{
TransformInspector,
SceneTools,
ScriptInspector,
AssetDragAndDrop,
AutoHideUI.Preferences.Headding,
EnhancedHierarchy.Preferences.Headding,
};
GatherGUIContentText(typeof(Preferences), keywords);
GatherGUIContentText(typeof(AutoHideUI.Preferences), keywords);
GatherGUIContentText(typeof(EnhancedHierarchy.Preferences), keywords);
_SettingsProvider = new SettingsProvider("Preferences/Inspector Gadgets", SettingsScope.User)
{
label = "Inspector Gadgets",
guiHandler = searchContext => DoGUI(),
keywords = keywords,
};
}
return _SettingsProvider;
}
/************************************************************************************************************************/
private static void GatherGUIContentText(Type type, HashSet<string> set)
{
var fields = type.GetFields(IGEditorUtils.StaticBindings);
for (int i = 0; i < fields.Length; i++)
{
var field = fields[i];
if (field.FieldType != typeof(GUIContent))
continue;
var value = (GUIContent)field.GetValue(null);
if (value != null)
set.Add(value.text);
}
}
/************************************************************************************************************************/
/// <summary>Callback for drawing the <c>SceneViewObjectPickerWindow</c> preferences.</summary>
/// <remarks>This allows that class to be defined outside the Inspector Gadgets Lite DLL.</remarks>
public static event Action DoSceneViewObjectPickerWindowGUI;
private static void DoGUI()
{
DoHeaderGUI();
var labelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = GUILayoutUtility.GetLastRect().width * 0.5f;
DoTransformInspectorGUI();
DoSceneToolsGUI();
DoScriptInspectorGUI();
AutoHideUI.Preferences.DoGUI();
DoSceneViewObjectPickerWindowGUI?.Invoke();
EnhancedHierarchy.Preferences.DoGUI();
DoAssetDragAndDropGUI();
GUI.enabled = true;// Still let the scroll bar catch scroll wheel events.
EditorGUIUtility.labelWidth = labelWidth;
}
/************************************************************************************************************************/
private static void DoHeaderGUI()
{
var headding = "Inspector Gadgets Pro v" + Strings.InspectorGadgetsVersion;
GUILayout.Label(headding, EditorStyles.boldLabel);
if (GUILayout.Button($"Open Documentation: {Strings.DocumentationURL}"))
IGEditorUtils.OpenDocumentation();
if (GUILayout.Button($"Contact: {Strings.DeveloperEmail}"))
{
GUIUtility.systemCopyBuffer = Strings.DeveloperEmail;
Debug.Log($"Copied '{Strings.DeveloperEmail}' to the clipboard");
EditorUtility.OpenWithDefaultApp($"mailto:{Strings.DeveloperEmail}?subject=InspectorGadgets");
}
if (GUILayout.Button($"Change Log"))
IGEditorUtils.OpenDocumentation("/docs/changes");
}
/************************************************************************************************************************/
private static readonly GUIContent
ShowCopyButton = new GUIContent("Show Copy Button",
"Should the Transform Inspector show the [C] button to copy a transform property to an internal clipboard?"),
ShowPasteButton = new GUIContent("Show Paste Button",
"Should the Transform Inspector show the [P] button to paste a transform property from the internal clipboard?"),
ShowSnapButton = new GUIContent("Show Snap Button",
"Should the Transform Inspector show the [S] button to snap a transform property to the nearest snap increment specified in Edit/Snap Setings?"),
ShowResetButton = new GUIContent("Show Reset Button",
"Should the Transform Inspector show the [R] button to reset a transform property to its default value?"),
DisableUselessButtons = new GUIContent("Disable Useless Buttons",
"Should the above buttons be greyed out when they would do nothing?"),
UseFieldColors = new GUIContent("Use Field Colors",
"Should the X/Y/Z fields be colored Red/Green/Blue respectively?"),
FieldPrimaryColor = new GUIContent("Field Primary Color",
"The strength of the main color for each axis."),
FieldSecondaryColor = new GUIContent("Field Secondary Color",
"The strength of the other colors."),
ShowApproximationsLabel = new GUIContent("Show Approximations",
"Should the fields show approximations if the value is too long for the GUI?"),
ItaliciseNonSnappedFields = new GUIContent("Italicise Non-Snapped Fields",
"Should Transform fields which aren't a multiple of the snap increment specified in Edit/Snap Setings use italic text?"),
ShrinkScientificFields = new GUIContent("Shrink Scientific Fields",
"Should the font size be decreased for values that are displayed using scientific notation?"),
DefaultToUniformScale = new GUIContent("Default to Uniform Scale",
"Should Transform scale be shown as a single float field by default when the selected object has the same scale on all axes?"),
SnapToGroundDistance = new GUIContent("Snap to Ground Distance",
"The distance within which to check for the ground when using the Snap to Ground function in the Transform Position context menu."),
SnapToGroundLayers = new GUIContent("Snap to Ground Layers",
"This layer mask determines which physics layers are treated as ground for the Transform Position context menu.");
private static void DoTransformInspectorGUI()
{
DoSectionHeader(TransformInspector);
var enabled = GUI.enabled;
TransformPropertyDrawer.ShowCopyButton.DoGUI(ShowCopyButton);
TransformPropertyDrawer.ShowPasteButton.DoGUI(ShowPasteButton);
TransformPropertyDrawer.ShowSnapButton.DoGUI(ShowSnapButton);
TransformPropertyDrawer.ShowResetButton.DoGUI(ShowResetButton);
TransformPropertyDrawer.DisableUselessButtons.DoGUI(DisableUselessButtons);
TransformPropertyDrawer.UseFieldColors.DoGUI(UseFieldColors);
if (!TransformPropertyDrawer.UseFieldColors)
GUI.enabled = false;
TransformPropertyDrawer.FieldPrimaryColor.DoGUI(FieldPrimaryColor, GUI.skin.horizontalSlider,
(area, label, value, style) =>
{
return EditorGUI.Slider(area, label, value, 0, 1);
});
TransformPropertyDrawer.FieldSecondaryColor.DoGUI(FieldSecondaryColor, GUI.skin.horizontalSlider,
(area, label, value, style) =>
{
return EditorGUI.Slider(area, label, value, 0, 1);
});
TransformPropertyDrawer.ShowApproximations.DoGUI(ShowApproximationsLabel);
TransformPropertyDrawer.ItaliciseNonSnappedFields.DoGUI(ItaliciseNonSnappedFields);
GUI.enabled = enabled;
TransformEditor.DefaultToUniformScale.DoGUI(DefaultToUniformScale);
if (PositionDrawer.SnapToGroundDistance.DoGUI(SnapToGroundDistance))
PositionDrawer.SnapToGroundDistance.Value = Mathf.Max(PositionDrawer.SnapToGroundDistance, 0);
PositionDrawer.SnapToGroundLayers.DoGUI(SnapToGroundLayers,
(position, label, value, style) => IGEditorUtils.DoLayerMaskField(position, label, value));
}
/************************************************************************************************************************/
private static readonly GUIContent
OverrideTransformGizmos = new GUIContent("Override Transform Gizmos",
"Should the default scene gizmos be overwritten in order to implement various features like \"Freeze child transforms\" and \"Draw gizmos for all selected objects\"?"),
ShowMovementGuides = new GUIContent("Show Movement Guides",
"Should the scene view movement tool show some extra lines while you are moving an object to indicate where you are moving it from?"),
ShowMovementDistance = new GUIContent("Show Movement Distance",
"Should moving an object display the distance from the old position?"),
ShowMovementDistancePerAxis = new GUIContent("Show Movement Distance Per Axis",
"Should the distance moved on each individual axis also be displayed?"),
SceneLabelBackgroundColor = new GUIContent("Scene Label Background Color",
"The color to use behind scene view labels to make them easier to read."),
ShowPositionLabels = new GUIContent("Show Position Labels",
"Should the scene view show the selected object's position around the Move tool?");
private static void DoSceneToolsGUI()
{
DoSectionHeader(SceneTools);
TransformEditor.OverrideTransformGizmos.DoGUI(OverrideTransformGizmos);
if (!TransformEditor.OverrideTransformGizmos)
{
EditorGUILayout.HelpBox(
"With this disabled, features like 'Freeze child transforms' and" +
" 'Draw gizmos for all selected objects' won't work.",
MessageType.Warning);
Tools.hidden = false;
GUI.enabled = false;
}
CustomTransformGizmo.DoPreferencesGUI();
PositionDrawer.ShowMovementGuides.DoGUI(ShowMovementGuides);
PositionDrawer.ShowMovementDistance.DoGUI(ShowMovementDistance);
var enabled = GUI.enabled;
if (!PositionDrawer.ShowMovementDistance)
GUI.enabled = false;
PositionDrawer.ShowMovementDistancePerAxis.DoGUI(ShowMovementDistancePerAxis);
GUI.enabled = enabled;
InternalGUI.SceneLabelBackgroundColor.DoColorGUI(SceneLabelBackgroundColor);
PositionDrawer.ShowPositionLabels.DoGUI(ShowPositionLabels);
}
/************************************************************************************************************************/
private static readonly GUIContent
HideScriptProperty = new GUIContent("Hide Script Property",
"Should the \"Script\" property at the top of each MonoBehaviour inspector be hidden to save space?"),
AutoGatherRequiredComponents = new GUIContent("Auto Gather Required Components",
"Should selecting an object in the editor automatically gather references for any of its component fields with a [RequireAssignment] attribute?" +
"\n\nGathering is conducted using InspectorGadgetsUtils.GetComponentInHierarchy which finds the most appropriately named component in the selected object's children or parents."),
AutoGatherSerializedComponents = new GUIContent("Auto Gather Serialized Components",
"Should selecting an object in the editor automatically gather references for any of its component fields which are public or have [SerializeField] attribute?" +
"\n\nGathering is conducted using InspectorGadgetsUtils.GetComponentInHierarchy which finds the most appropriately named component in the selected object's children or parents."),
ItaliciseSelfReferences = new GUIContent("Italicise Self References",
"Should Object reference fields be drawn in italics when referencing another component on the same GameObject?"),
AlwaysShowGet = new GUIContent("Always Show Get",
"� If enabled, the Get button will be shown on Object reference fields at all times." +
"\n� If disabled, it will only be shown when the field is empty or you're holding Alt."),
DefaultEditorState = new GUIContent("Default Editor State",
"When to show Inspectables if not specified in their constructor"),
ObjectEditorNestLimit = new GUIContent("Object Editor Nest Limit",
"If higher than 0, Object fields will be drawn with a foldout arrow to draw the target object's inspector nested inside the current one.");
private static void DoScriptInspectorGUI()
{
DoSectionHeader(ScriptInspector);
ComponentEditor.HideScriptProperty.DoGUI(HideScriptProperty);
ObjectDrawer.ItaliciseSelfReferences.DoGUI(ItaliciseSelfReferences);
ObjectDrawer.AlwaysShowGet.DoGUI(AlwaysShowGet);
ObjectDrawer.ObjectEditorNestLimit.DoGUI(ObjectEditorNestLimit,
(position, label, value, style) => EditorGUI.IntSlider(position, label, value, 0, 10));
EditorGUI.BeginChangeCheck();
var defaultEditorState = EditorGUILayout.EnumPopup(DefaultEditorState, IGEditorUtils.DefaultEditorState);
if (EditorGUI.EndChangeCheck())
IGEditorUtils.DefaultEditorState = (EditorState)defaultEditorState;
if (GUILayout.Button("Find and Fix Missing Scripts"))
EditorWindow.GetWindow<MissingScriptWindow>();
}
/************************************************************************************************************************/
private static readonly GUIContent
DragAndDropSubAssetsLabel = new GUIContent("Drag and Drop Sub-Assets",
"Holding these keys while you Drag and Drop one Asset onto another will move the dragged asset" +
" into the target as a Sub-Asset."),
AdvancedDragAndDropLabel = new GUIContent("Advanced Drag and Drop Assets",
"Holding these keys while you Drag and Drop Assets or External Files will open a window with options" +
" to choose what you want to do with the dropped files." +
"\n\nNote that you must drop onto an Asset. Dropping onto empty space will not work.");
private static void DoAssetDragAndDropGUI()
{
DoSectionHeader(AssetDragAndDrop);
DragAndDropSubAssets.Modifiers.DoGUI(DragAndDropSubAssetsLabel);
AdvancedDragAndDrop.Modifiers.DoGUI(AdvancedDragAndDropLabel);
}
/************************************************************************************************************************/
/// <summary>Enables the GUI and draws a space and label.</summary>
public static void DoSectionHeader(string text)
{
GUI.enabled = true;
EditorGUILayout.Space();
GUILayout.Label(text, EditorStyles.boldLabel);
}
/************************************************************************************************************************/
/// <summary>Draws a pref which enables the following GUI group and which is Pro-Only.</summary>
public static void DoProOnlyGroupEnabledPref(InspectorGadgets.AutoPrefs.Bool pref, GUIContent label)
{
pref.DoGUI(label);
GUI.enabled = pref;
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1724b3bc505d77a468e224d2d49912f9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,163 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace InspectorGadgets
{
/// <summary>
/// A static stack that creates new elements as needed but keeps and reuses them instead of actually adding and
/// removing like a regular stack.
/// </summary>
public abstract class StaticLazyStack<T> where T : StaticLazyStack<T>, new()
{
/************************************************************************************************************************/
/// <summary>
/// The elements managed by this class.
/// <para></para>
/// Note that the number of active elements is stored in <see cref="StackHeight"/> rather than the count of
/// this list.
/// </summary>
/// <remarks>
/// Not an actual Stack because that class is in System.dll which is not otherwise needed.
/// </remarks>
protected static readonly List<T> Stack = new List<T>();
/// <summary>
/// The number of elements currently active in the stack.
/// </summary>
public static int StackHeight { get; private set; }
/************************************************************************************************************************/
/// <summary>
/// The maximum number of elements that can be in the stack at a time. Default = 10.
/// </summary>
public static int MaxHeight { get; set; }
static StaticLazyStack()
{
MaxHeight = 10;
}
/************************************************************************************************************************/
/// <summary>
/// Increases the <see cref="StackHeight"/> by 1, ensures that an element exists at the last index and returns it.
/// </summary>
public static T Increment()
{
T item;
if (StackHeight < Stack.Count)
{
item = Stack[StackHeight++];
}
else
{
item = new T();
Stack.Add(item);
StackHeight++;
if (StackHeight > MaxHeight)
Debug.LogWarning($"StaticLazyStack<{typeof(T).Name}>.StackHeight" +
$" has exceeded the specified maximum of {MaxHeight}." +
$" Please ensure that you Remove as many items as you Add and increase the {nameof(MaxHeight)} if necessary.");
}
item.OnIncrement();
return item;
}
/// <summary>Called by <see cref="Increment"/>.</summary>
protected virtual void OnIncrement() { }
/************************************************************************************************************************/
/// <summary>Decreases the <see cref="StackHeight"/> by 1.</summary>
public static void Decrement()
=> StackHeight--;
/************************************************************************************************************************/
/// <summary>Gets the element currently at the top of the stack.</summary>
public static T GetCurrent()
=> StackHeight > 0 ? Stack[StackHeight - 1] : null;
/************************************************************************************************************************/
}
/// <summary>
/// A <see cref="StaticLazyStack{T}"/> which implements <see cref="IDisposable.Dispose"/> to automatically call
/// <see cref="StaticLazyStack{T}.Decrement"/>
/// </summary>
public abstract class DisposableStaticLazyStack<T> : StaticLazyStack<T>, IDisposable where T : StaticLazyStack<T>, new()
{
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="StaticLazyStack{T}.Decrement"/> and can be overridden.
/// </summary>
public virtual void Dispose()
{
Decrement();
}
/************************************************************************************************************************/
}
/// <summary>
/// A <see cref="DisposableStaticLazyStack{T}"/> that gets and stores a particular <typeparamref name="TValue"/>,
/// sets it to a different value, and then reverts it to the previous value when disposed.
/// </summary>
public abstract class SimpleStaticLazyStack<TStack, TValue> : DisposableStaticLazyStack<TStack>
where TStack : SimpleStaticLazyStack<TStack, TValue>, new()
{
/************************************************************************************************************************/
/// <summary>The <see cref="CurrentValue"/> from before this stack element was activated.</summary>
public TValue PreviousValue { get; private set; }
/// <summary>The <typeparamref name="TValue"/> being controlled by this stack.</summary>
public abstract TValue CurrentValue { get; set; }
/************************************************************************************************************************/
/// <summary>
/// Called by <see cref="StaticLazyStack{T}.Increment"/>.
/// Stores the <see cref="CurrentValue"/> in <see cref="PreviousValue"/>.
/// </summary>
protected override void OnIncrement()
{
base.OnIncrement();
PreviousValue = CurrentValue;
}
/// <summary>
/// Calls <see cref="StaticLazyStack{T}.Decrement"/> and reverts the <see cref="CurrentValue"/> to the
/// <see cref="PreviousValue"/>.
/// </summary>
public override void Dispose()
{
base.Dispose();
CurrentValue = PreviousValue;
}
/************************************************************************************************************************/
/// <summary>
/// Increments the stack, sets the <see cref="CurrentValue"/>, and returns the top element of the stack.
/// </summary>
public static TStack Get(TValue value)
{
var context = Increment();
context.CurrentValue = value;
return context;
}
/************************************************************************************************************************/
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4e655873a96047141a0df5c5c6874732
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,101 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only] String constants used throughout <see cref="InspectorGadgets"/>.</summary>
public static class EditorStrings
{
/************************************************************************************************************************/
public const string PrefsKeyPrefix = nameof(InspectorGadgets) + ".";
public const string Context = "CONTEXT/";
public const string Alt = "&";
public const string Ctrl = "%";
public const string Shift = "#";
/// <summary>
/// Menu items where the last word begins with an underscore or certain other characters are interpreted as
/// having a keyboard shortcut. So we use the '\b' (backspace) character to prevent it from doing that.
/// </summary>
public const string NegateShortcut = "\b";
// Some of the following constants aren't using nameof statements
// due to referencing issues in InspectorGadgets.Lite.dll.
public const string CommentAssetIncludeInBuild = Context + "CommentAsset/Include in Build";
public const string CommentComponentIncludeInBuild = Context + "CommentComponent/Include in Build";
public const string RectTransformToggleAutoHideUI = Context + nameof(RectTransform) + "/Toggle Auto Hide UI";
public const string UIBehaviourToggleAutoHideUI = Context + "UIBehaviour/Toggle Auto Hide UI";
#if UNITY_UGUI
public const string CanvasToggleAutoHideUI = Context + nameof(Canvas) + "/Toggle Auto Hide UI";
#endif
public const string PingScriptAsset = Context + nameof(MonoBehaviour) + "/Ping Script Asset";
public const string ShowOrHideScriptProperty = Context + nameof(MonoBehaviour) + "/Show or Hide Script Property";
public const string CreateEditorScript = Context + nameof(Component) + "/Create Editor Script";
public const string CopyTransformPath = Context + nameof(Transform) + "/Copy Transform Path";
public const string OpenDocumentation = Context + nameof(Transform) + "/Inspector Gadgets Documentation";
public const string CollapseAllComponents = Context + nameof(Transform) + "/Collapse All Components";
public const string RemoveAllComponents = Context + nameof(Transform) + "/Remove All Components";
public const string PingObject = Context + nameof(Transform) + "/Ping Object in Hierarchy";
public const string NewLockedInspector = "/New Locked Inspector " + Ctrl + Alt + "I";
public const string MainNewLockedInspector = "Edit/Selection" + NewLockedInspector;
public const string ObjectNewLockedInspector = Context + nameof(Object) + NewLockedInspector;
public const string GameObjectResetSelectedTransforms = nameof(GameObject) + "/Reset Selected Transforms " + Ctrl + Alt + "Z";
public const string SnapToGrid = nameof(GameObject) + "/Snap to Grid " + Ctrl + Alt + "X";
public const string Watch = Context + nameof(Object) + "/Watch";
/************************************************************************************************************************/
public const string PersistAfterPlayMode = "Persist After Play Mode";
public const string PersistAfterPlayModeComponent = Context + nameof(Component) + "/" + PersistAfterPlayMode;
/************************************************************************************************************************/
public static class GUI
{
/************************************************************************************************************************/
public const string
LocalWorldTooltip = "[L] Local position/rotation/scale\n[W] World position/rotation/scale",
ScaleModeTooltip = "[=] Uniform scale\n[≠] Non-uniform scale",
ScaleSkewWarning = "An arbitrarily rotated object cannot have its World Scale properly represented by a Vector3" +
" because it may be skewed, so the value may not be accurate.",
PrecisionWarning = "Due to floating-point precision limitations, it is recommended to bring the world coordinates" +
" of the GameObject within a smaller range.";
/************************************************************************************************************************/
public static readonly GUIContent
X = new GUIContent("X"),
Y = new GUIContent("Y"),
Z = new GUIContent("Z"),
LocalMode = new GUIContent("L", LocalWorldTooltip),
WorldMode = new GUIContent("W", LocalWorldTooltip),
FreezeChildTransforms = new GUIContent("F", "Freeze child transforms?"),
DrawAllGizmos = new GUIContent("G", "Draw gizmos for all selected objects?"),
Copy = new GUIContent("C", "Left Click = Copy to clipboard\nRight Click = Log current value"),
Reset = new GUIContent("R", "Reset to Default"),
UniformScale = new GUIContent("≠", ScaleModeTooltip),
NonUniformScale = new GUIContent("=", ScaleModeTooltip);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c1d8e2dc3eb33d64abedbde41a8ee8cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,485 @@
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2025 Kybernetik //
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only] [Pro-Only] Improved GUI for the Hierarchy window.</summary>
public class EnhancedHierarchy
{
/************************************************************************************************************************/
[InitializeOnLoadMethod]
private static void Initialize()
{
EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyWindowItemGUI;
EditorApplication.hierarchyChanged += OnHierarchyChanged;
}
/************************************************************************************************************************/
/// <summary>Called for each object shown in the Hierarchy window.</summary>
private static void OnHierarchyWindowItemGUI(int instanceID, Rect area)
{
if (!IsEnabled)
return;
var item = EditorUtility.InstanceIDToObject(instanceID);
if (item is GameObject gameObject)
OnHierarchyWindowItemGUI(gameObject, area);
}
/************************************************************************************************************************/
private static readonly List<Component> Components = new List<Component>();
private static void OnHierarchyWindowItemGUI(GameObject gameObject, Rect area)
{
Components.Clear();
gameObject.GetComponents(Components);
if (MaxComponentCount >= 1)
DoTransformGUI(Components[0], ref area);
var count = Mathf.Min(Components.Count, MaxComponentCount);
for (int i = 1; i < count; i++)
DoComponentGUI(Components[i], ref area);
DoChildCountGUI(gameObject, ref area);
DoLayerAndTagGUI(gameObject, ref area);
}
/************************************************************************************************************************/
private static void DoTransformGUI(Component component, ref Rect area)
{
var isActive = component.gameObject.activeSelf;
var color = SetDisabledColor(!isActive);
var clicked = DoComponentButtonGUI(component, ref area, component.GetType().Name, out var iconArea);
GUI.color = color;
if (clicked && Modifiers.AreKeysCurrentlyDown)
{
Undo.RegisterCompleteObjectUndo(component.gameObject, "Set Active");
component.gameObject.SetActive(!isActive);
}
CheckDragAndDropOntoTransform(iconArea, component);
}
/************************************************************************************************************************/
private static void CheckDragAndDropOntoTransform(Rect area, Component dropOnto)
{
switch (Event.current.type)
{
case EventType.DragUpdated:
if (TryGetDragAndDropTransform(area, dropOnto, out _))
{
DragAndDrop.visualMode = DragAndDropVisualMode.Move;
Event.current.Use();
}
break;
case EventType.DragPerform:
if (TryGetDragAndDropTransform(area, dropOnto, out var transform))
DropOntoTransform(transform, dropOnto as Transform);
break;
}
}
/************************************************************************************************************************/
private static bool TryGetDragAndDropTransform(Rect area, Component dropOnto, out Transform transform)
{
transform = null;
if (!area.Contains(Event.current.mousePosition))
return false;
var dragging = DragAndDrop.objectReferences;
if (dragging.Length != 1)
return false;
transform = IGUtils.GetTransform(dragging[0]);
return transform != null && transform != dropOnto;
}
/************************************************************************************************************************/
private static void DropOntoTransform(Transform drop, Transform onto)
{
DragAndDrop.AcceptDrag();
Event.current.Use();
var menu = new GenericMenu();
menu.AddDisabledItem(new GUIContent("Drop: " + drop.name));
menu.AddDisabledItem(new GUIContent("Target: " + onto.name));
menu.AddItem(new GUIContent("Reparent Drop under Target"), false, () =>
{
Undo.SetTransformParent(drop, onto, "Reparent");
});
menu.AddItem(new GUIContent("Reparent Drop under Target and Reset Local Transform"), false, () =>
{
Undo.SetTransformParent(drop, onto, "Reparent");
drop.localPosition = Vector3.zero;
drop.localRotation = Quaternion.identity;
drop.localScale = Vector3.one;
});
menu.AddItem(new GUIContent("Move Drop to Target"), false, () =>
{
Undo.RecordObject(drop, "Move");
drop.SetPositionAndRotation(onto.position, onto.rotation);
});
menu.AddItem(new GUIContent("Move Target to Drop"), false, () =>
{
Undo.RecordObject(onto, "Move");
onto.SetPositionAndRotation(drop.position, drop.rotation);
});
menu.ShowAsContext();
}
/************************************************************************************************************************/
private static void DoComponentGUI(Component component, ref Rect area)
{
if (component == null)
return;
var isEnabled = EditorUtility.GetObjectEnabled(component);
var color = SetDisabledColor(isEnabled == 0);
var controlID = GUIUtility.GetControlID(FocusType.Passive);
var clicked = DoComponentButtonGUI(component, ref area, component.GetType().Name, out _);
GUI.color = color;
if (clicked && isEnabled >= 0 && Modifiers.AreKeysCurrentlyDown)
{
Undo.RecordObject(component, "Enable Component");
EditorUtility.SetObjectEnabled(component, isEnabled == 0);
}
}
/************************************************************************************************************************/
private static GUIStyle _IconStyle;
private static bool DoComponentButtonGUI(Component component, ref Rect area, string tooltip, out Rect iconArea)
{
if (_IconStyle == null)
_IconStyle = new GUIStyle();
iconArea = IGEditorUtils.StealFromRight(ref area, area.height);
var content = IGEditorUtils.TempContent(null, tooltip);
content.image = AssetPreview.GetMiniThumbnail(component);
var clicked = GUI.Button(iconArea, content, _IconStyle);
content.image = null;
var currentEvent = Event.current;
if (currentEvent.type == EventType.MouseDrag &&
currentEvent.button == 0 &&
iconArea.Contains(currentEvent.mousePosition))
{
DragAndDrop.PrepareStartDrag();
DragAndDrop.objectReferences = new Object[] { component };
DragAndDrop.StartDrag(component.GetType().Name);
currentEvent.Use();
}
return clicked;
}
/************************************************************************************************************************/
private static Color SetDisabledColor(bool isDisabled)
{
var color = GUI.color;
if (isDisabled)
{
var newColor = color;
newColor.a *= 0.5f;
GUI.color = newColor;
}
return color;
}
/************************************************************************************************************************/
private static void DrawLabelBackground(Rect area)
{
var color = EditorGUIUtility.isProSkin
? new Color(0.3f, 0.3f, 0.3f, 1)
: new Color(0.7f, 0.7f, 0.7f, 1);
EditorGUI.DrawRect(area, color);
}
/************************************************************************************************************************/
#region Layer and Tag
/************************************************************************************************************************/
private static void DoLayerAndTagGUI(GameObject gameObject, ref Rect area)
{
DoTagGUI(gameObject, area, true, false);
var layerWidth = DoLayerGUI(gameObject, area);
var tagWidth = DoTagGUI(gameObject, area, false, true);
area.width -= Mathf.Max(layerWidth, tagWidth);
}
/************************************************************************************************************************/
private static float DoLayerGUI(GameObject gameObject, Rect area)
{
var layer = gameObject.layer;
if (((1 << layer) & ShowLayers) == 0)
return 0;
var layerName = LayerMask.LayerToName(layer);
return DrawTinyLabel(ref area, layerName, "Layer", true).width;
}
/************************************************************************************************************************/
private static float DoTagGUI(GameObject gameObject, Rect area, bool drawBackground, bool drawText)
{
if (!ShowTags ||
gameObject.CompareTag("Untagged"))
return 0;
return DrawTinyLabel(ref area, gameObject.tag, "Tag", false, drawBackground, drawText).width;
}
/************************************************************************************************************************/
private static GUIStyle _LayerTagStyle;
private static Rect DrawTinyLabel(
ref Rect area,
string text,
string tooltip,
bool top,
bool drawBackground = true,
bool drawText = true)
{
if (_LayerTagStyle == null)
_LayerTagStyle = new GUIStyle(GUI.skin.label)
{
padding = new RectOffset(),
fontSize = Mathf.CeilToInt(EditorStyles.label.fontSize * 0.5f) + 3,
alignment = TextAnchor.MiddleLeft,
};
var label = IGEditorUtils.TempContent(text, tooltip);
_LayerTagStyle.CalcMinMaxWidth(label, out _, out var width);
width += 1;
var labelArea = IGEditorUtils.StealFromRight(ref area, width);
labelArea.height *= 0.5f;
if (top)
{
labelArea.height += 1;
}
else
{
labelArea.y += labelArea.height;
labelArea.y -= 1;
labelArea.height += 1;
}
if (drawBackground)
DrawLabelBackground(labelArea);
if (drawText)
GUI.Label(labelArea, label, _LayerTagStyle);
return labelArea;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Child Count
/************************************************************************************************************************/
private static GUIStyle _ChildCountStyle;
private static void DoChildCountGUI(GameObject gameObject, ref Rect area)
{
if (!ShowChildCount)
return;
var childCount = GetChildCount(gameObject);
if (childCount == null)
return;
if (_ChildCountStyle == null)
_ChildCountStyle = new GUIStyle(GUI.skin.label)
{
padding = new RectOffset(2, 2, 0, 0),
alignment = TextAnchor.MiddleRight,
};
var label = IGEditorUtils.TempContent(childCount, "Child Count");
_ChildCountStyle.CalcMinMaxWidth(label, out _, out var width);
var labelArea = IGEditorUtils.StealFromRight(ref area, width + 1);
labelArea.xMin += 1;
DrawLabelBackground(labelArea);
GUI.Button(labelArea, label, _ChildCountStyle);
}
/************************************************************************************************************************/
private static readonly Dictionary<GameObject, string>
GameObjectToChildCount = new Dictionary<GameObject, string>();
private static readonly List<Transform>
Transforms = new List<Transform>();
private static string GetChildCount(GameObject gameObject)
{
if (!GameObjectToChildCount.TryGetValue(gameObject, out var childCount))
{
Transforms.Clear();
gameObject.GetComponentsInChildren(true, Transforms);
var count = Transforms.Count - 1;
if (count == 0)
childCount = null;
else if (count < 1000)
childCount = $"{count}";
else if (count < 1000000)
childCount = $"{count / 1000}.{count % 1000 / 100}K";
else
childCount = $"{count / 1000000}.{count % 1000000 / 100000}M";
Transforms.Clear();
GameObjectToChildCount.Add(gameObject, childCount);
}
return childCount;
}
/************************************************************************************************************************/
private static void OnHierarchyChanged()
{
GameObjectToChildCount.Clear();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Preferences
/************************************************************************************************************************/
/// <summary>Should the <see cref="EnhancedHierarchy"/> be displayed?</summary>
public static readonly AutoPrefs.EditorBool
IsEnabled = new AutoPrefs.EditorBool(EditorStrings.PrefsKeyPrefix + nameof(IsEnabled), true);
/// <summary>Should the <see cref="GameObject.tag"/> be shown?</summary>
public static readonly AutoPrefs.EditorBool
ShowTags = new AutoPrefs.EditorBool(EditorStrings.PrefsKeyPrefix + nameof(ShowTags), true);
/// <summary>Should the <see cref="Transform.childCount"/> (recursive) be shown?</summary>
public static readonly AutoPrefs.EditorBool
ShowChildCount = new AutoPrefs.EditorBool(EditorStrings.PrefsKeyPrefix + nameof(ShowChildCount), true);
/// <summary>Should the <see cref="GameObject.layer"/> be shown?</summary>
public static readonly AutoPrefs.EditorInt
ShowLayers = new AutoPrefs.EditorInt(EditorStrings.PrefsKeyPrefix + nameof(ShowLayers), ~(1 << 0));
/// <summary>The maximum number of components that should be shown.</summary>
public static readonly AutoPrefs.EditorInt
MaxComponentCount = new AutoPrefs.EditorInt(EditorStrings.PrefsKeyPrefix + nameof(MaxComponentCount), 5);
/// <summary>Which keys should be held when clicking a component to enable/disable it?</summary>
private static readonly ModifierKeysPref
Modifiers = new ModifierKeysPref(EditorStrings.PrefsKeyPrefix + nameof(Modifiers), ModifierKey.Alt);
/************************************************************************************************************************/
/// <summary>Editor GUI to display the <see cref="EnhancedHierarchy"/> preferences.</summary>
public static class Preferences
{
/************************************************************************************************************************/
/// <summary>The headding of this section.</summary>
public const string Headding = "Enhanced Hierarchy";
private static readonly GUIContent
IsEnabledContent = new GUIContent("Enabled",
"Should the following enhancements be displayed in the Hierarchy?"),
ShowLayerContent = new GUIContent("Show Layers",
"Which layers should be shown in the Hierarchy?"),
ShowTagsContent = new GUIContent("Show Tag",
"Should the Hierarchy show the Tag of each object?"),
ShowChildCountContent = new GUIContent("Show Child Count",
"Should the Hierarchy show the number of children each object has?"),
MaxComponentCountContent = new GUIContent("Max Component Count",
"How many Components should the Hierarchy show for each object?"),
ModifiersContent = new GUIContent("Click Modifiers",
"Clicking a Component Icon in the Hierarchy while holding these keys will Enable/Disable it." +
" Clicking the Transform Icon will Activate/Deactivate the GameObject.");
/************************************************************************************************************************/
/// <summary>Draws the preferences GUI.</summary>
public static void DoGUI()
{
Editor.Preferences.DoSectionHeader(Headding);
Editor.Preferences.DoProOnlyGroupEnabledPref(IsEnabled, IsEnabledContent);
ShowLayers.DoGUI(ShowLayerContent,
(position, label, value, style) => IGEditorUtils.DoLayerMaskField(position, label, value));
ShowTags.DoGUI(ShowTagsContent);
ShowChildCount.DoGUI(ShowChildCountContent);
MaxComponentCount.DoGUI(MaxComponentCountContent);
if (MaxComponentCount < 0)
MaxComponentCount.Value = 0;
if (MaxComponentCount == 0)
GUI.enabled = false;
Modifiers.DoGUI(ModifiersContent);
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d86035a662a77124bb044837c53e0fe9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da844247aca1e0b458120ccbd26b9a67
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More