1
This commit is contained in:
@@ -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:
|
||||
+164
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e982d21179aefd54aa31d81d0fda05c0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+72
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6f9f25a826251946832ff52c7b1e800
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+295
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abe62369061dcc2459a726fc769c071a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+169
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a4e659332e96cf4390c78e230503875
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+167
@@ -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
|
||||
|
||||
+11
@@ -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:
|
||||
+38
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d51c640b686c8a40915056f9ddff2ff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+90
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7af0f370a1ee83049ac34148b66ecd0d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+53
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47345018c846f1a43a77c3bde9ea154c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+151
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfb9e8bedfb1b8e4b9a6431befbecb57
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+28
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa6a81e5126acac498b33675e5b897b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+47
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e35b3ffd6bf43114792cfd43eaeec762
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+187
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50cbcd2ee58e2fd479df04ac86b27da1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+196
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f1c338e81e558947be4990b1c2174a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+98
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec84a027b80a78a4e928c3008b3386fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+477
@@ -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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc58f5a52d383c241adcc5cf32e8191c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+90
@@ -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
|
||||
|
||||
+11
@@ -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:
|
||||
+71
@@ -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
|
||||
+11
@@ -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:
|
||||
+85
@@ -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
|
||||
|
||||
+11
@@ -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:
|
||||
+257
@@ -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
|
||||
|
||||
+11
@@ -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
|
||||
+11
@@ -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
|
||||
+11
@@ -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:
|
||||
+152
@@ -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
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60a000d388aa48b40ac228365048bf31
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+641
@@ -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
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b1d180fa9369124aa95bdba3dd0c362
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+399
@@ -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
|
||||
+11
@@ -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
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c0f294e9b1d7a14d9f3a8d060c392d8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+418
@@ -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
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9edf4968362d1c84b964f149978ccf62
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1082
File diff suppressed because it is too large
Load Diff
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58cf44485b662fd438585709c4b1a624
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+49
@@ -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
|
||||
}
|
||||
+7
@@ -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
Reference in New Issue
Block a user