using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor.AnimatedValues;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Rendering;
namespace UnityEditor.Rendering
{
///
/// This attributes tells a class which type of
/// it's an editor for.
/// When you make a custom editor for a component, you need put this attribute on the editor
/// class.
///
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class VolumeComponentEditorAttribute : Attribute
{
///
/// A type derived from .
///
public readonly Type componentType;
///
/// Creates a new instance.
///
/// A type derived from
public VolumeComponentEditorAttribute(Type componentType)
{
this.componentType = componentType;
}
}
///
/// A custom editor class that draws a in the Inspector. If you do not
/// provide a custom editor for a , Unity uses the default one.
/// You must use a to let the editor know which
/// component this drawer is for.
///
///
/// Below is an example of a custom :
///
/// using UnityEngine.Rendering;
///
/// [Serializable, VolumeComponentMenu("Custom/Example Component")]
/// public class ExampleComponent : VolumeComponent
/// {
/// public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);
/// }
///
/// And its associated editor:
///
/// using UnityEditor.Rendering;
///
/// [VolumeComponentEditor(typeof(ExampleComponent))]
/// class ExampleComponentEditor : VolumeComponentEditor
/// {
/// SerializedDataParameter m_Intensity;
///
/// public override void OnEnable()
/// {
/// var o = new PropertyFetcher<ExampleComponent>(serializedObject);
/// m_Intensity = Unpack(o.Find(x => x.intensity));
/// }
///
/// public override void OnInspectorGUI()
/// {
/// PropertyField(m_Intensity);
/// }
/// }
///
///
///
public class VolumeComponentEditor
{
class Styles
{
public static GUIContent overrideSettingText { get; } = EditorGUIUtility.TrTextContent("", "Override this setting for this volume.");
public static GUIContent allText { get; } = EditorGUIUtility.TrTextContent("ALL", "Toggle all overrides on. To maximize performances you should only toggle overrides that you actually need.");
public static GUIContent noneText { get; } = EditorGUIUtility.TrTextContent("NONE", "Toggle all overrides off.");
public static string toggleAllText { get; } = L10n.Tr("Toggle All");
public const int overrideCheckboxWidth = 14;
public const int overrideCheckboxOffset = 9;
}
Vector2? m_OverrideToggleSize;
internal Vector2 overrideToggleSize
{
get
{
if (!m_OverrideToggleSize.HasValue)
m_OverrideToggleSize = CoreEditorStyles.smallTickbox.CalcSize(Styles.overrideSettingText);
return m_OverrideToggleSize.Value;
}
}
///
/// Specifies the this editor is drawing.
///
public VolumeComponent target { get; private set; }
///
/// A SerializedObject representing the object being inspected.
///
public SerializedObject serializedObject { get; private set; }
///
/// The copy of the serialized property of the being
/// inspected. Unity uses this to track whether the editor is collapsed in the Inspector or not.
///
public SerializedProperty baseProperty { get; internal set; }
///
/// The serialized property of for the component being
/// inspected.
///
public SerializedProperty activeProperty { get; internal set; }
#region Additional Properties
AnimFloat m_AdditionalPropertiesAnimation;
EditorPrefBool m_ShowAdditionalProperties;
List m_VolumeNotAdditionalParameters;
///
/// Override this property if your editor makes use of the "Additional Properties" feature.
///
public virtual bool hasAdditionalProperties => target.parameters.Count != m_VolumeNotAdditionalParameters.Count;
///
/// Set to true to show additional properties.
///
public bool showAdditionalProperties
{
get => m_ShowAdditionalProperties.value;
set
{
if (value && !m_ShowAdditionalProperties.value)
{
m_AdditionalPropertiesAnimation.value = 1.0f;
m_AdditionalPropertiesAnimation.target = 0.0f;
}
SetAdditionalPropertiesPreference(value);
}
}
///
/// Start a scope for additional properties.
/// This will handle the highlight of the background when toggled on and off.
///
/// True if the additional content should be drawn.
protected bool BeginAdditionalPropertiesScope()
{
if (hasAdditionalProperties && showAdditionalProperties)
{
CoreEditorUtils.BeginAdditionalPropertiesHighlight(m_AdditionalPropertiesAnimation);
return true;
}
else
{
return false;
}
}
///
/// End a scope for additional properties.
///
protected void EndAdditionalPropertiesScope()
{
if (hasAdditionalProperties && showAdditionalProperties)
{
CoreEditorUtils.EndAdditionalPropertiesHighlight();
}
}
#endregion
///
/// A reference to the parent editor in the Inspector.
///
protected Editor m_Inspector;
List<(GUIContent displayName, int displayOrder, SerializedDataParameter param)> m_Parameters;
static Dictionary s_ParameterDrawers;
static VolumeComponentEditor()
{
s_ParameterDrawers = new Dictionary();
ReloadDecoratorTypes();
}
[Callbacks.DidReloadScripts]
static void OnEditorReload()
{
ReloadDecoratorTypes();
}
static void ReloadDecoratorTypes()
{
s_ParameterDrawers.Clear();
// Look for all the valid parameter drawers
var types = CoreUtils.GetAllTypesDerivedFrom()
.Where(t => t.IsDefined(typeof(VolumeParameterDrawerAttribute), false) && !t.IsAbstract);
// Store them
foreach (var type in types)
{
var attr = (VolumeParameterDrawerAttribute)type.GetCustomAttributes(typeof(VolumeParameterDrawerAttribute), false)[0];
var decorator = (VolumeParameterDrawer)Activator.CreateInstance(type);
s_ParameterDrawers.Add(attr.parameterType, decorator);
}
}
///
/// Triggers an Inspector repaint event.
///
public void Repaint()
{
if (m_Inspector != null) // Can happen in tests.
m_Inspector.Repaint();
// Volume Component Editors can be shown in the ProjectSettings window (default volume profile)
// This will force a repaint of the whole window, otherwise, additional properties highlight animation does not work properly.
SettingsService.RepaintAllSettingsWindow();
}
internal void InitAdditionalPropertiesPreference()
{
string key = $"UI_Show_Additional_Properties_{GetType()}";
m_ShowAdditionalProperties = new EditorPrefBool(key);
}
internal void SetAdditionalPropertiesPreference(bool value)
{
m_ShowAdditionalProperties.value = value;
}
internal void Init(VolumeComponent target, Editor inspector)
{
this.target = target;
m_Inspector = inspector;
serializedObject = new SerializedObject(target);
activeProperty = serializedObject.FindProperty("active");
InitAdditionalPropertiesPreference();
m_AdditionalPropertiesAnimation = new AnimFloat(0, Repaint)
{
speed = CoreEditorConstants.additionalPropertiesHightLightSpeed
};
InitParameters();
OnEnable();
}
void InitParameters()
{
m_VolumeNotAdditionalParameters = new List();
VolumeComponent.FindParameters(target, m_VolumeNotAdditionalParameters, field => field.GetCustomAttribute() == null);
}
void GetFields(object o, List<(FieldInfo, SerializedProperty)> infos, SerializedProperty prop = null)
{
if (o == null)
return;
var fields = o.GetType()
.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var field in fields)
{
if (field.FieldType.IsSubclassOf(typeof(VolumeParameter)))
{
if ((field.GetCustomAttributes(typeof(HideInInspector), false).Length == 0) &&
((field.GetCustomAttributes(typeof(SerializeField), false).Length > 0) ||
(field.IsPublic && field.GetCustomAttributes(typeof(NonSerializedAttribute), false).Length == 0)))
infos.Add((field, prop == null ?
serializedObject.FindProperty(field.Name) : prop.FindPropertyRelative(field.Name)));
}
else if (!field.FieldType.IsArray && field.FieldType.IsClass)
GetFields(field.GetValue(o), infos, prop == null ?
serializedObject.FindProperty(field.Name) : prop.FindPropertyRelative(field.Name));
}
}
///
/// Unity calls this method when the object loads.
///
///
/// You can safely override this method and not call base.OnEnable() unless you want
/// Unity to display all the properties from the automatically.
///
public virtual void OnEnable()
{
// Grab all valid serializable field on the VolumeComponent
// TODO: Should only be done when needed / on demand as this can potentially be wasted CPU when a custom editor is in use
var fields = new List<(FieldInfo, SerializedProperty)>();
GetFields(target, fields);
m_Parameters = fields
.Select(t =>
{
var name = "";
var order = 0;
var (fieldInfo, serializedProperty) = t;
var attr = (DisplayInfoAttribute[])fieldInfo.GetCustomAttributes(typeof(DisplayInfoAttribute), true);
if (attr.Length != 0)
{
name = attr[0].name;
order = attr[0].order;
}
var parameter = new SerializedDataParameter(t.Item2);
return (EditorGUIUtility.TrTextContent(name), order, parameter);
})
.OrderBy(t => t.order)
.ToList();
}
///
/// Unity calls this method when the object goes out of scope.
///
public virtual void OnDisable()
{
}
internal void OnInternalInspectorGUI()
{
serializedObject.Update();
using (new EditorGUILayout.VerticalScope())
{
TopRowFields();
OnInspectorGUI();
EditorGUILayout.Space();
}
serializedObject.ApplyModifiedProperties();
}
///
/// Unity calls this method each time it re-draws the Inspector.
///
///
/// You can safely override this method and not call base.OnInspectorGUI() unless you
/// want Unity to display all the properties from the
/// automatically.
///
public virtual void OnInspectorGUI()
{
// Display every field as-is
foreach (var parameter in m_Parameters)
{
if (!string.IsNullOrEmpty(parameter.displayName.text))
PropertyField(parameter.param, parameter.displayName);
else
PropertyField(parameter.param);
}
}
///
/// Sets the label for the component header. Override this method to provide
/// a custom label. If you don't, Unity automatically obtains one from the class name.
///
/// A label to display in the component header.
public virtual GUIContent GetDisplayTitle()
{
var targetType = target.GetType();
string title = string.IsNullOrEmpty(target.displayName) ? ObjectNames.NicifyVariableName(target.GetType().Name) : target.displayName;
string tooltip = targetType.GetCustomAttribute(typeof(VolumeComponentMenuForRenderPipeline), false) is VolumeComponentMenuForRenderPipeline supportedOn
? string.Join(", ", supportedOn.pipelineTypes.Select(t => ObjectNames.NicifyVariableName(t.Name)))
: string.Empty;
return EditorGUIUtility.TrTextContent(title, tooltip);
}
void AddToogleState(GUIContent content, bool state)
{
bool allOverridesSameState = AreOverridesTo(state);
if (GUILayout.Toggle(allOverridesSameState, content, CoreEditorStyles.miniLabelButton, GUILayout.ExpandWidth(false)) && !allOverridesSameState)
SetOverridesTo(state);
}
void TopRowFields()
{
using (new EditorGUILayout.HorizontalScope())
{
AddToogleState(Styles.allText, true);
AddToogleState(Styles.noneText, false);
}
}
///
/// Checks if all the visible parameters have the given state
///
/// The state to check
internal bool AreOverridesTo(bool state)
{
if (hasAdditionalProperties && showAdditionalProperties)
return AreAllOverridesTo(state);
for (int i = 0; i < m_VolumeNotAdditionalParameters.Count; ++i)
{
if (m_VolumeNotAdditionalParameters[i].overrideState != state)
return false;
}
return true;
}
///
/// Sets the given state to all the visible parameters
///
/// The state to check
internal void SetOverridesTo(bool state)
{
if (hasAdditionalProperties && showAdditionalProperties)
SetAllOverridesTo(state);
else
{
Undo.RecordObject(target, Styles.toggleAllText);
target.SetOverridesTo(m_VolumeNotAdditionalParameters, state);
serializedObject.Update();
}
}
internal bool AreAllOverridesTo(bool state)
{
for (int i = 0; i < target.parameters.Count; ++i)
{
if (target.parameters[i].overrideState != state)
return false;
}
return true;
}
internal void SetAllOverridesTo(bool state)
{
Undo.RecordObject(target, Styles.toggleAllText);
target.SetAllOverridesTo(state);
serializedObject.Update();
}
///
/// Generates and auto-populates a from a serialized
/// .
///
/// A serialized property holding a
///
///
protected SerializedDataParameter Unpack(SerializedProperty property)
{
Assert.IsNotNull(property);
return new SerializedDataParameter(property);
}
///
/// Draws a given in the editor.
///
/// The property to draw in the editor
/// true if the property field has been rendered
protected bool PropertyField(SerializedDataParameter property)
{
var title = EditorGUIUtility.TrTextContent(property.displayName,
property.GetAttribute()?.tooltip); // avoid property from getting the tooltip of another one with the same name
return PropertyField(property, title);
}
static readonly Dictionary s_HeadersGuiContents = new Dictionary();
///
/// Draws a header into the inspector with the given title
///
/// The title for the header
protected void DrawHeader(string header)
{
if (!s_HeadersGuiContents.TryGetValue(header, out GUIContent content))
{
content = EditorGUIUtility.TrTextContent(header);
s_HeadersGuiContents.Add(header, content);
}
var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight));
EditorGUI.LabelField(rect, content, EditorStyles.miniLabel);
}
///
/// Handles unity built-in decorators (Space, Header, Tooltips, ...) from attributes
///
/// The property to obtain the attributes and handle the decorators
/// A custom label and/or tooltip that might be updated by and/or by
void HandleDecorators(SerializedDataParameter property, GUIContent title)
{
foreach (var attr in property.attributes)
{
if (!(attr is PropertyAttribute))
continue;
switch (attr)
{
case SpaceAttribute spaceAttribute:
EditorGUILayout.GetControlRect(false, spaceAttribute.height);
break;
case HeaderAttribute headerAttribute:
DrawHeader(headerAttribute.header);
break;
case TooltipAttribute tooltipAttribute:
if (string.IsNullOrEmpty(title.tooltip))
title.tooltip = tooltipAttribute.tooltip;
break;
case InspectorNameAttribute inspectorNameAttribute:
title.text = inspectorNameAttribute.displayName;
break;
}
}
}
///
/// Get indentation from Indent attribute
///
/// The property to obtain the attributes
/// The relative indent level change
int HandleRelativeIndentation(SerializedDataParameter property)
{
foreach (var attr in property.attributes)
{
if (attr is VolumeComponent.Indent indent)
return indent.relativeAmount;
}
return 0;
}
///
/// Draws a given in the editor using a custom label
/// and tooltip.
///
/// The property to draw in the editor.
/// A custom label and/or tooltip.
/// true if the property field has been rendered
protected bool PropertyField(SerializedDataParameter property, GUIContent title)
{
if (VolumeParameter.IsObjectParameter(property.referenceType))
return DrawEmbeddedField(property, title);
else
return DrawPropertyField(property, title);
}
///
/// Draws a given in the editor using a custom label
/// and tooltip.
///
/// The property to draw in the editor.
/// A custom label and/or tooltip.
private bool DrawPropertyField(SerializedDataParameter property, GUIContent title)
{
using (var scope = new OverridablePropertyScope(property, title, this))
{
if (!scope.displayed)
return false;
// Custom drawer
if (scope.drawer?.OnGUI(property, title) ?? false)
return true;
// Standard Unity drawer
EditorGUILayout.PropertyField(property.value, title);
}
return true;
}
///
/// Draws a given in the editor using a custom label
/// and tooltip. This variant is only for embedded class / struct
///
/// The property to draw in the editor.
/// A custom label and/or tooltip.
private bool DrawEmbeddedField(SerializedDataParameter property, GUIContent title)
{
bool isAdditionalProperty = property.GetAttribute() != null;
bool displayed = !isAdditionalProperty || BeginAdditionalPropertiesScope();
if (!displayed)
return false;
// Custom parameter drawer
s_ParameterDrawers.TryGetValue(property.referenceType, out VolumeParameterDrawer drawer);
if (drawer != null && !drawer.IsAutoProperty())
if (drawer.OnGUI(property, title))
{
if (isAdditionalProperty)
EndAdditionalPropertiesScope();
return true;
}
// Standard Unity drawer
using (new IndentLevelScope())
{
bool expanded = property?.value?.isExpanded ?? true;
expanded = EditorGUILayout.Foldout(expanded, title, true);
if (expanded)
{
// Not the fastest way to do it but that'll do just fine for now
var it = property.value.Copy();
var end = it.GetEndProperty();
bool first = true;
while (it.Next(first) && !SerializedProperty.EqualContents(it, end))
{
PropertyField(Unpack(it));
first = false;
}
}
property.value.isExpanded = expanded;
}
if (isAdditionalProperty)
EndAdditionalPropertiesScope();
return true;
}
///
/// Draws the override checkbox used by a property in the editor.
///
/// The property to draw the override checkbox for
protected void DrawOverrideCheckbox(SerializedDataParameter property)
{
// Create a rect the height + vspacing of the property that is being overriden
float height = EditorGUI.GetPropertyHeight(property.value) + EditorGUIUtility.standardVerticalSpacing;
var overrideRect = GUILayoutUtility.GetRect(Styles.allText, CoreEditorStyles.miniLabelButton, GUILayout.Height(height), GUILayout.Width(Styles.overrideCheckboxWidth + Styles.overrideCheckboxOffset), GUILayout.ExpandWidth(false));
// also center vertically the checkbox
overrideRect.yMin += height * 0.5f - overrideToggleSize.y * 0.5f;
overrideRect.xMin += Styles.overrideCheckboxOffset;
property.overrideState.boolValue = GUI.Toggle(overrideRect, property.overrideState.boolValue, Styles.overrideSettingText, CoreEditorStyles.smallTickbox);
}
///
/// Scope for property that handle:
/// - Layout decorator (Space, Header)
/// - Naming decorator (Tooltips, InspectorName)
/// - Overridable checkbox if parameter IsAutoProperty
/// - disabled GUI if Overridable checkbox (case above) is unchecked
/// - additional property scope
/// This is automatically used inside PropertyField method
///
protected struct OverridablePropertyScope : IDisposable
{
bool isAdditionalProperty;
VolumeComponentEditor editor;
IDisposable disabledScope;
IDisposable indentScope;
internal bool haveCustomOverrideCheckbox { get; private set; }
internal VolumeParameterDrawer drawer { get; private set; }
///
/// Either the content property will be displayed or not (can varry with additional property settings)
///
public bool displayed { get; private set; }
///
/// The title modified regarding attribute used on the field
///
public GUIContent label { get; private set; }
///
/// Constructor
///
/// The property that will be drawn
/// The label of this property
/// The editor that will draw it
public OverridablePropertyScope(SerializedDataParameter property, GUIContent label, VolumeComponentEditor editor)
{
disabledScope = null;
indentScope = null;
haveCustomOverrideCheckbox = false;
drawer = null;
displayed = false;
isAdditionalProperty = false;
this.label = label;
this.editor = editor;
Init(property, label, editor);
}
///
/// Constructor
///
/// The property that will be drawn
/// The label of this property
/// The editor that will draw it
public OverridablePropertyScope(SerializedDataParameter property, string label, VolumeComponentEditor editor)
{
disabledScope = null;
indentScope = null;
haveCustomOverrideCheckbox = false;
drawer = null;
displayed = false;
isAdditionalProperty = false;
this.label = EditorGUIUtility.TrTextContent(label);
this.editor = editor;
Init(property, this.label, editor);
}
void Init(SerializedDataParameter property, GUIContent label, VolumeComponentEditor editor)
{
// Below, 3 is horizontal spacing and there is one between label and field and another between override checkbox and label
EditorGUIUtility.labelWidth -= Styles.overrideCheckboxWidth + Styles.overrideCheckboxOffset + 3 + 3;
isAdditionalProperty = property.GetAttribute() != null;
displayed = !isAdditionalProperty || editor.BeginAdditionalPropertiesScope();
s_ParameterDrawers.TryGetValue(property.referenceType, out VolumeParameterDrawer vpd);
drawer = vpd;
//never draw override for embedded class/struct
haveCustomOverrideCheckbox = (displayed && !(drawer?.IsAutoProperty() ?? true))
|| VolumeParameter.IsObjectParameter(property.referenceType);
if (displayed)
{
editor.HandleDecorators(property, label);
int relativeIndentation = editor.HandleRelativeIndentation(property);
if (relativeIndentation != 0)
indentScope = new IndentLevelScope(relativeIndentation * 15);
if (!haveCustomOverrideCheckbox)
{
EditorGUILayout.BeginHorizontal();
editor.DrawOverrideCheckbox(property);
disabledScope = new EditorGUI.DisabledScope(!property.overrideState.boolValue);
}
}
}
///
/// Dispose
///
void IDisposable.Dispose()
{
disabledScope?.Dispose();
indentScope?.Dispose();
if (!haveCustomOverrideCheckbox && displayed)
EditorGUILayout.EndHorizontal();
if (isAdditionalProperty)
editor.EndAdditionalPropertiesScope();
EditorGUIUtility.labelWidth += Styles.overrideCheckboxWidth + Styles.overrideCheckboxOffset + 3 + 3;
}
}
///
/// Like EditorGUI.IndentLevelScope but this one will also indent the override checkboxes.
///
protected class IndentLevelScope : GUI.Scope
{
int m_Offset;
///
/// Constructor
///
/// [optional] Change the indentation offset
public IndentLevelScope(int offset = 15)
{
m_Offset = offset;
// When using EditorGUI.indentLevel++, the clicking on the checkboxes does not work properly due to some issues on the C++ side.
// This scope is a work-around for this issue.
GUILayout.BeginHorizontal();
EditorGUILayout.Space(offset, false);
GUIStyle style = new GUIStyle();
GUILayout.BeginVertical(style);
EditorGUIUtility.labelWidth -= m_Offset;
}
///
/// Dispose
///
protected override void CloseScope()
{
EditorGUIUtility.labelWidth += m_Offset;
GUILayout.EndVertical();
GUILayout.EndHorizontal();
}
}
}
}