Skip to content

Instantly share code, notes, and snippets.

@capnslipp
Last active June 18, 2023 19:47
Show Gist options
  • Save capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0 to your computer and use it in GitHub Desktop.
Save capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0 to your computer and use it in GitHub Desktop.
ShowWhen, HideWhen, EnableWhen, & DisableWhen Attributes for #Unity3D
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Shows a field as disabled in Unity's Inspector only when a different method/property/field in the same MonoBehaviour returns/is-equal-to a given value or values.
/// @why: Because this functionality should be built-into Unity.
/// @usage: Add `[DisableWhen(typeof(«MonoBehaviour-sub-type-name»), "«trigger-member-name»", «value-that-the-trigger-member-returns»)]` attribute to a (serializable public) field on your `MonoBehaviour`-class.
/// @intended project path: Assets/Plugins/EditorUtils/DisableWhenAttribute.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using System;
using UnityEngine;
using UObject=UnityEngine.Object;
public class DisableWhenAttribute : WhenAttributeBase
{
public DisableWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1 })
{}
public DisableWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1, object whenValue2) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1, whenValue2 })
{}
public DisableWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1, object whenValue2, object whenValue3) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1, whenValue2, whenValue3 })
{}
}
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Unity Inspector drawer for `DisableWhenAttribute`.
/// @why: Because this functionality should be built-into Unity.
/// @usage: @See `DisableWhenAttribute.cs`.
/// @intended project path: Assets/Plugins/Editor/EditorUtils/DisableWhenDrawer.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using UnityEngine;
using UObject=UnityEngine.Object;
using UnityEditor;
[CustomPropertyDrawer(typeof(DisableWhenAttribute))]
class DisableWhenDrawer : WhenDrawerBase
{
override protected bool ShouldShow(UObject target, WhenAttributeBase attribute) {
return true;
}
override protected bool ShouldEnable(UObject target, WhenAttributeBase attribute)
{
bool shouldDisable = IsWhenPropertyAtAWhenValue(target, attribute);
return !shouldDisable;
}
}
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Shows a field as enabled in Unity's Inspector only when a different method/property/field in the same MonoBehaviour returns/is-equal-to a given value or values.
/// @why: Because this functionality should be built-into Unity.
/// @usage: Add `[DisableWhen(typeof(«MonoBehaviour-sub-type-name»), "«trigger-member-name»", «value-that-the-trigger-member-returns»)]` attribute to a (serializable public) field on your `MonoBehaviour`-class.
/// @intended project path: Assets/Plugins/EditorUtils/EnableWhenAttribute.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using System;
using UnityEngine;
using UObject=UnityEngine.Object;
public class EnableWhenAttribute : WhenAttributeBase
{
public EnableWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1 })
{}
public EnableWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1, object whenValue2) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1, whenValue2 })
{}
public EnableWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1, object whenValue2, object whenValue3) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1, whenValue2, whenValue3 })
{}
}
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Unity Inspector drawer for `EnableWhenAttribute`.
/// @why: Because this functionality should be built-into Unity.
/// @usage: @See `EnableWhenAttribute.cs`.
/// @intended project path: Assets/Plugins/Editor/EditorUtils/EnableWhenDrawer.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using UnityEngine;
using UObject=UnityEngine.Object;
using UnityEditor;
[CustomPropertyDrawer(typeof(EnableWhenAttribute))]
class EnableWhenDrawer : WhenDrawerBase
{
override protected bool ShouldShow(UObject target, WhenAttributeBase attribute) {
return true;
}
override protected bool ShouldEnable(UObject target, WhenAttributeBase attribute)
{
bool shouldEnable = IsWhenPropertyAtAWhenValue(target, attribute);
return shouldEnable;
}
}
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Hides a field in Unity's Inspector only when a different method/property/field in the same MonoBehaviour returns/is-equal-to a given value or values.
/// @why: Because this functionality should be built-into Unity.
/// @usage: Add `[HideWhen(typeof(«MonoBehaviour-sub-type-name»), "«trigger-member-name»", «value-that-the-trigger-member-returns»)]` attribute to a (serializable public) field on your `MonoBehaviour`-class.
/// @intended project path: Assets/Plugins/EditorUtils/HideWhenAttribute.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using System;
using UnityEngine;
using UObject=UnityEngine.Object;
public class HideWhenAttribute : WhenAttributeBase
{
public HideWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1 })
{}
public HideWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1, object whenValue2) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1, whenValue2 })
{}
public HideWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1, object whenValue2, object whenValue3) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1, whenValue2, whenValue3 })
{}
}
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Unity Inspector drawer for `HideWhenAttribute`.
/// @why: Because this functionality should be built-into Unity.
/// @usage: @See `HideWhenAttribute.cs`.
/// @intended project path: Assets/Plugins/Editor/EditorUtils/HideWhenDrawer.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using UnityEngine;
using UObject=UnityEngine.Object;
using UnityEditor;
[CustomPropertyDrawer(typeof(HideWhenAttribute))]
class HideWhenDrawer : WhenDrawerBase
{
override protected bool ShouldShow(UObject target, WhenAttributeBase attribute)
{
bool shouldHide = IsWhenPropertyAtAWhenValue(target, attribute);
return !shouldHide;
}
override protected bool ShouldEnable(UObject target, WhenAttributeBase attribute) {
return true;
}
}
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Shows a field in Unity's Inspector only when a different method/property/field in the same MonoBehaviour returns/is-equal-to a given value or values.
/// @why: Because this functionality should be built-into Unity.
/// @usage: Add `[ShowWhen(typeof(«MonoBehaviour-sub-type-name»), "«trigger-member-name»", «value-that-the-trigger-member-returns»)]` attribute to a (serializable public) field on your `MonoBehaviour`-class.
/// @intended project path: Assets/Plugins/EditorUtils/ShowWhenAttribute.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using System;
using UnityEngine;
using UObject=UnityEngine.Object;
public class ShowWhenAttribute : WhenAttributeBase
{
public ShowWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1 })
{}
public ShowWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1, object whenValue2) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1, whenValue2 })
{}
public ShowWhenAttribute(Type classType, string methodPropertyOrFieldName, object whenValue1, object whenValue2, object whenValue3) :
base(classType, methodPropertyOrFieldName, new object[] { whenValue1, whenValue2, whenValue3 })
{}
}
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Unity Inspector drawer for `ShowWhenAttribute`.
/// @why: Because this functionality should be built-into Unity.
/// @usage: @See `ShowWhenAttribute.cs`.
/// @intended project path: Assets/Plugins/Editor/EditorUtils/ShowWhenDrawer.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using UnityEngine;
using UObject=UnityEngine.Object;
using UnityEditor;
[CustomPropertyDrawer(typeof(ShowWhenAttribute))]
class ShowWhenDrawer : WhenDrawerBase
{
override protected bool ShouldShow(UObject target, WhenAttributeBase attribute)
{
bool shouldShow = IsWhenPropertyAtAWhenValue(target, attribute);
return shouldShow;
}
override protected bool ShouldEnable(UObject target, WhenAttributeBase attribute) {
return true;
}
}
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Common-functionality base class for `ShowWhenAttribute` & `HideWhenAttribute`
/// @why: Because this functionality should be built-into Unity.
/// @usage: @See `ShowWhenAttribute.cs` & `HideWhenAttribute.cs`.
/// @intended project path: Assets/Plugins/EditorUtils/WhenAttributeBase.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using System;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEngine;
using UObject=UnityEngine.Object;
using System.Linq;
public abstract class WhenAttributeBase : PropertyAttribute
{
const BindingFlags InstanceAnyPrivacyBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
const BindingFlags StaticPublicBindingFlags = BindingFlags.Public | BindingFlags.Static;
protected static bool IsPossibleMethodName(string name, out string normalizedMethodName)
{
int firstOpeningParenthesisIndex = name.IndexOf("(");
if (firstOpeningParenthesisIndex == -1) {
normalizedMethodName = null;
return false;
}
normalizedMethodName = name.Substring(0, firstOpeningParenthesisIndex);
bool isIdentifier = IsPossibleIdentifier(normalizedMethodName);
return isIdentifier;
}
protected static bool IsPossiblePropertyName(string name, out string normalizedPropertyName)
{
const string GetPropertyEnding = ".get";
bool isGetProperty = name.EndsWith(GetPropertyEnding);
if (!isGetProperty) {
normalizedPropertyName = null;
return false;
}
normalizedPropertyName = name.Substring(0, name.Length - GetPropertyEnding.Length);
bool isIdentifier = IsPossibleIdentifier(normalizedPropertyName);
return isIdentifier;
}
protected static bool IsPossibleFieldName(string name)
{
bool isIdentifier = IsPossibleIdentifier(name);
return isIdentifier;
}
static Regex IdentifierRegexp = new Regex(@"^\w+$");
protected static bool IsPossibleIdentifier(string name)
{
return IdentifierRegexp.IsMatch(name);
}
public readonly IWhenCondition whenCondition;
public WhenAttributeBase(Type classType, string methodPropertyOrFieldName, params object[] whenValues)
{
string normalizedName = null;
if (IsPossibleMethodName(methodPropertyOrFieldName, out normalizedName)) {
this.whenCondition = CreateWhenConditionFromMethod(classType, normalizedName, whenValues);
}
else if (IsPossiblePropertyName(methodPropertyOrFieldName, out normalizedName)) {
this.whenCondition = CreateWhenConditionFromProperty(classType, normalizedName, whenValues);
}
else if (IsPossibleFieldName(methodPropertyOrFieldName)) {
this.whenCondition = CreateWhenConditionFromField(classType, methodPropertyOrFieldName, whenValues);
}
else {
throw new ArgumentException("Unable to find method, property, or field matching name `"+methodPropertyOrFieldName+"` on "+classType+".", methodPropertyOrFieldName);
}
}
protected static IWhenCondition CreateWhenConditionFromField(Type ownerType, string fieldName, params object[] whenValues)
{
FieldInfo fieldInfo = ownerType.GetField(fieldName, InstanceAnyPrivacyBindingFlags);
if (fieldInfo == null)
throw new ArgumentException("Unable to find field `"+fieldName+"`.", "fieldName");
Type dataType = fieldInfo.FieldType;
return CreateWhenCondition("FromField", dataType, ownerType, fieldName, whenValues);
}
protected static IWhenCondition CreateWhenConditionFromProperty(Type ownerType, string propertyName, params object[] whenValues)
{
PropertyInfo propertyInfo = ownerType.GetProperty(propertyName, InstanceAnyPrivacyBindingFlags);
if (propertyInfo == null)
throw new ArgumentException("Unable to find property `"+propertyInfo+"`.", "propertyName");
Type dataType = propertyInfo.PropertyType;
return CreateWhenCondition("FromProperty", dataType, ownerType, propertyName, whenValues);
}
protected static IWhenCondition CreateWhenConditionFromMethod(Type ownerType, string methodName, params object[] whenValues)
{
MethodInfo methodInfo = ownerType.GetMethod(methodName, InstanceAnyPrivacyBindingFlags, Type.DefaultBinder, Type.EmptyTypes, null);
if (methodInfo == null)
throw new ArgumentException("Unable to find method `"+methodName+"()`.", "methodName");
Type dataType = methodInfo.ReturnType;
return CreateWhenCondition("FromMethod", dataType, ownerType, methodName, whenValues);
}
static IWhenCondition CreateWhenCondition(string initializerMethodName, Type dataType, Type ownerType, string memberName, params object[] whenValues)
{
bool areWhenValuesCorrectType = whenValues.All(value => dataType.IsAssignableFrom(value.GetType()));
if (!areWhenValuesCorrectType)
throw new ArgumentException("Not all whenValues are of expected type "+dataType+".");
var dataTypedWhenValues = Array.CreateInstance(dataType, whenValues.Length);
for (int i = whenValues.Length - 1; i >= 0; --i)
dataTypedWhenValues.SetValue(whenValues[i], i);
Type dataTypedWhenConditionType = typeof(WhenCondition<>).MakeGenericType(new Type[] { dataType });
MethodInfo whenConditionInitializer = dataTypedWhenConditionType.GetMethod(initializerMethodName, StaticPublicBindingFlags);
var whenCondition = whenConditionInitializer.Invoke(dataTypedWhenConditionType,
new object[] { ownerType, memberName, dataTypedWhenValues }
);
return (IWhenCondition)whenCondition;
}
public interface IWhenCondition
{
bool IsAtAWhenValue(UObject ownerObject);
}
protected class WhenCondition<DataT> : IWhenCondition
{
public delegate DataT ValueDelegate(UObject owner);
ValueDelegate _valueDelegate;
DataT[] _whenValues;
public WhenCondition(ValueDelegate valueDelegate, params DataT[] whenValues)
{
_valueDelegate = valueDelegate;
_whenValues = whenValues;
}
public static WhenCondition<DataT> FromField(Type ownerType, string fieldName, params DataT[] whenValues) {
return new WhenCondition<DataT>(
FindValueDelegateViaField(ownerType, fieldName),
whenValues
);
}
public static WhenCondition<DataT> FromProperty(Type ownerType, string propertyName, params DataT[] whenValues) {
return new WhenCondition<DataT>(
FindValueDelegateViaProperty(ownerType, propertyName),
whenValues
);
}
public static WhenCondition<DataT> FromMethod(Type ownerType, string methodName, params DataT[] whenValues) {
return new WhenCondition<DataT>(
FindValueDelegateViaMethod(ownerType, methodName),
whenValues
);
}
public bool IsAtAWhenValue(UObject ownerObject)
{
DataT currentValue = _valueDelegate(ownerObject);
bool isAtAWhenValue = (Array.IndexOf(_whenValues, currentValue) != -1);
return isAtAWhenValue;
}
static ValueDelegate FindValueDelegateViaField(Type ownerType, string fieldName)
{
if (ownerType == null || fieldName == null)
return null;
FieldInfo fieldInfo = ownerType.GetField(fieldName, InstanceAnyPrivacyBindingFlags);
if (fieldInfo == null)
throw new ArgumentException("Unable to find field `"+fieldName+"`.", "fieldName");
Type fieldType = fieldInfo.FieldType;
if (fieldType != typeof(DataT))
throw new ArgumentException("Field `"+fieldName+"` not of type "+typeof(DataT)+"; found `"+fieldType+" "+fieldName+"` instead.", "fieldName");
ValueDelegate valueDelegate = (UObject owner) => {
return (DataT)fieldInfo.GetValue(owner);
};
return valueDelegate;
}
static ValueDelegate FindValueDelegateViaProperty(Type ownerType, string propertyName)
{
if (ownerType == null || propertyName == null)
return null;
PropertyInfo propertyInfo = ownerType.GetProperty(propertyName, InstanceAnyPrivacyBindingFlags);
if (propertyInfo == null)
throw new ArgumentException("Unable to find property `"+propertyName+"`.", "propertyName");
Type propertyType = propertyInfo.PropertyType;
if (propertyType != typeof(DataT))
throw new ArgumentException("Property `"+propertyName+"` not of type "+typeof(DataT)+"; found `"+propertyType+" "+propertyName+"` instead.", "propertyName");
ValueDelegate valueDelegate = (UObject owner) => {
return (DataT)propertyInfo.GetValue(owner, index: null);
};
return valueDelegate;
}
static ValueDelegate FindValueDelegateViaMethod(Type ownerType, string methodName)
{
if (ownerType == null || methodName == null)
return null;
MethodInfo methodInfo = ownerType.GetMethod(methodName, InstanceAnyPrivacyBindingFlags, Type.DefaultBinder, Type.EmptyTypes, null);
if (methodInfo == null)
throw new ArgumentException("Unable to find method `"+methodName+"()`.", "methodName");
Type returnType = methodInfo.ReturnType;
if (returnType != typeof(DataT))
throw new ArgumentException("Method `"+methodName+"` does not have return type "+typeof(DataT)+"; found `"+returnType+" "+methodName+"()` instead.", "methodName");
ValueDelegate valueDelegate = (UObject owner) => {
return (DataT)methodInfo.Invoke(owner, parameters: null);
};
return valueDelegate;
}
}
}
/// @creator: Slipp Douglas Thompson
/// @license: Public Domain per The Unlicense. See <http://unlicense.org/>.
/// @purpose: Common-functionality base class for `ShowWhenDrawer` & `HideWhenDrawer`
/// @why: Because this functionality should be built-into Unity.
/// @usage: @See `ShowWhenAttribute.cs` & `HideWhenAttribute.cs`.
/// @intended project path: Assets/Plugins/Editor/EditorUtils/WhenDrawerBase.cs
/// @interwebsouce: https://gist.github.com/capnslipp/9b99f83aa7b80dfb589a5b23e8e0cfa0
using System;
using UnityEngine;
using UObject=UnityEngine.Object;
using UnityEditor;
abstract class WhenDrawerBase : PropertyDrawer
{
public override void OnGUI(Rect totalPosition, SerializedProperty uProperty, GUIContent label)
{
bool shouldShow = DetermineIfShouldShow(uProperty);
bool shouldEnable = DetermineIfShouldEnable(uProperty);
if (shouldShow) {
bool preexistingGUIEnabled = GUI.enabled;
GUI.enabled = shouldEnable;
EditorGUI.PropertyField(totalPosition, uProperty, label, true);
GUI.enabled = preexistingGUIEnabled;
}
}
public override float GetPropertyHeight(SerializedProperty uProperty, GUIContent label)
{
bool shouldShow = DetermineIfShouldShow(uProperty);
if (shouldShow)
return EditorGUI.GetPropertyHeight(uProperty, label, true);
else
return 0.0f;
}
private bool DetermineIfShouldShow(SerializedProperty uProperty)
{
UObject target = uProperty.serializedObject.targetObject;
WhenAttributeBase attribute = (WhenAttributeBase)this.attribute;
return ShouldShow(target, attribute);
}
private bool DetermineIfShouldEnable(SerializedProperty uProperty)
{
UObject target = uProperty.serializedObject.targetObject;
WhenAttributeBase attribute = (WhenAttributeBase)this.attribute;
return ShouldEnable(target, attribute);
}
protected bool IsWhenPropertyAtAWhenValue(UObject target, WhenAttributeBase attribute)
{
return attribute.whenCondition.IsAtAWhenValue(target);
}
// To Be Implemented By Concrete Subclasses
abstract protected bool ShouldShow(UObject target, WhenAttributeBase attribute);
abstract protected bool ShouldEnable(UObject target, WhenAttributeBase attribute);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment