Property drawer for enum flags/masks (download)

Hi guys, I’d like to share with you a little script I made that adds an attribute called EnumMask that you can add to an enum field. Then that enum field will be displayed differently in the editor (see the screenshot) that makes it way easier to edit than a regular mask field.

Screenshot

Example:

[System.Flags]
public enum BlockSides
{
    Left    = (1 << 0),
    Right   = (1 << 1),
    Front   = (1 << 2),
    Back    = (1 << 3),
    Up      = (1 << 4),
    Down    = (1 << 5)
}

[EnumMask] BlockSides attachableSides;

All you’ve got to do is copy the source code from here and add it to a C# source file (outside of the Editor folder). Or, if you prefer, download the .cs file I attached to this post and add it to your project.

EDIT: As pointed out by Madgvox here this property drawer did not support “holes” in the flags values (check out the link so you understand what I mean) and I also found out it didn’t support types such as long, ulong, short, ushort, byte and sbyte. All of these issues have been fixed and both the source code and the file for download have been updated.

EDIT: As @Keepabee pointed out here this property drawer did not support “None” and “All” elements (elements with values 0 and ~0, respectively). This issue has been fixed and the source code and file for download have been updated.

Source code

/*
 Written by: Lucas Antunes (aka ItsaMeTuni), lucasba8@gmail.com
 In: 2/15/2018
 The only thing that you cannot do with this script is sell it by itself without substantially modifying it.
 */

using System;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
[CustomPropertyDrawer(typeof(EnumMaskAttribute))]
public class EnumMaskPropertyDrawer : PropertyDrawer
{
    bool foldoutOpen = false;

    object theEnum;
    Array enumValues;
    Type enumUnderlyingType;

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        if (foldoutOpen)
            return EditorGUIUtility.singleLineHeight * (Enum.GetValues(fieldInfo.FieldType).Length + 2);
        else
            return EditorGUIUtility.singleLineHeight;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        theEnum = fieldInfo.GetValue(property.serializedObject.targetObject);
        enumValues = Enum.GetValues(theEnum.GetType());
        enumUnderlyingType = Enum.GetUnderlyingType(theEnum.GetType());

        //We need to convert the enum to its underlying type, if we don't it will be boxed
        //into an object later and then we would need to unbox it like (UnderlyingType)(EnumType)theEnum.
        //If we do this here we can just do (UnderlyingType)theEnum later (plus we can visualize the value of theEnum in VS when debugging)
        theEnum = Convert.ChangeType(theEnum, enumUnderlyingType);

        EditorGUI.BeginProperty(position, label, property);

        foldoutOpen = EditorGUI.Foldout(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), foldoutOpen, label);

        if (foldoutOpen)
        {
            //Draw the All button
            if (GUI.Button(new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight * 1, 30, 15), "All"))
            {
                theEnum = DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType);
            }

            //Draw the None button
            if (GUI.Button(new Rect(position.x + 32, position.y + EditorGUIUtility.singleLineHeight * 1, 40, 15), "None"))
            {
                theEnum = Convert.ChangeType(0, enumUnderlyingType);
            }

            //Draw the list
            for (int i = 0; i < Enum.GetNames(fieldInfo.FieldType).Length; i++)
            {
                if (EditorGUI.Toggle(new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight * (2 + i), position.width, EditorGUIUtility.singleLineHeight), Enum.GetNames(fieldInfo.FieldType)[i], IsSet(i)))
                {
                    ToggleIndex(i, true);
                }
                else
                {
                    ToggleIndex(i, false);
                }
            }
        }

        fieldInfo.SetValue(property.serializedObject.targetObject, theEnum);
        property.serializedObject.ApplyModifiedProperties();
    }

    /// <summary>
    /// Get the value of an enum element at the specified index (i.e. at the index of the name of the element in the names array)
    /// </summary>
    object GetEnumValue(int _index)
    {
        return Convert.ChangeType(enumValues.GetValue(_index), enumUnderlyingType);
    }

    /// <summary>
    /// Sets or unsets a bit in theEnum based on the index of the enum element (i.e. the index of the element in the names array)
    /// </summary>
    /// <param name="_set">If true the flag will be set, if false the flag will be unset.</param>
    void ToggleIndex(int _index, bool _set)
    {
        if(_set)
        {
            if(IsNoneElement(_index))
            {
                theEnum = Convert.ChangeType(0, enumUnderlyingType);
            }

            //enum = enum | val
            theEnum = DoOrOperator(theEnum, GetEnumValue(_index), enumUnderlyingType);
        }
        else
        {
            if (IsNoneElement(_index) || IsAllElement(_index))
            {
                return;
            }

            object val = GetEnumValue(_index);
            object notVal = DoNotOperator(val, enumUnderlyingType);

            //enum = enum & ~val
            theEnum = DoAndOperator(theEnum, notVal, enumUnderlyingType);
        }

    }

    /// <summary>
    /// Checks if a bit flag is set at the provided index of the enum element (i.e. the index of the element in the names array)
    /// </summary>
    bool IsSet(int _index)
    {
        object val = DoAndOperator(theEnum, GetEnumValue(_index), enumUnderlyingType);

        //We handle All and None elements differently, since they're "special"
        if(IsAllElement(_index))
        {
            //If all other bits visible to the user (elements) are set, the "All" element checkbox has to be checked
            //We don't do a simple AND operation because there might be missing bits.
            //e.g. An enum with 6 elements including the "All" element. If we set all bits visible except the "All" bit,
            //two bits might be unset. Since we want the "All" element checkbox to be checked when all other elements are set
            //we have to make sure those two extra bits are also set.
            bool allSet = true;
            for (int i = 0; i < Enum.GetNames(fieldInfo.FieldType).Length; i++)
            {
                if(i != _index && !IsNoneElement(i) && !IsSet(i))
                {
                    allSet = false;
                    break;
                }
            }

            //Make sure all bits are set if all "visible bits" are set
            if(allSet)
            {
                theEnum = DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType);
            }

            return allSet;
        }
        else if (IsNoneElement(_index))
        {
            //Just check the "None" element checkbox our enum's value is 0
            return Convert.ChangeType(theEnum, enumUnderlyingType).Equals(Convert.ChangeType(0, enumUnderlyingType));
        }

        return !val.Equals(Convert.ChangeType(0, enumUnderlyingType));
    }

    /// <summary>
    /// Call the bitwise OR operator (|) on _lhs and _rhs given their types.
    /// Will basically return _lhs | _rhs
    /// </summary>
    /// <param name="_lhs">Left-hand side of the operation.</param>
    /// <param name="_rhs">Right-hand side of the operation.</param>
    /// <param name="_type">Type of the objects.</param>
    /// <returns>Result of the operation</returns>
    static object DoOrOperator(object _lhs, object _rhs, Type _type)
    {
        if(_type == typeof(int))
        {
            return ((int)_lhs) | ((int)_rhs);
        }
        else if (_type == typeof(uint))
        {
            return ((uint)_lhs) | ((uint)_rhs);
        }
        else if (_type == typeof(short))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((short)((short)_lhs | (short)_rhs));
        }
        else if (_type == typeof(ushort))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((ushort)((ushort)_lhs | (ushort)_rhs));
        }
        else if (_type == typeof(long))
        {
            return ((long)_lhs) | ((long)_rhs);
        }
        else if (_type == typeof(ulong))
        {
            return ((ulong)_lhs) | ((ulong)_rhs);
        }
        else if (_type == typeof(byte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((byte)((byte)_lhs | (byte)_rhs));
        }
        else if (_type == typeof(sbyte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((sbyte)((sbyte)_lhs | (sbyte)_rhs));
        }
        else
        {
            throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
        }
    }

    /// <summary>
    /// Call the bitwise AND operator (&) on _lhs and _rhs given their types.
    /// Will basically return _lhs & _rhs
    /// </summary>
    /// <param name="_lhs">Left-hand side of the operation.</param>
    /// <param name="_rhs">Right-hand side of the operation.</param>
    /// <param name="_type">Type of the objects.</param>
    /// <returns>Result of the operation</returns>
    static object DoAndOperator(object _lhs, object _rhs, Type _type)
    {
        if (_type == typeof(int))
        {
            return ((int)_lhs) & ((int)_rhs);
        }
        else if (_type == typeof(uint))
        {
            return ((uint)_lhs) & ((uint)_rhs);
        }
        else if (_type == typeof(short))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((short)((short)_lhs & (short)_rhs));
        }
        else if (_type == typeof(ushort))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((ushort)((ushort)_lhs & (ushort)_rhs));
        }
        else if (_type == typeof(long))
        {
            return ((long)_lhs) & ((long)_rhs);
        }
        else if (_type == typeof(ulong))
        {
            return ((ulong)_lhs) & ((ulong)_rhs);
        }
        else if (_type == typeof(byte))
        {
            return unchecked((byte)((byte)_lhs & (byte)_rhs));
        }
        else if (_type == typeof(sbyte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((sbyte)((sbyte)_lhs & (sbyte)_rhs));
        }
        else
        {
            throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
        }
    }

    /// <summary>
    /// Call the bitwise NOT operator (~) on _lhs given its type.
    /// Will basically return ~_lhs
    /// </summary>
    /// <param name="_lhs">Left-hand side of the operation.</param>
    /// <param name="_type">Type of the object.</param>
    /// <returns>Result of the operation</returns>
    static object DoNotOperator(object _lhs, Type _type)
    {
        if (_type == typeof(int))
        {
            return ~(int)_lhs;
        }
        else if (_type == typeof(uint))
        {
            return ~(uint)_lhs;
        }
        else if (_type == typeof(short))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((short)~(short)_lhs);
        }
        else if (_type == typeof(ushort))
        {

            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((ushort)~(ushort)_lhs);
        }
        else if (_type == typeof(long))
        {
            return ~(long)_lhs;
        }
        else if (_type == typeof(ulong))
        {
            return ~(ulong)_lhs;
        }
        else if (_type == typeof(byte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return (byte)~(byte)_lhs;
        }
        else if (_type == typeof(sbyte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((sbyte)~(sbyte)_lhs);
        }
        else
        {
            throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
        }
    }

    /// <summary>
    /// Check if the element of specified index is a "None" element (all bits unset, value = 0).
    /// </summary>
    /// <param name="_index">Index of the element.</param>
    /// <returns>If the element has all bits unset or not.</returns>
    bool IsNoneElement(int _index)
    {
        return GetEnumValue(_index).Equals(Convert.ChangeType(0, enumUnderlyingType));
    }

    /// <summary>
    /// Check if the element of specified index is an "All" element (all bits set, value = ~0).
    /// </summary>
    /// <param name="_index">Index of the element.</param>
    /// <returns>If the element has all bits set or not.</returns>
    bool IsAllElement(int _index)
    {
        object elemVal = GetEnumValue(_index);
        return elemVal.Equals(DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType));
    }
}
#endif

[System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false)]
public class EnumMaskAttribute : PropertyAttribute
{

}

I hope this helps someone!

3392975–356650–EnumMaskPropertyDrawer.cs (12.9 KB)

10 Likes

Alternatively you can use the MaskField to display like a multi select dropdown

    public override void OnGUI(Rect position,
                                SerializedProperty property,
                                GUIContent label)
    {
        EditorGUI.BeginChangeCheck();
        uint a = (uint)(EditorGUI.MaskField(position, label, property.intValue, property.enumNames));
        if (EditorGUI.EndChangeCheck())
        {
            property.intValue = (int)a;
        }
    }
[code]
3 Likes

Yes, but mask fields suck (a lot). You can’t see what the value is until you click it so it shows the popup and if you need to set a lot of items in it it’s going to take you a lot of time.

1 Like

Yeah, I agree that the “mixed…” isn’t very nice. Its extra annoying because layermask types work nicely. i.e. instead of mixed, it will say: layer1, layer2, layer3

but fair enough on your other points

Does this script go in the editor folder? Doesn’t seem to work for me i can’t use the attribute it doesn’t appear to exist.

Unfortunately it seems that this script suffers from the same core flaw that Unity’s own FlagsField does.

By iterating on the names only, and using the index as the enum value, you make it impossible to use enums that have gaps in their values (a situation that is very possible to occur):

[Flags]
enum MyEnum {
  Foo = 0x1,
  Bar = 0x2,
 
  Baz = 0x8 // <-- skips 0x4
}

You should be iterating over Enum.GetValues instead of creating your own.

1 Like

I haven’t thought about this at the time I wrote this script, neither I thought that the user could want to use a ushort, a long, a ulong, etc instead of an int as the enum subtype.

Just to let you know I am working on solving this. Solving skipped bits was easy, the thing is that I encountered a very odd error that for some reason ~(ushort)1 is showing up in VS as -2 (which shouldn’t even be possible) and throwing an exception because of that…

1 Like

~1 is two’s complement -2:

11111111 11111110

My first guess is that something’s converting the ushort back to a signed value somewhere.

I asked a question in StackOverflow about this here and it turns out that short and ushort don’t have bitwise operators so they are converted to int or uint before the operation. I won’t be finishing the fix now as I’m busy at the moment.

@Madgvox I fixed it and updated the post with the working source and file for download :slight_smile:
I had to make some wizardry to make it work XD but I really appreciate that you replied to this thread since fixing this was fun and made me learn a bit more about C#.

1 Like

Sure, no problem!

Thank you for the script @ItsaMeTuni !!
It works really well with byte enums but I was wondering why the behavior seems to break when I try to use an Int enum with more than 8 (including none/all) values? An Int shouldn’t have more that 8 bits “usable” in it?
I’m not well versed in bit operations so a bit of an explanation would be awesome!
Thank you in advance!

What behavior breaks? Is it the editor drawer or your functionality? I made an enum with more than 8 elements with an int as underlying type, and the editor drawer works as expected. If it is your functionality that is breaking: 1) it has nothing to do with this drawer, since it only works in the editor and doesn’t even touch your enum (you just mark it as a Flags enum so the editor recognizes it and displays the appropriate drawer); and 2) if you’re not familiar with enum masks/flags (they’re the same thing) you might want to take a look at this article I wrote here.

Ok, I just tried on an empty project to show you the issue and… the issue disappeared haha!
So nevermind, your script is just perfect! Thank you so much for sharing :slight_smile:
I’ll read your article, I really need to learn from it.

No problem :slight_smile: If you still need help (or have any questions), create a new thread and be sure to mention me so I can help you!

The script seems to work very nicely as long as the enums only define values, but seemed to break if values for “None” or “All” are defined in the enum (values 0 and ~0) declaration. Didn’t have time to figure that out myself yet, but just mentioning it here if Tuni or someone else wants to take a look at fixing it.

Also in my current layout the default values for layout were slightly off so I did some minor customizations to enlarge the “All” and “None” button rects so the word “None” didn’t get cut off.

A lovely script, lots of thanks!

@Keepabee I just fixed it! Took me some time and a lot of debugging, but I was able to fix it. Post is updated with fixed code. Thanks for pointing it out! :slight_smile:

1 Like

Thank you!
I am trying to use an enum in a nested class, but it dont works for me.

public class TeamManager : MonoBehaviour {

    [System.Serializable]
    public class Team {
        public TeamName name;
        public Color color = Color.white;
        public Sprite teamIcon;

        [EnumMask]
        public TeamName enemyTeams;
    }

    [SerializeField]
    private List<Team> teams;
}

[System.Flags]
public enum TeamName {
    Neutral = (1 << 0),
    A = (1 << 1),
    B = (1 << 2),
    C = (1 << 3)
}

When I expand the list, the console shows the following exception:
Show Console Exception…

ArgumentException: Field enemyTeams defined on type TeamManager+Team is not a field on the target object which is of type TeamManager.
Parameter name: obj
System.Reflection.MonoField.GetValue (System.Object obj) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Reflection/MonoField.cs:110)
EnumMaskPropertyDrawer.OnGUI (Rect position, UnityEditor.SerializedProperty property, UnityEngine.GUIContent label) (at Assets/Scripts/Utility/EnumMaskPropertyDrawer.cs:31)
UnityEditor.PropertyDrawer.OnGUISafe (Rect position, UnityEditor.SerializedProperty property, UnityEngine.GUIContent label) (at C:/buildslave/unity/build/Editor/Mono/ScriptAttributeGUI/PropertyDrawer.cs:22)
UnityEditor.PropertyHandler.OnGUI (Rect position, UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, Boolean includeChildren) (at C:/buildslave/unity/build/Editor/Mono/ScriptAttributeGUI/PropertyHandler.cs:142)
UnityEditor.EditorGUI.PropertyFieldInternal (Rect position, UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, Boolean includeChildren) (at C:/buildslave/unity/build/Editor/Mono/EditorGUI.cs:5225)
UnityEditor.EditorGUI.PropertyField (Rect position, UnityEditor.SerializedProperty property, Boolean includeChildren) (at C:/buildslave/unity/build/artifacts/generated/common/editor/EditorGUIBindings.gen.cs:1034)
UnityEditor.EditorGUI.PropertyField (Rect position, UnityEditor.SerializedProperty property) (at C:/buildslave/unity/build/artifacts/generated/common/editor/EditorGUIBindings.gen.cs:1029)
UnityEditor.Editor.OptimizedInspectorGUIImplementation (Rect contentRect) (at C:/buildslave/unity/build/artifacts/generated/common/editor/EditorBindings.gen.cs:273)
UnityEditor.GenericInspector.OnOptimizedInspectorGUI (Rect contentRect) (at C:/buildslave/unity/build/Editor/Mono/Inspector/GenericInspector.cs:32)
UnityEditor.InspectorWindow.DrawEditor (UnityEditor.Editor editor, Int32 editorIndex, Boolean rebuildOptimizedGUIBlock, System.Boolean& showImportedObjectBarNext, UnityEngine.Rect& importedObjectBarRect) (at C:/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:1220)
UnityEditor.InspectorWindow.DrawEditors (UnityEditor.Editor[ ] editors) (at C:/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:1030)
UnityEditor.InspectorWindow.OnGUI () (at C:/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:359)
System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[ ] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:222)

1 Like

This has helped me a lot and should be included in Unity by default. Thanks a lot

I found this through Google, and there’s some issues I’ve fixed. Great starting point, though, @ItsaMeTuni

As @Noxury points out, this doesn’t work when used in [Serializable] classes that’s nested inside something. This is because the enum value is gotten and later written to like this:

theEnum = fieldInfo.GetValue(property.serializedObject.targetObject);
// later
fieldInfo.SetValue(property.serializedObject.targetObject, theEnum);

I added a fallback when that fails to assume that the enum is a part of a nested value, and used the property path to get the actual property containing the enum filed. Then I grabbed the object behind that field - I believe that code originally comes from @lordofduct 's SpacePuppy framework.

object targetObject;
try {
    targetObject = property.serializedObject.targetObject;
    theEnum = fieldInfo.GetValue(targetObject);
}
catch (ArgumentException ) {
    targetObject = GetTargetObjectOfProperty(GetParentProperty(property));
    theEnum = fieldInfo.GetValue(targetObject);
}

// later:
fieldInfo.SetValue(targetObject, theEnum);

There’s another bug where if you’re looking at several of these enum mask flag fields, they might get folded out at the same time - especially when they’re in an array. This is because Unity reuses property drawers. I’m not quite sure how exactly they’re shared, but I fixed the issue by adding a Dictionary to hold the foldout value, where the key is the property path of the property.

I also fixed the None-button being too small, and the All/None buttons not being indented according to IndentLevel. (that’s an internal bug in Unity that you have to work around every time).

Finally, I added an option to lay out the enum members horizontally instead of vertically, and an option to always fold out (and not show the foldout).

EDIT 2020: Latest version of this should be here.

Attribute Code:

using System;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class EnumMaskAttribute : PropertyAttribute
{
    public bool alwaysFoldOut;
    public EnumMaskLayout layout = EnumMaskLayout.Vertical;
}

public enum EnumMaskLayout
{
    Vertical,
    Horizontal
}

Drawer Code:

/*
Written by: Lucas Antunes (aka ItsaMeTuni), lucasba8@gmail.com
In: 2/15/2018
The only thing that you cannot do with this script is sell it by itself without substantially modifying it.

Updated by Baste Nesse Buanes, baste@rain-games.com (thanks to @lordofduct for GetTargetOfProperty implementation)
06-Sep-2019
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

using UnityEditor;
[CustomPropertyDrawer(typeof(EnumMaskAttribute))]
public class EnumMaskPropertyDrawer : PropertyDrawer
{
    Dictionary<string, bool> openFoldouts = new Dictionary<string, bool>();

    object theEnum;
    Array enumValues;
    Type enumUnderlyingType;

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        var enumMaskAttribute = ((EnumMaskAttribute) attribute);
        var foldoutOpen = enumMaskAttribute.alwaysFoldOut;

        if (!foldoutOpen)
        {
            if (!openFoldouts.TryGetValue(property.propertyPath, out foldoutOpen))
            {
                openFoldouts[property.propertyPath] = false;
            }
        }
        if (foldoutOpen)
        {
            var layout = ((EnumMaskAttribute )attribute).layout;
            if (layout == EnumMaskLayout.Vertical)
                return EditorGUIUtility.singleLineHeight * (Enum.GetValues(fieldInfo.FieldType).Length + 2);
            else
                return EditorGUIUtility.singleLineHeight * 3;
        }
        else
            return EditorGUIUtility.singleLineHeight;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        object targetObject;
        try {
            targetObject = property.serializedObject.targetObject;
            theEnum = fieldInfo.GetValue(targetObject);
        }
        catch (ArgumentException ) {
            targetObject = GetTargetObjectOfProperty(GetParentProperty(property));
            theEnum = fieldInfo.GetValue(targetObject);
        }

        enumValues = Enum.GetValues(theEnum.GetType());
        enumUnderlyingType = Enum.GetUnderlyingType(theEnum.GetType());

        //We need to convert the enum to its underlying type, if we don't it will be boxed
        //into an object later and then we would need to unbox it like (UnderlyingType)(EnumType)theEnum.
        //If we do this here we can just do (UnderlyingType)theEnum later (plus we can visualize the value of theEnum in VS when debugging)
        theEnum = Convert.ChangeType(theEnum, enumUnderlyingType);

        EditorGUI.BeginProperty(position, label, property);

        var enumMaskAttribute = ((EnumMaskAttribute) attribute);
        var alwaysFoldOut = enumMaskAttribute.alwaysFoldOut;
        var foldoutOpen = alwaysFoldOut;

        if (alwaysFoldOut) {
            EditorGUI.LabelField(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), label);
        }
        else {
            if (!openFoldouts.TryGetValue(property.propertyPath, out foldoutOpen)) {
                openFoldouts[property.propertyPath] = false;
            }

            EditorGUI.BeginChangeCheck();
            foldoutOpen = EditorGUI.Foldout(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), foldoutOpen, label);

            if (EditorGUI.EndChangeCheck())
                openFoldouts[property.propertyPath] =  foldoutOpen;
        }

        if (foldoutOpen)
        {
            //Draw the All button
            if (GUI.Button(new Rect(position.x + (15f * EditorGUI.indentLevel), position.y + EditorGUIUtility.singleLineHeight * 1, 30, 15), "All"))
            {
                theEnum = DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType);
            }

            //Draw the None button
            if (GUI.Button(new Rect(position.x + 32 + (15f * EditorGUI.indentLevel), position.y + EditorGUIUtility.singleLineHeight * 1, 50, 15), "None"))
            {
                theEnum = Convert.ChangeType(0, enumUnderlyingType);
            }

            var layout = enumMaskAttribute.layout;

            if (layout == EnumMaskLayout.Vertical)
            {
                //Draw the list vertically
                for (int i = 0; i < Enum.GetNames(fieldInfo.FieldType).Length; i++)
                {
                    if (EditorGUI.Toggle(new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight * (2 + i), position.width, EditorGUIUtility.singleLineHeight), Enum.GetNames(fieldInfo.FieldType)[i], IsSet(i)))
                    {
                        ToggleIndex(i, true);
                    }
                    else
                    {
                        ToggleIndex(i, false);
                    }
                }
            }
            else
            {
                var enumNames = Enum.GetNames(fieldInfo.FieldType);

                var style = new GUIStyle(GUI.skin.label) {
                    alignment = TextAnchor.MiddleRight,
                    clipping = TextClipping.Overflow
                };

                //Draw the list horizontally
                var labelWidth = 50f;
                for (int i = 0; i < enumNames.Length; i++)
                    labelWidth = Mathf.Max(labelWidth, GUI.skin.label.CalcSize(new GUIContent(enumNames[i])).x);
                var toggleWidth = labelWidth + 20;

                var oldLabelWidth = EditorGUIUtility.labelWidth;
                var oldIndentLevel = EditorGUI.indentLevel; // Toggles kinda are broken at non-zero indent levels, as the indentation eats a part of the clickable rect.

                EditorGUIUtility.labelWidth = labelWidth;
                EditorGUI.indentLevel = 0;

                position.width = toggleWidth;
                position.y += + EditorGUIUtility.singleLineHeight * 2;
                var xBase = position.x + oldIndentLevel * 15f;
                for (int i = 0; i < enumNames.Length; i++)
                {
                    position.x = xBase + (i * position.width);
                    var togglePos = EditorGUI.PrefixLabel(position, new GUIContent(enumNames[i]), style);
                    if (EditorGUI.Toggle(togglePos, IsSet(i)))
                    {
                        ToggleIndex(i, true);
                    }
                    else
                    {
                        ToggleIndex(i, false);
                    }
                }

                EditorGUIUtility.labelWidth = oldLabelWidth;
                EditorGUI.indentLevel = oldIndentLevel;
            }
        }

        property.intValue = (int) theEnum;
    }

    /// <summary>
    /// Get the value of an enum element at the specified index (i.e. at the index of the name of the element in the names array)
    /// </summary>
    object GetEnumValue(int _index)
    {
        return Convert.ChangeType(enumValues.GetValue(_index), enumUnderlyingType);
    }

    /// <summary>
    /// Sets or unsets a bit in theEnum based on the index of the enum element (i.e. the index of the element in the names array)
    /// </summary>
    /// <param name="_set">If true the flag will be set, if false the flag will be unset.</param>
    void ToggleIndex(int _index, bool _set)
    {
        if(_set)
        {
            if(IsNoneElement(_index))
            {
                theEnum = Convert.ChangeType(0, enumUnderlyingType);
            }

            //enum = enum | val
            theEnum = DoOrOperator(theEnum, GetEnumValue(_index), enumUnderlyingType);
        }
        else
        {
            if (IsNoneElement(_index) || IsAllElement(_index))
            {
                return;
            }

            object val = GetEnumValue(_index);
            object notVal = DoNotOperator(val, enumUnderlyingType);

            //enum = enum & ~val
            theEnum = DoAndOperator(theEnum, notVal, enumUnderlyingType);
        }

    }

    /// <summary>
    /// Checks if a bit flag is set at the provided index of the enum element (i.e. the index of the element in the names array)
    /// </summary>
    bool IsSet(int _index)
    {
        object val = DoAndOperator(theEnum, GetEnumValue(_index), enumUnderlyingType);

        //We handle All and None elements differently, since they're "special"
        if(IsAllElement(_index))
        {
            //If all other bits visible to the user (elements) are set, the "All" element checkbox has to be checked
            //We don't do a simple AND operation because there might be missing bits.
            //e.g. An enum with 6 elements including the "All" element. If we set all bits visible except the "All" bit,
            //two bits might be unset. Since we want the "All" element checkbox to be checked when all other elements are set
            //we have to make sure those two extra bits are also set.
            bool allSet = true;
            for (int i = 0; i < Enum.GetNames(fieldInfo.FieldType).Length; i++)
            {
                if(i != _index && !IsNoneElement(i) && !IsSet(i))
                {
                    allSet = false;
                    break;
                }
            }

            //Make sure all bits are set if all "visible bits" are set
            if(allSet)
            {
                theEnum = DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType);
            }

            return allSet;
        }
        else if (IsNoneElement(_index))
        {
            //Just check the "None" element checkbox our enum's value is 0
            return Convert.ChangeType(theEnum, enumUnderlyingType).Equals(Convert.ChangeType(0, enumUnderlyingType));
        }

        return !val.Equals(Convert.ChangeType(0, enumUnderlyingType));
    }

    /// <summary>
    /// Call the bitwise OR operator (|) on _lhs and _rhs given their types.
    /// Will basically return _lhs | _rhs
    /// </summary>
    /// <param name="_lhs">Left-hand side of the operation.</param>
    /// <param name="_rhs">Right-hand side of the operation.</param>
    /// <param name="_type">Type of the objects.</param>
    /// <returns>Result of the operation</returns>
    static object DoOrOperator(object _lhs, object _rhs, Type _type)
    {
        if(_type == typeof(int))
        {
            return ((int)_lhs) | ((int)_rhs);
        }
        else if (_type == typeof(uint))
        {
            return ((uint)_lhs) | ((uint)_rhs);
        }
        else if (_type == typeof(short))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((short)((short)_lhs | (short)_rhs));
        }
        else if (_type == typeof(ushort))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((ushort)((ushort)_lhs | (ushort)_rhs));
        }
        else if (_type == typeof(long))
        {
            return ((long)_lhs) | ((long)_rhs);
        }
        else if (_type == typeof(ulong))
        {
            return ((ulong)_lhs) | ((ulong)_rhs);
        }
        else if (_type == typeof(byte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((byte)((byte)_lhs | (byte)_rhs));
        }
        else if (_type == typeof(sbyte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((sbyte)((sbyte)_lhs | (sbyte)_rhs));
        }
        else
        {
            throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
        }
    }

    /// <summary>
    /// Call the bitwise AND operator (&) on _lhs and _rhs given their types.
    /// Will basically return _lhs & _rhs
    /// </summary>
    /// <param name="_lhs">Left-hand side of the operation.</param>
    /// <param name="_rhs">Right-hand side of the operation.</param>
    /// <param name="_type">Type of the objects.</param>
    /// <returns>Result of the operation</returns>
    static object DoAndOperator(object _lhs, object _rhs, Type _type)
    {
        if (_type == typeof(int))
        {
            return ((int)_lhs) & ((int)_rhs);
        }
        else if (_type == typeof(uint))
        {
            return ((uint)_lhs) & ((uint)_rhs);
        }
        else if (_type == typeof(short))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((short)((short)_lhs & (short)_rhs));
        }
        else if (_type == typeof(ushort))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((ushort)((ushort)_lhs & (ushort)_rhs));
        }
        else if (_type == typeof(long))
        {
            return ((long)_lhs) & ((long)_rhs);
        }
        else if (_type == typeof(ulong))
        {
            return ((ulong)_lhs) & ((ulong)_rhs);
        }
        else if (_type == typeof(byte))
        {
            return unchecked((byte)((byte)_lhs & (byte)_rhs));
        }
        else if (_type == typeof(sbyte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((sbyte)((sbyte)_lhs & (sbyte)_rhs));
        }
        else
        {
            throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
        }
    }

    /// <summary>
    /// Call the bitwise NOT operator (~) on _lhs given its type.
    /// Will basically return ~_lhs
    /// </summary>
    /// <param name="_lhs">Left-hand side of the operation.</param>
    /// <param name="_type">Type of the object.</param>
    /// <returns>Result of the operation</returns>
    static object DoNotOperator(object _lhs, Type _type)
    {
        if (_type == typeof(int))
        {
            return ~(int)_lhs;
        }
        else if (_type == typeof(uint))
        {
            return ~(uint)_lhs;
        }
        else if (_type == typeof(short))
        {
            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((short)~(short)_lhs);
        }
        else if (_type == typeof(ushort))
        {

            //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((ushort)~(ushort)_lhs);
        }
        else if (_type == typeof(long))
        {
            return ~(long)_lhs;
        }
        else if (_type == typeof(ulong))
        {
            return ~(ulong)_lhs;
        }
        else if (_type == typeof(byte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return (byte)~(byte)_lhs;
        }
        else if (_type == typeof(sbyte))
        {
            //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
            return unchecked((sbyte)~(sbyte)_lhs);
        }
        else
        {
            throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
        }
    }

    /// <summary>
    /// Check if the element of specified index is a "None" element (all bits unset, value = 0).
    /// </summary>
    /// <param name="_index">Index of the element.</param>
    /// <returns>If the element has all bits unset or not.</returns>
    bool IsNoneElement(int _index)
    {
        return GetEnumValue(_index).Equals(Convert.ChangeType(0, enumUnderlyingType));
    }

    /// <summary>
    /// Check if the element of specified index is an "All" element (all bits set, value = ~0).
    /// </summary>
    /// <param name="_index">Index of the element.</param>
    /// <returns>If the element has all bits set or not.</returns>
    bool IsAllElement(int _index)
    {
        object elemVal = GetEnumValue(_index);
        return elemVal.Equals(DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType));
    }

    private static object GetTargetObjectOfProperty(SerializedProperty prop)
    {
        var path = prop.propertyPath.Replace(".Array.data[", "[");
        object obj = prop.serializedObject.targetObject;
        var elements = path.Split('.');
        foreach (var element in elements)
        {
            if (element.Contains("["))
            {
                var elementName = element.Substring(0, element.IndexOf("["));
                var index = Convert.ToInt32(element.Substring(element.IndexOf("[")).Replace("[", "").Replace("]", ""));
                obj = GetValue_Imp(obj, elementName, index);
            }
            else
            {
                obj = GetValue_Imp(obj, element);
            }
        }

        return obj;
    }

    private static object GetValue_Imp(object source, string name)
    {
        if (source == null)
            return null;
        var type = source.GetType();

        while (type != null)
        {
            var f = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            if (f != null)
                return f.GetValue(source);

            var p = type.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
            if (p != null)
                return p.GetValue(source, null);

            type = type.BaseType;
        }

        return null;
    }

    private static object GetValue_Imp(object source, string name, int index)
    {
        var enumerable = GetValue_Imp(source, name) as IEnumerable;
        if (enumerable == null)
            return null;
        var enm = enumerable.GetEnumerator();

        for (int i = 0; i <= index; i++)
        {
            if (!enm.MoveNext())
                return null;
        }

        return enm.Current;
    }

    private static SerializedProperty GetParentProperty(SerializedProperty prop) {
        var path = prop.propertyPath;
        var parentPathParts = path.Split('.');
        string parentPath = "";
        for (int i = 0; i < parentPathParts.Length - 1; i++) {
            parentPath += parentPathParts[i];
            if (i < parentPathParts.Length - 2)
                parentPath += ".";
        }

        var parentProp = prop.serializedObject.FindProperty(parentPath);
        if (parentProp == null) {
            Debug.LogError("Couldn't find parent " + parentPath + ", child path is " + prop.propertyPath);
        }

        return parentProp;
    }
}
1 Like