// Crest Water System // Copyright © 2024 Wave Harmonic. All rights reserved. using System; using System.Reflection; using UnityEditor; using UnityEngine; using WaveHarmonic.Crest.Attributes; using WaveHarmonic.Crest.Editor; namespace WaveHarmonic.Crest { abstract class Predicated : Decorator { enum Mode { Property, Member, Type, RenderPipeline, } readonly Mode _Mode; readonly bool _Inverted; readonly bool _Hide; readonly bool _Implicit; readonly string _PropertyName; readonly object _DisableValue; readonly Type _Type; readonly MemberInfo _Member; readonly RenderPipeline _RenderPipeline; /// /// The field with this attribute will be drawn enabled/disabled based on return of method. /// /// The type to call the method on. Must be either a static type or the type the field is defined on. /// Member name. Method must match signature: bool MethodName(Component component). Can be any visibility and static or instance. /// Disable/Hide if matches this value. /// Flip behaviour - for example disable if a bool field is set to true (instead of false). /// Hide this component in the inspector. public Predicated(Type type, string member, object value, bool inverted = false, bool hide = false) { _Mode = Mode.Member; _Inverted = inverted; _Hide = hide; _Type = type; _DisableValue = value; _Member = _Type.GetMember(member, Helpers.s_AnyMethod)[0]; } /// public Predicated(Type type, string member, bool inverted = false, bool hide = false) : this(type, member, true, inverted, hide) { } /// /// Enable/Disable field depending on the current type of the component. /// /// If a component of this type is not attached to this GameObject, disable the GUI (or enable if inverted is true). /// Flip behaviour - for example disable if a bool field is set to true (instead of false). /// Hide this component in the inspector. public Predicated(Type type, bool inverted = false, bool hide = false) { _Mode = Mode.Type; _Inverted = inverted; _Hide = hide; _Type = type; } public Predicated(string property, bool inverted = false, bool hide = false) { _Mode = Mode.Property; _Inverted = inverted; _Hide = hide; _PropertyName = property; _Implicit = true; } /// /// The field with this attribute will be drawn enabled/disabled based on another field. For example can be used /// to disable a field if a toggle is false. /// /// The name of the other property whose value dictates whether this field is enabled or not. /// Flip behaviour - for example disable if a bool field is set to true (instead of false). /// If the field has this value, disable the GUI (or enable if inverted is true). /// Hide this component in the inspector. public Predicated(string property, object value, bool inverted = false, bool hide = false) { _Mode = Mode.Property; _Inverted = inverted; _Hide = hide; _PropertyName = property; _DisableValue = value; } /// /// Field is predicated (enabled/shown) on which render pipeline is active. /// /// Enable if this render pipeline is active. /// Invert behaviour. /// Hide instead of disable. public Predicated(RenderPipeline rp, bool inverted = false, bool hide = false) { _Mode = Mode.RenderPipeline; _Inverted = inverted; _Hide = hide; _RenderPipeline = rp; } public override bool AlwaysVisible => true; public bool GUIEnabled(SerializedProperty property) { if (_Implicit && property.propertyType is not SerializedPropertyType.Boolean and not SerializedPropertyType.ObjectReference and not SerializedPropertyType.ManagedReference) { Debug.Log($"Crest.Predicated: Implicit predicated on {property.name} cannot be used for {property.propertyType}."); return false; } return _Inverted != property.propertyType switch { // Enable GUI if int value of field is not equal to 0, or whatever the disable-value is set to SerializedPropertyType.Integer => property.intValue != ((int?)_DisableValue ?? 0), // Enable GUI if disable-value is 0 and field is true, or disable-value is not 0 and field is false SerializedPropertyType.Boolean => _Implicit ? !property.boolValue : (bool)_DisableValue == property.boolValue, SerializedPropertyType.Float => property.floatValue != ((float?)_DisableValue ?? 0), SerializedPropertyType.String => property.stringValue != ((string)_DisableValue ?? ""), // Must pass nameof enum entry as we are comparing names because index != value. SerializedPropertyType.Enum => property.enumNames[property.enumValueIndex] != ((string)_DisableValue ?? ""), SerializedPropertyType.ObjectReference => _Implicit ? property.objectReferenceValue == null : property.objectReferenceValue != null, SerializedPropertyType.ManagedReference => _Implicit ? property.managedReferenceValue == null : property.managedReferenceValue != null, _ => throw new ArgumentException($"Crest.Predicated: property type on {property.serializedObject.targetObject} not implemented yet: {property.propertyType}."), }; } internal override void Decorate(Rect position, SerializedProperty property, GUIContent label, DecoratedDrawer drawer) { var enabled = true; if (_Mode == Mode.Property) { var propertyPath = _PropertyName; if (property.depth > 0) { // Get the property path so we can find it from the serialized object. propertyPath = $"{string.Join(".", property.propertyPath.Split(".", StringSplitOptions.None)[0..^1])}.{propertyPath}"; } // Get the other property to be the predicate for the enabled/disabled state of this property. // Do not use property.FindPropertyRelative as it does not work with nested properties. // Try and find the nested property first and then fallback to the root object. var otherProperty = property.serializedObject.FindProperty(propertyPath) ?? property.serializedObject.FindProperty(_PropertyName); Debug.AssertFormat(otherProperty != null, "Crest.Predicated: {0} or {1} property on {2} ({3}) could not be found!", propertyPath, _PropertyName, property.serializedObject.targetObject.GetType(), property.name); if (otherProperty != null) { enabled = GUIEnabled(otherProperty); } } else if (_Mode == Mode.Member) { // Static is both abstract and sealed: https://stackoverflow.com/a/1175950 object @object = _Type.IsAbstract && _Type.IsSealed ? null : property.serializedObject.targetObject; // If this is a nested type, grab that type. This may not be suitable in all cases. if (property.depth > 0) { // Get the property path so we can find it from the serialized object. var propertyPath = string.Join(".", property.propertyPath.Split(".", StringSplitOptions.None)[0..^1]); var otherProperty = property.serializedObject.FindProperty(propertyPath); @object = otherProperty.propertyType switch { SerializedPropertyType.ManagedReference => otherProperty.managedReferenceValue, SerializedPropertyType.Generic => otherProperty.boxedValue, _ => @object, }; } if (_Member is PropertyInfo autoProperty) { // == operator does not work. enabled = !autoProperty.GetValue(@object).Equals(_DisableValue); } else if (_Member is MethodInfo method) { enabled = !method.Invoke(@object, new object[] { }).Equals(_DisableValue); } if (_Inverted) enabled = !enabled; } else if (_Mode == Mode.Type) { var type = property.serializedObject.targetObject.GetType(); // If this is a nested type, grab that type. This may not be suitable in all cases. if (property.depth > 0) { // Get the property path so we can find it from the serialized object. var propertyPath = string.Join(".", property.propertyPath.Split(".", StringSplitOptions.None)[0..^1]); var otherProperty = property.serializedObject.FindProperty(propertyPath); type = otherProperty.propertyType switch { SerializedPropertyType.ManagedReference => otherProperty.managedReferenceValue.GetType(), SerializedPropertyType.Generic => otherProperty.boxedValue.GetType(), _ => type, }; } var enabledByTypeCheck = _Type.IsAssignableFrom(type); if (!_Inverted) enabledByTypeCheck = !enabledByTypeCheck; enabled = enabledByTypeCheck && enabled; } else if (_Mode == Mode.RenderPipeline) { enabled = RenderPipelineHelper.RenderPipeline == _RenderPipeline == _Inverted; } // Keep current disabled state. GUI.enabled &= enabled; // Keep previous hidden state. DecoratedDrawer.s_HideInInspector = DecoratedDrawer.s_HideInInspector || (_Hide && !enabled); } } sealed class Show : Predicated { const bool k_Hide = true; const bool k_Inverted = true; /// /// Show this field if member's (method) returned value matches provided. /// /// public Show(Type type, string member, object value) : base(type, member, value, k_Inverted, k_Hide) { } /// /// Show this field if member (method) returns true. /// /// public Show(Type type, string member) : this(type, member, value: true) { } /// /// Show field if owning class is of type. /// public Show(Type type) : base(type, k_Inverted, k_Hide) { } /// /// Show if field is null or false if reference or boolean respectively. /// public Show(string property) : base(property, k_Inverted, k_Hide) { } /// /// Show if field matches value. /// public Show(string property, object value) : base(property, value, k_Inverted, k_Hide) { } /// /// Show if current render pipeline matches. /// public Show(RenderPipeline rp) : base(rp, k_Inverted, k_Hide) { } } sealed class Hide : Predicated { const bool k_Hide = true; const bool k_Inverted = false; /// /// Hide this field if member's (method) returned value matches provided. /// /// public Hide(Type type, string member, object value) : base(type, member, value, k_Inverted, k_Hide) { } /// /// Hide this field if member (method) returns true. /// /// public Hide(Type type, string member) : this(type, member, value: true) { } /// /// Hide field if owning class is of type. /// public Hide(Type type) : base(type, k_Inverted, k_Hide) { } /// /// Hide if field is null or false if reference or boolean respectively. /// public Hide(string property) : base(property, k_Inverted, k_Hide) { } /// /// Hide if field matches value. /// public Hide(string property, object value) : base(property, value, k_Inverted, k_Hide) { } /// /// Hide if current render pipeline matches. /// public Hide(RenderPipeline rp) : base(rp, k_Inverted, k_Hide) { } } sealed class Enable : Predicated { const bool k_Hide = false; const bool k_Inverted = true; /// /// Enable this field if member's (method) returned value matches provided. /// /// public Enable(Type type, string member, object value) : base(type, member, value, k_Inverted, k_Hide) { } /// /// Enable this field if member (method) returns true. /// /// public Enable(Type type, string member) : this(type, member, value: true) { } /// /// Enable field if owning class is of type. /// public Enable(Type type) : base(type, k_Inverted, k_Hide) { } /// /// Enable if field is null or false if reference or boolean respectively. /// public Enable(string property) : base(property, k_Inverted, k_Hide) { } /// /// Enable if field matches value. /// public Enable(string property, object value) : base(property, value, k_Inverted, k_Hide) { } /// /// Enable if current render pipeline matches. /// public Enable(RenderPipeline rp) : base(rp, k_Inverted, k_Hide) { } } sealed class Disable : Predicated { const bool k_Hide = false; const bool k_Inverted = false; /// /// Disable this field if member's (method) returned value matches provided. /// /// public Disable(Type type, string member, object value) : base(type, member, value, k_Inverted, k_Hide) { } /// /// Disable this field if member (method) returns true. /// /// public Disable(Type type, string member) : this(type, member, value: true) { } /// /// Disable field if owning class is of type. /// public Disable(Type type) : base(type, k_Inverted, k_Hide) { } /// /// Disable if field is null or false if reference or boolean respectively. /// public Disable(string property) : base(property, k_Inverted, k_Hide) { } /// /// Disable if field matches value. /// public Disable(string property, object value) : base(property, value, k_Inverted, k_Hide) { } /// /// Disable if current render pipeline matches. /// public Disable(RenderPipeline rp) : base(rp, k_Inverted, k_Hide) { } } }