Skip to content

A Unity Editor extension for customizing inspector layout with attributes.

License

Notifications You must be signed in to change notification settings

gasgiant/Markup-Attributes

Repository files navigation

Markup Attributes

A Unity Editor extension for customizing inspector layout with attributes.

Key Features

  • Create tabs, boxes, foldouts and other groups, hide/disable properties on condition, inline editors.
  • Works in C# and in ShaderLab.
  • The inspector allows to inject custom code into it before/after/instead of any property.
  • MIT licensed, small, opt-in.

Why?

Anyone who wrote custom editors knows, that they are prone to boilerplate and often rely on hardcoded names of the properties, which adds unnecessary friction to development. One way to deal with it is to use C# Attributes, and the most prominent project to do so is Odin Inspector. It is great, but it can't be used in open source and on the Asset Store because it's paid and huge. Also, as far as know, it doesn't do anything for shader editors, which drag with them even more boilerplate and bookkeeping that the regular ones. So, here is my take on the problem.

Markup Attributes is MIT licensed and relatively small, focusing exclusively on editor layout. It works both in C# and in ShaderLab. Custom inspector provides hooks at any of the properties, which makes it possible to extend the inspector without loosing the layout functionality.

Table of Contents

  1. Installation
  2. Usage
  3. Layout Attributes
  4. Special Attributes
  5. Custom Marked Up Inspectors
  6. MarkupGUI

Installation

You can install MarkupAttributes with Unity Package Manager. Git URL:

https://github.com/gasgiant/Markup-Attributes.git#upm

Usage

MonoBehaviour and ScriptableObject

For MonoBehaviours and ScriptableObjects you need to create a custom editor that inherits form MarkedUpEditor:

using UnityEditor;
using MarkupAttributes;

[CustomEditor(typeof(MyComponent)), CanEditMultipleObjects]
internal class MyComponentEditor : MarkedUpEditor
{        
}

Alternatively, you can apply MarkedUpEditor to all classes that inherit from MonoBehaviour or ScriptableObject and don't have their own custom editor:

[CustomEditor(typeof(MonoBehaviour), true), CanEditMultipleObjects]
internal class MarkedUpMonoBehaviourEditor : MarkedUpEditor
{
}

[CustomEditor(typeof(ScriptableObject), true), CanEditMultipleObjects]
internal class MarkedUpScriptableObjectEditor : MarkedUpEditor
{
}

Serializable classes and structs

To make the attributes work inside serialized classes or structs you can add MarkedUpType to their definition or add MarkedUpField to the fields representing them:

[MarkedUpType]
[System.Serializable]
struct MyStruct
{
    ...
}

[System.Serializable]
class MyClass
{
    ...
}

class MyComponent : MonoBehaviour
{
    public MyStruct myStruct;

    [MarkedUpField]
    public MyClass myClass;
}

Note, that the attributes will work in marked up types only inside MarkedUpEditor. Currently marked up types are not supported inside arrays and lists. You can nest marked up types inside other marked up types.

Shaders

To apply attributes to the materials with a certain Shader you should tell Unity to use MarkedUpShaderGUI:

Shader "Unlit/MyShader"
{
    Properties
    {
        ...
    }

    CustomEditor "MarkupAttributes.Editor.MarkedUpShaderGUI"
    
    ...
}

Nesting Groups

Any group attribute requires a path in group hierarchy. The last entry in the path is the name of the group.

[Box("Group")]
public int one;
[TitleGroup("Group/Nested Group")]
public int two;
public int three;

Starting a group closes all groups untill a path match.

[Box("Group")]
public int one;
[TitleGroup("Group/Nested Group 1")]
public int two;
public int three;
[TitleGroup("Group/Nested Group 2")]
public int four;
public int five;

./ shortcut opens a group on top of the current one, ../ closes the topmost group and then opens a new one on top.

[Box("Group")]
public int one;
[TitleGroup("./Nested Group 1")]
public int two;
public int three;
[TitleGroup("../Nested Group 2")]
public int four;
public int five;

EndGroup closes the topmost group, or, when provided with a name, closes the named group and all of its children.

[Box("Group")]
public int one;
[Box("./Nested Group")]
public int two;
public int three;
[EndGroup("Group")]

public int four;
public int five;

ShaderLab Specifics

Unfortunately, ShaderLab does not allow any special symbols in property attributes. Because of that, we can't use / to write paths and have to replace them with spaces. Underscores then mark were you want actual spaces to be. Also, unlike in C#, you should not use quotes around the strings. For example, instead of

[Box("Parent Group/My Box")]

you would write

[Box(Parent_Group My_Box)].

The same goes for shortcuts, so

[Box("./My Box")] and [Box("../My Box")]

becomes

[Box(. MyBox)] and [Box(.. My_Box)].

Layout Attributes

EndGroup

Closes the topmost group. If provided with a name, closes the named group and all its children.

Box

Starts a vertical group in a box.

Parameter Description
string path Path to the group (see Nesting Groups).
bool labeled Adds a label to the box. Default: true.
float space Adds space before the group. Default: 0.
// C#
[Box("Labeled Box")]
public int one;
public int two;
public int three;

[Box("Unlabeled Box", labeled: false)]
public int four;
public int five;
public int six;
// ShaderLab
[Box(Labeled_Box)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[Box(Unlabeled_Box, false)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

TitleGroup

Starts a vertical group with a title.

Parameter Description
string path Path to the group (see Nesting Groups).
bool contentBox Adds a box around the group content. Default: false.
bool underline Underlines the title. Default: true.
float space Adds space before the group. Default: 3.
// C#
[TitleGroup("Title Group")]
public int one;
public int two;
public int three;

[TitleGroup("Title Group With A Content Box", contentBox: true)]
public int four;
public int five;
public int six;
// ShaderLab
[TitleGroup(Title_Group)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[TitleGroup(Title_Group_With_A_Box, true)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

Foldout

Starts a collapsible vertical group.

Parameter Description
string path Path to the group (see Nesting Groups).
bool box Puts the foldout inside a box. Default: true.
float space Adds space before the group. Default: 0.
// C#
[Foldout("Foldout In A Box")]
public int one;
public int two;
public int three;

[Foldout("Foldout", box: false)]
public int four;
public int five;
public int six;
// ShaderLab
[Foldout(Foldout_In_A_Box)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[Foldout(Foldout, false)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

TabScope and Tab

TabScope creates a control for switching tabs. Tab starts a group placed on a specified page. Names of the pages must match the names defined in TabScope.

TabScope

Parameter Description
string path Path to the group (see Nesting Groups).
string tabs Names of the tabs separated by | in C# and by space in ShaderLab.
bool box Puts the tabs inside a box. Default: false.
float space Adds space before the group. Default: 0.

Tab

Parameter Description
string path Path to the group. Name of the group must match one of the names specified in TabScope. See Nesting Groups.
// C#
[TabScope("Tab Scope", "Left|Middle|Right", box: true)]
[Tab("./Left")]
public int one;
public int two;
public int three;

[Tab("../Middle")]
public int four;
public int five;
public int six;

[Tab("../Right")]
public int seven;
public int eight;
public int nine;
// ShaderLab
[TabScope(Tab_Scope, Left Middle Right, true)]
[Tab(. Left)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[Tab(.. Middle)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

[Tab(.. Right)]
_Seven("Seven", Float) = 0
_Eight("Eight", Float) = 0
_Nine("Nine", Float) = 0

HorizontalGroup and VerticalGroup

HorizontalGroup and VerticalGroup start horizontal and vertical groups, respectively.

VerticalGroup

Parameter Description
string path Path to the group (see Nesting Groups).
float space Adds space before the group. Default: 0.

HorizontalGroup

Parameter Description
string path Path to the group (see Nesting Groups).
float labelWidth Label width inside the horizontal group.
float space Adds space before the group. Default: 0.
// C#
[HorizontalGroup("Split", labelWidth: 50)]
[VerticalGroup("./Left")]
public int one;
public int two;
public int three;

[VerticalGroup("../Right")]
public int four;
public int five;
public int six;
// ShaderLab
[HorizontalGroup(Split, 50)]
[VerticalGroup(. Left)]
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[VerticalGroup(.. Right)]
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

ReadOnly

ReadOnly and ReadOnlyGroup disable a property or a group of properties in the inspector.

Parameter Description
string path Path to the group (see Nesting Groups).
// C#
[ReadOnly]
public int one;

[ReadOnlyGroup("Read Only Group")]
public int two;
public int three;
public int four;
// ShaderLab
[ReadOnly]
_One("One", Float) = 0

[ReadOnlyGroup(Read_Only_Group)]
_Two("Two", Float) = 0
_Three("Three", Float) = 0
_Four("Four", Float) = 0

Conditionals

HideIf, ShowIf, HideIfGroup and ShowIfGroup hide/show properties or groups of properties depending on a condition.

DisableIf, EnableIf, DisableIfGroup and EnableIfGroup disable/enable properties or groups of properties depending on a condition.

Non-Group attributes work on the property they are applied to. Group attributes work like all other groups and require a path (see Nesting Groups).

C#

HideIf, ShowIf, DisableIf, EnableIf

Parameter Description
string memberName Member to check the value of. Can be instance or static. Can be a field, a property or a method, that takes no arguments. If no value is provided, must be of type bool.
object value (optional) Condition will check if member equals value using Equals method.

HideIfGroup, ShowIfGroup, DisableIfGroup, EnableIfGroup

Parameter Description
string path Path to the group (see Nesting Groups).
string memberName Member to check the value of. Can be instance or static. Can be a field, a property or a method, that takes no arguments. If no value is provided, must be of type bool.
object value (optional) Condition will check if member equals value using Equals method.
private bool boolField;
private bool BoolProperty => one % 2 == 0;
private static bool BoolMethod() => true;
public enum SomeEnum { Foo, Bar }
public SomeEnum state;

// Hide If Field
[HideIf(nameof(boolField))]
public int one;
[HideIf(nameof(boolField))]
public int two;

// Enable If Property
[EnableIfGroup("Enable If Property", nameof(BoolProperty))]
public int three;
public int four;
[EndGroup]

// Disable If Method
[DisableIfGroup("Disable If Method", nameof(BoolMethod))]
public int five;
public int six;
[EndGroup]

// Show If Enum Value
[ShowIf(nameof(state), SomeEnum.Foo)]
public int seven;
[ShowIf(nameof(state), SomeEnum.Bar)]
public int eight;

ShaderLab

HideIf, ShowIf, DisableIf, EnableIf

Parameter Description
string condition Name of the condition. Can be a float property (true if greater than zero, false otherwise), material keyword, or a global keyword (to indicate that keyword is global add G before it).

HideIfGroup, ShowIfGroup, DisableIfGroup, EnableIfGroup

Parameter Description
string path Path to the group (see Nesting Groups).
string condition Name of the condition. Can be a float property (true if greater than zero, false otherwise), material keyword, or a global keyword (to indicate that keyword is global add G before it).
// Float Property Condition
_Toggle("Toggle", Float) = 0
[ShowIf(_Toggle)]
_One("One", Float) = 0
[ShowIf(_Toggle)]
_Two("Two", Float) = 0

// Material Keyword Condition
[EnableIfGroup(Enable_If_Keyword, MY_KEYWORD)]
_Three("Three", Float) = 0
_Four("Four", Float) = 0

// Global Keyword Condition
[DisableIfGroup(Hide_If_Global_Keyword, G MY_KEYWORD)]
_Five("Five", Float) = 0
_Six("Six", Float) = 0

ToggleGroup

Starts a vertical group with a toggle, that can be hidden or disabled depending on the toggle value.

C#

In C# ToggleGroup should be used on a serialized bool field.

Parameter Description
string path Path to the group (see Nesting Groups).
bool foldable Makes the group collapsible. Default: false.
bool box Puts the group inside a box. Default: true.
float space Adds space before the group. Default: 0.
[ToggleGroup("Toggle Group")]
public bool boolean;
public int one;
public int two;
public int three;

[ToggleGroup("Foldable Toggle Group", foldable: true)]
public bool anotherBoolean;
public int four;
public int five;
public int six;

ShaderLab

In ShaderLab ToggleGroup should be used on a float property.

Parameter Description
string path Path to the group (see Nesting Groups).
bool foldable Makes the group collapsible. Default: false.
bool box Puts the group inside a box. Default: true.
string keyword (optional) Keyword to turn on and off (like the built-in Toggle drawer).
[ToggleGroup(Toggle_Group)]
_Toggle("Toggle", Float) = 0
_One("One", Float) = 0
_Two("Two", Float) = 0
_Three("Three", Float) = 0

[ToggleGroup(Toggle_Group_With_Keyword, true, MY_KEYWORD)]
_AnotherToggle("Another Toggle", Float) = 0
_Four("Four", Float) = 0
_Five("Five", Float) = 0
_Six("Six", Float) = 0

Special Attributes

MarkedUpType

C# only

Makes attributes work inside serializable classes and structs. See Usage: Serializable classes and structs. Can optionally hide the target's control (foldout) and remove indent from target's children.

[MarkedUpType]
class SomeClass
{
    ...
}

[MarkedUpType(indentChildren: false)]
struct SomeStruct
{
    ...
}

MarkedUpField

C# only

Makes attributes work inside fields of serializable classes and structs. See Usage: Serializable classes and structs. Can optionally hide the target's control (foldout) and remove indent from target's children.

[MarkedUpField]
public SomeClass one;

[MarkedUpField(indentChildren: false)]
public SomeClass two;

[MarkedUpField(indentChildren: false, showControl: false)]
public SomeClass three;

InlineEditor

C# only

Shows the inspector of some Unity.Object (MonoBehaviour, ScripatableObject and Material are Unity.Objects, for instance) "inline" — embeds it in the current inspector. Can be used on an object reference field. Works in MarkedUpEditors and MarkedUpFields inside them.

Parameter Description
InlineEditorMode mode Box Shows object field and inspector body inside a foldable box.
ContentBox Shows object field with foldable inspector body.
Stripped Shows only inspector body.
[InlineEditor]
public SomeData someData;

[InlineEditor(InlineEditorMode.ContentBox)]
public SomeComponent someComponent;

[TitleGroup("Stripped")]
[InlineEditor(InlineEditorMode.Stripped)]
public SomeData stripped1;

DrawSystemProperties

ShaderLab only

Tells MarkedUpShaderGUI to draw Render Queue, Enable Instancing (if applicable) and Double Sided Global Illumination properties below this property.

[DrawSystemProperties]
_SomeProperty("Some Property", Float) = 0

Custom Marked Up Inspectors

Regular Inspectors

MarkedUpEditor allows to inject custom code into itself before, after and instead of any property. Here are the methods used for extension:

  • protected bool DrawMarkedUpInspector()

    Works like Editor's DrawDefaultInspector, but for the marked up editor. Call it if you have overridden the OnInspectorGUI and want to draw MarkedUpEditor as is.

  • protected virtual void OnInitialize()

    Is called after MarkedUpEditor have initialized itself in OnEnable.

  • protected void AddCallback(SerializedProperty property, CallbackEvent type, Action<SerializedProperty> callback)

    Adds a callback to a specified SerializedProperty at a specified CallbackEvent. Only one callback can be added for a given property at a given event. Should be used in OnInitialize.

  • protected virtual void OnCleanup()

    Is called before MarkedUpEditor started cleanup in OnDisable.

using UnityEditor;
using UnityEngine;
using MarkupAttributes.Editor;

[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : MarkedUpEditor
{
    protected override void OnInitialize()
    {
        AddCallback(serializedObject.FindProperty("one"), 
            CallbackEvent.AfterProperty, ButtonAfterOne);

        AddCallback(serializedObject.FindProperty("six"),
            CallbackEvent.BeforeProperty, ButtonBeforeSix);

        AddCallback(serializedObject.FindProperty("three"),
            CallbackEvent.ReplaceProperty, ButtonReplaceThree);
    }

    private void ButtonAfterOne(SerializedProperty property)
    {
        GUILayout.Button("After One");
    }

    private void ButtonBeforeSix(SerializedProperty property)
    {
        GUILayout.Button("Before Six");
    }

    private void ButtonReplaceThree(SerializedProperty property)
    {
        GUILayout.Button("Replace Three");
    }
}

Material Inspectors

MarkedUpShaderGUI can be extended in a similar manner. It provides the following methods:

  • protected virtual void OnInitialize(MaterialEditor materialEditor, MaterialProperty[] properties)

    Is called after MarkedUpShaderGUI have initialized itself. AddCallback should be used here.

  • protected void AddCallback(MaterialProperty property, CallbackEvent type, Action<MaterialEditor, MaterialProperty[], MaterialProperty> callback)

    Adds a callback to a specified MaterialProperty at a specified CallbackEvent. Only one callback can be added for a given property at a given event. Should be used in OnInitialize.

using UnityEditor;
using UnityEngine;
using MarkupAttributes.Editor;

public class MyShaderEditor : MarkedUpShaderGUI
{
    protected override void OnInitialize(MaterialEditor materialEditor, MaterialProperty[] properties)
    {
        AddCallback(FindProperty("_Color", properties), CallbackEvent.AfterProperty, ButtonAfterColor);
    }

    private void ButtonAfterColor(MaterialEditor materialEditor, MaterialProperty[] properties,
        MaterialProperty property)
    {
        GUILayout.Button("Button After Color");
    }
}

Don't forget to tell Unity to use the modified editor for your shader.

Shader "Unlit/MyShader"
{
    Properties
    {
        ...
    }

    CustomEditor "MyShaderEditor"
    
    ...
}

MarkupGUI

Class MarkupGUI exposes methods for creating Markup Attributes styled groups. They can be useful for making EditorWindows or custom inspectors, that don't use attributes themselves.

using UnityEditor;
using MarkupAttributes.Editor;

public class CustomWindow : EditorWindow
{
    MarkupGUI.GroupsStack groupsStack = new MarkupGUI.GroupsStack();
    int activeTab = 0;
    bool foldout = true;

    public static void Open()
    {
        CustomWindow window = (CustomWindow)GetWindow(typeof(CustomWindow), 
            false, "Custom Window Sample");
        window.Show();
    }

    void OnGUI()
    {
        // Always clear the groups stack.
        groupsStack.Clear();

        groupsStack += MarkupGUI.BeginBoxGroup("Box");
        // group contents ...
        groupsStack.EndGroup();

        groupsStack += MarkupGUI.BeginTitleGroup("TitleGroup", true);
        // group contents ...
        groupsStack.EndGroup();

        groupsStack += MarkupGUI.BeginFoldoutGroup(ref foldout, "Foldout");
        // group contents ...
        groupsStack.EndGroup();

        groupsStack += MarkupGUI.BeginTabsGroup(ref activeTab, 
            new string[] { "Left", "Middle", "Right" }, true);
        // group contents ...

        // Always end all groups at the end.
        groupsStack.EndAll();
    }
}

About

A Unity Editor extension for customizing inspector layout with attributes.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages