Search Unity

Property drawer for enum flags/masks (download)

Discussion in 'Immediate Mode GUI (IMGUI)' started by ItsaMeTuni, Feb 15, 2018.

  1. ItsaMeTuni

    ItsaMeTuni

    Joined:
    Jan 19, 2015
    Posts:
    44
    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.


    Example:
    Code (CSharp):
    1. [System.Flags]
    2. public enum BlockSides
    3. {
    4.     Left    = (1 << 0),
    5.     Right   = (1 << 1),
    6.     Front   = (1 << 2),
    7.     Back    = (1 << 3),
    8.     Up      = (1 << 4),
    9.     Down    = (1 << 5)
    10. }
    11.  
    12. [EnumMask] BlockSides attachableSides;
    13.  
    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 @Meatgrind 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.

    Code (CSharp):
    1.  
    2. /*
    3.  Written by: Lucas Antunes (aka ItsaMeTuni), lucasba8@gmail.com
    4.  In: 2/15/2018
    5.  The only thing that you cannot do with this script is sell it by itself without substantially modifying it.
    6.  */
    7.  
    8. using System;
    9. using UnityEngine;
    10.  
    11. #if UNITY_EDITOR
    12. using UnityEditor;
    13. [CustomPropertyDrawer(typeof(EnumMaskAttribute))]
    14. public class EnumMaskPropertyDrawer : PropertyDrawer
    15. {
    16.     bool foldoutOpen = false;
    17.  
    18.     object theEnum;
    19.     Array enumValues;
    20.     Type enumUnderlyingType;
    21.  
    22.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    23.     {
    24.         if (foldoutOpen)
    25.             return EditorGUIUtility.singleLineHeight * (Enum.GetValues(fieldInfo.FieldType).Length + 2);
    26.         else
    27.             return EditorGUIUtility.singleLineHeight;
    28.     }
    29.  
    30.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    31.     {
    32.         theEnum = fieldInfo.GetValue(property.serializedObject.targetObject);
    33.         enumValues = Enum.GetValues(theEnum.GetType());
    34.         enumUnderlyingType = Enum.GetUnderlyingType(theEnum.GetType());
    35.  
    36.         //We need to convert the enum to its underlying type, if we don't it will be boxed
    37.         //into an object later and then we would need to unbox it like (UnderlyingType)(EnumType)theEnum.
    38.         //If we do this here we can just do (UnderlyingType)theEnum later (plus we can visualize the value of theEnum in VS when debugging)
    39.         theEnum = Convert.ChangeType(theEnum, enumUnderlyingType);
    40.  
    41.         EditorGUI.BeginProperty(position, label, property);
    42.  
    43.         foldoutOpen = EditorGUI.Foldout(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), foldoutOpen, label);
    44.  
    45.         if (foldoutOpen)
    46.         {
    47.             //Draw the All button
    48.             if (GUI.Button(new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight * 1, 30, 15), "All"))
    49.             {
    50.                 theEnum = DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType);
    51.             }
    52.  
    53.             //Draw the None button
    54.             if (GUI.Button(new Rect(position.x + 32, position.y + EditorGUIUtility.singleLineHeight * 1, 40, 15), "None"))
    55.             {
    56.                 theEnum = Convert.ChangeType(0, enumUnderlyingType);
    57.             }
    58.  
    59.             //Draw the list
    60.             for (int i = 0; i < Enum.GetNames(fieldInfo.FieldType).Length; i++)
    61.             {
    62.                 if (EditorGUI.Toggle(new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight * (2 + i), position.width, EditorGUIUtility.singleLineHeight), Enum.GetNames(fieldInfo.FieldType)[i], IsSet(i)))
    63.                 {
    64.                     ToggleIndex(i, true);
    65.                 }
    66.                 else
    67.                 {
    68.                     ToggleIndex(i, false);
    69.                 }
    70.             }
    71.         }
    72.  
    73.         fieldInfo.SetValue(property.serializedObject.targetObject, theEnum);
    74.         property.serializedObject.ApplyModifiedProperties();
    75.     }
    76.  
    77.     /// <summary>
    78.     /// 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)
    79.     /// </summary>
    80.     object GetEnumValue(int _index)
    81.     {
    82.         return Convert.ChangeType(enumValues.GetValue(_index), enumUnderlyingType);
    83.     }
    84.  
    85.     /// <summary>
    86.     /// 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)
    87.     /// </summary>
    88.     /// <param name="_set">If true the flag will be set, if false the flag will be unset.</param>
    89.     void ToggleIndex(int _index, bool _set)
    90.     {
    91.         if(_set)
    92.         {
    93.             if(IsNoneElement(_index))
    94.             {
    95.                 theEnum = Convert.ChangeType(0, enumUnderlyingType);
    96.             }
    97.  
    98.             //enum = enum | val
    99.             theEnum = DoOrOperator(theEnum, GetEnumValue(_index), enumUnderlyingType);
    100.         }
    101.         else
    102.         {
    103.             if (IsNoneElement(_index) || IsAllElement(_index))
    104.             {
    105.                 return;
    106.             }
    107.  
    108.             object val = GetEnumValue(_index);
    109.             object notVal = DoNotOperator(val, enumUnderlyingType);
    110.  
    111.             //enum = enum & ~val
    112.             theEnum = DoAndOperator(theEnum, notVal, enumUnderlyingType);
    113.         }
    114.  
    115.     }
    116.  
    117.     /// <summary>
    118.     /// 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)
    119.     /// </summary>
    120.     bool IsSet(int _index)
    121.     {
    122.         object val = DoAndOperator(theEnum, GetEnumValue(_index), enumUnderlyingType);
    123.  
    124.         //We handle All and None elements differently, since they're "special"
    125.         if(IsAllElement(_index))
    126.         {
    127.             //If all other bits visible to the user (elements) are set, the "All" element checkbox has to be checked
    128.             //We don't do a simple AND operation because there might be missing bits.
    129.             //e.g. An enum with 6 elements including the "All" element. If we set all bits visible except the "All" bit,
    130.             //two bits might be unset. Since we want the "All" element checkbox to be checked when all other elements are set
    131.             //we have to make sure those two extra bits are also set.
    132.             bool allSet = true;
    133.             for (int i = 0; i < Enum.GetNames(fieldInfo.FieldType).Length; i++)
    134.             {
    135.                 if(i != _index && !IsNoneElement(i) && !IsSet(i))
    136.                 {
    137.                     allSet = false;
    138.                     break;
    139.                 }
    140.             }
    141.  
    142.             //Make sure all bits are set if all "visible bits" are set
    143.             if(allSet)
    144.             {
    145.                 theEnum = DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType);
    146.             }
    147.  
    148.             return allSet;
    149.         }
    150.         else if (IsNoneElement(_index))
    151.         {
    152.             //Just check the "None" element checkbox our enum's value is 0
    153.             return Convert.ChangeType(theEnum, enumUnderlyingType).Equals(Convert.ChangeType(0, enumUnderlyingType));
    154.         }
    155.  
    156.         return !val.Equals(Convert.ChangeType(0, enumUnderlyingType));
    157.     }
    158.  
    159.     /// <summary>
    160.     /// Call the bitwise OR operator (|) on _lhs and _rhs given their types.
    161.     /// Will basically return _lhs | _rhs
    162.     /// </summary>
    163.     /// <param name="_lhs">Left-hand side of the operation.</param>
    164.     /// <param name="_rhs">Right-hand side of the operation.</param>
    165.     /// <param name="_type">Type of the objects.</param>
    166.     /// <returns>Result of the operation</returns>
    167.     static object DoOrOperator(object _lhs, object _rhs, Type _type)
    168.     {
    169.         if(_type == typeof(int))
    170.         {
    171.             return ((int)_lhs) | ((int)_rhs);
    172.         }
    173.         else if (_type == typeof(uint))
    174.         {
    175.             return ((uint)_lhs) | ((uint)_rhs);
    176.         }
    177.         else if (_type == typeof(short))
    178.         {
    179.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    180.             return unchecked((short)((short)_lhs | (short)_rhs));
    181.         }
    182.         else if (_type == typeof(ushort))
    183.         {
    184.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    185.             return unchecked((ushort)((ushort)_lhs | (ushort)_rhs));
    186.         }
    187.         else if (_type == typeof(long))
    188.         {
    189.             return ((long)_lhs) | ((long)_rhs);
    190.         }
    191.         else if (_type == typeof(ulong))
    192.         {
    193.             return ((ulong)_lhs) | ((ulong)_rhs);
    194.         }
    195.         else if (_type == typeof(byte))
    196.         {
    197.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    198.             return unchecked((byte)((byte)_lhs | (byte)_rhs));
    199.         }
    200.         else if (_type == typeof(sbyte))
    201.         {
    202.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    203.             return unchecked((sbyte)((sbyte)_lhs | (sbyte)_rhs));
    204.         }
    205.         else
    206.         {
    207.             throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
    208.         }
    209.     }
    210.  
    211.     /// <summary>
    212.     /// Call the bitwise AND operator (&) on _lhs and _rhs given their types.
    213.     /// Will basically return _lhs & _rhs
    214.     /// </summary>
    215.     /// <param name="_lhs">Left-hand side of the operation.</param>
    216.     /// <param name="_rhs">Right-hand side of the operation.</param>
    217.     /// <param name="_type">Type of the objects.</param>
    218.     /// <returns>Result of the operation</returns>
    219.     static object DoAndOperator(object _lhs, object _rhs, Type _type)
    220.     {
    221.         if (_type == typeof(int))
    222.         {
    223.             return ((int)_lhs) & ((int)_rhs);
    224.         }
    225.         else if (_type == typeof(uint))
    226.         {
    227.             return ((uint)_lhs) & ((uint)_rhs);
    228.         }
    229.         else if (_type == typeof(short))
    230.         {
    231.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    232.             return unchecked((short)((short)_lhs & (short)_rhs));
    233.         }
    234.         else if (_type == typeof(ushort))
    235.         {
    236.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    237.             return unchecked((ushort)((ushort)_lhs & (ushort)_rhs));
    238.         }
    239.         else if (_type == typeof(long))
    240.         {
    241.             return ((long)_lhs) & ((long)_rhs);
    242.         }
    243.         else if (_type == typeof(ulong))
    244.         {
    245.             return ((ulong)_lhs) & ((ulong)_rhs);
    246.         }
    247.         else if (_type == typeof(byte))
    248.         {
    249.             return unchecked((byte)((byte)_lhs & (byte)_rhs));
    250.         }
    251.         else if (_type == typeof(sbyte))
    252.         {
    253.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    254.             return unchecked((sbyte)((sbyte)_lhs & (sbyte)_rhs));
    255.         }
    256.         else
    257.         {
    258.             throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
    259.         }
    260.     }
    261.  
    262.     /// <summary>
    263.     /// Call the bitwise NOT operator (~) on _lhs given its type.
    264.     /// Will basically return ~_lhs
    265.     /// </summary>
    266.     /// <param name="_lhs">Left-hand side of the operation.</param>
    267.     /// <param name="_type">Type of the object.</param>
    268.     /// <returns>Result of the operation</returns>
    269.     static object DoNotOperator(object _lhs, Type _type)
    270.     {
    271.         if (_type == typeof(int))
    272.         {
    273.             return ~(int)_lhs;
    274.         }
    275.         else if (_type == typeof(uint))
    276.         {
    277.             return ~(uint)_lhs;
    278.         }
    279.         else if (_type == typeof(short))
    280.         {
    281.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    282.             return unchecked((short)~(short)_lhs);
    283.         }
    284.         else if (_type == typeof(ushort))
    285.         {
    286.  
    287.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    288.             return unchecked((ushort)~(ushort)_lhs);
    289.         }
    290.         else if (_type == typeof(long))
    291.         {
    292.             return ~(long)_lhs;
    293.         }
    294.         else if (_type == typeof(ulong))
    295.         {
    296.             return ~(ulong)_lhs;
    297.         }
    298.         else if (_type == typeof(byte))
    299.         {
    300.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    301.             return (byte)~(byte)_lhs;
    302.         }
    303.         else if (_type == typeof(sbyte))
    304.         {
    305.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    306.             return unchecked((sbyte)~(sbyte)_lhs);
    307.         }
    308.         else
    309.         {
    310.             throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
    311.         }
    312.     }
    313.  
    314.     /// <summary>
    315.     /// Check if the element of specified index is a "None" element (all bits unset, value = 0).
    316.     /// </summary>
    317.     /// <param name="_index">Index of the element.</param>
    318.     /// <returns>If the element has all bits unset or not.</returns>
    319.     bool IsNoneElement(int _index)
    320.     {
    321.         return GetEnumValue(_index).Equals(Convert.ChangeType(0, enumUnderlyingType));
    322.     }
    323.  
    324.     /// <summary>
    325.     /// Check if the element of specified index is an "All" element (all bits set, value = ~0).
    326.     /// </summary>
    327.     /// <param name="_index">Index of the element.</param>
    328.     /// <returns>If the element has all bits set or not.</returns>
    329.     bool IsAllElement(int _index)
    330.     {
    331.         object elemVal = GetEnumValue(_index);
    332.         return elemVal.Equals(DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType));
    333.     }
    334. }
    335. #endif
    336.  
    337. [System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false)]
    338. public class EnumMaskAttribute : PropertyAttribute
    339. {
    340.  
    341. }
    342.  
    343.  

    I hope this helps someone!
     

    Attached Files:

    Last edited: Jan 10, 2019
  2. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    Alternatively you can use the MaskField to display like a multi select dropdown

    Code (csharp):
    1.  
    2.     public override void OnGUI(Rect position,
    3.                                 SerializedProperty property,
    4.                                 GUIContent label)
    5.     {
    6.         EditorGUI.BeginChangeCheck();
    7.         uint a = (uint)(EditorGUI.MaskField(position, label, property.intValue, property.enumNames));
    8.         if (EditorGUI.EndChangeCheck())
    9.         {
    10.             property.intValue = (int)a;
    11.         }
    12.     }
    13. [code]
     
    Dawdlebird, NotaNaN and r2games312 like this.
  3. ItsaMeTuni

    ItsaMeTuni

    Joined:
    Jan 19, 2015
    Posts:
    44
    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.
     
    Cippman likes this.
  4. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    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
     
  5. TheCelt

    TheCelt

    Joined:
    Feb 27, 2013
    Posts:
    742
    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.
     
  6. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    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):

    Code (CSharp):
    1. [Flags]
    2. enum MyEnum {
    3.   Foo = 0x1,
    4.   Bar = 0x2,
    5.  
    6.   Baz = 0x8 // <-- skips 0x4
    7. }
    You should be iterating over Enum.GetValues instead of creating your own.
     
    ItsaMeTuni likes this.
  7. ItsaMeTuni

    ItsaMeTuni

    Joined:
    Jan 19, 2015
    Posts:
    44
    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...
     
    Madgvox likes this.
  8. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    ~1
    is two's complement
    -2
    :

    Code (Raw):
    1. 11111111 11111110
    My first guess is that something's converting the ushort back to a signed value somewhere.
     
  9. ItsaMeTuni

    ItsaMeTuni

    Joined:
    Jan 19, 2015
    Posts:
    44
    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.
     
  10. ItsaMeTuni

    ItsaMeTuni

    Joined:
    Jan 19, 2015
    Posts:
    44
    @Madgvox I fixed it and updated the post with the working source and file for download :)
    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#.
     
    Madgvox likes this.
  11. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    Sure, no problem!
     
  12. stalker_shin

    stalker_shin

    Joined:
    Jun 25, 2016
    Posts:
    17
    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!
     
  13. ItsaMeTuni

    ItsaMeTuni

    Joined:
    Jan 19, 2015
    Posts:
    44
    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.
     
  14. stalker_shin

    stalker_shin

    Joined:
    Jun 25, 2016
    Posts:
    17
    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 :)
    I'll read your article, I really need to learn from it.
     
  15. ItsaMeTuni

    ItsaMeTuni

    Joined:
    Jan 19, 2015
    Posts:
    44
    No problem :) If you still need help (or have any questions), create a new thread and be sure to mention me so I can help you!
     
  16. Keepabee

    Keepabee

    Joined:
    Jul 12, 2012
    Posts:
    58
    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!
     
  17. ItsaMeTuni

    ItsaMeTuni

    Joined:
    Jan 19, 2015
    Posts:
    44
    @Meatgrind 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! :)
     
    SamFernGamer4k likes this.
  18. Noxury

    Noxury

    Joined:
    Mar 8, 2015
    Posts:
    22
    Thank you!
    I am trying to use an enum in a nested class, but it dont works for me.

    Code (CSharp):
    1. public class TeamManager : MonoBehaviour {
    2.  
    3.     [System.Serializable]
    4.     public class Team {
    5.         public TeamName name;
    6.         public Color color = Color.white;
    7.         public Sprite teamIcon;
    8.  
    9.         [EnumMask]
    10.         public TeamName enemyTeams;
    11.     }
    12.  
    13.     [SerializeField]
    14.     private List<Team> teams;
    15. }
    16.  
    17. [System.Flags]
    18. public enum TeamName {
    19.     Neutral = (1 << 0),
    20.     A = (1 << 1),
    21.     B = (1 << 2),
    22.     C = (1 << 3)
    23. }
    24.  
    When I expand the list, the console shows the following 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)
     
    Last edited: Mar 11, 2019
    StolenCheese likes this.
  19. StolenCheese

    StolenCheese

    Joined:
    Aug 23, 2017
    Posts:
    1
    This has helped me a lot and should be included in Unity by default. Thanks a lot
     
  20. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    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:

    Code (csharp):
    1. theEnum = fieldInfo.GetValue(property.serializedObject.targetObject);
    2. // later
    3. 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.

    Code (csharp):
    1. object targetObject;
    2. try {
    3.     targetObject = property.serializedObject.targetObject;
    4.     theEnum = fieldInfo.GetValue(targetObject);
    5. }
    6. catch (ArgumentException ) {
    7.     targetObject = GetTargetObjectOfProperty(GetParentProperty(property));
    8.     theEnum = fieldInfo.GetValue(targetObject);
    9. }
    10.  
    11. // later:
    12. 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:
    Code (csharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    5. public class EnumMaskAttribute : PropertyAttribute
    6. {
    7.     public bool alwaysFoldOut;
    8.     public EnumMaskLayout layout = EnumMaskLayout.Vertical;
    9. }
    10.  
    11. public enum EnumMaskLayout
    12. {
    13.     Vertical,
    14.     Horizontal
    15. }
    Drawer Code:
    Code (csharp):
    1. /*
    2. Written by: Lucas Antunes (aka ItsaMeTuni), lucasba8@gmail.com
    3. In: 2/15/2018
    4. The only thing that you cannot do with this script is sell it by itself without substantially modifying it.
    5.  
    6. Updated by Baste Nesse Buanes, baste@rain-games.com (thanks to @lordofduct for GetTargetOfProperty implementation)
    7. 06-Sep-2019
    8. */
    9.  
    10. using System;
    11. using System.Collections;
    12. using System.Collections.Generic;
    13. using System.Reflection;
    14. using UnityEngine;
    15.  
    16. using UnityEditor;
    17. [CustomPropertyDrawer(typeof(EnumMaskAttribute))]
    18. public class EnumMaskPropertyDrawer : PropertyDrawer
    19. {
    20.     Dictionary<string, bool> openFoldouts = new Dictionary<string, bool>();
    21.  
    22.     object theEnum;
    23.     Array enumValues;
    24.     Type enumUnderlyingType;
    25.  
    26.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    27.     {
    28.         var enumMaskAttribute = ((EnumMaskAttribute) attribute);
    29.         var foldoutOpen = enumMaskAttribute.alwaysFoldOut;
    30.  
    31.         if (!foldoutOpen)
    32.         {
    33.             if (!openFoldouts.TryGetValue(property.propertyPath, out foldoutOpen))
    34.             {
    35.                 openFoldouts[property.propertyPath] = false;
    36.             }
    37.         }
    38.         if (foldoutOpen)
    39.         {
    40.             var layout = ((EnumMaskAttribute )attribute).layout;
    41.             if (layout == EnumMaskLayout.Vertical)
    42.                 return EditorGUIUtility.singleLineHeight * (Enum.GetValues(fieldInfo.FieldType).Length + 2);
    43.             else
    44.                 return EditorGUIUtility.singleLineHeight * 3;
    45.         }
    46.         else
    47.             return EditorGUIUtility.singleLineHeight;
    48.     }
    49.  
    50.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    51.     {
    52.         object targetObject;
    53.         try {
    54.             targetObject = property.serializedObject.targetObject;
    55.             theEnum = fieldInfo.GetValue(targetObject);
    56.         }
    57.         catch (ArgumentException ) {
    58.             targetObject = GetTargetObjectOfProperty(GetParentProperty(property));
    59.             theEnum = fieldInfo.GetValue(targetObject);
    60.         }
    61.  
    62.         enumValues = Enum.GetValues(theEnum.GetType());
    63.         enumUnderlyingType = Enum.GetUnderlyingType(theEnum.GetType());
    64.  
    65.         //We need to convert the enum to its underlying type, if we don't it will be boxed
    66.         //into an object later and then we would need to unbox it like (UnderlyingType)(EnumType)theEnum.
    67.         //If we do this here we can just do (UnderlyingType)theEnum later (plus we can visualize the value of theEnum in VS when debugging)
    68.         theEnum = Convert.ChangeType(theEnum, enumUnderlyingType);
    69.  
    70.         EditorGUI.BeginProperty(position, label, property);
    71.  
    72.         var enumMaskAttribute = ((EnumMaskAttribute) attribute);
    73.         var alwaysFoldOut = enumMaskAttribute.alwaysFoldOut;
    74.         var foldoutOpen = alwaysFoldOut;
    75.  
    76.         if (alwaysFoldOut) {
    77.             EditorGUI.LabelField(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), label);
    78.         }
    79.         else {
    80.             if (!openFoldouts.TryGetValue(property.propertyPath, out foldoutOpen)) {
    81.                 openFoldouts[property.propertyPath] = false;
    82.             }
    83.  
    84.             EditorGUI.BeginChangeCheck();
    85.             foldoutOpen = EditorGUI.Foldout(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), foldoutOpen, label);
    86.  
    87.             if (EditorGUI.EndChangeCheck())
    88.                 openFoldouts[property.propertyPath] =  foldoutOpen;
    89.         }
    90.  
    91.         if (foldoutOpen)
    92.         {
    93.             //Draw the All button
    94.             if (GUI.Button(new Rect(position.x + (15f * EditorGUI.indentLevel), position.y + EditorGUIUtility.singleLineHeight * 1, 30, 15), "All"))
    95.             {
    96.                 theEnum = DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType);
    97.             }
    98.  
    99.             //Draw the None button
    100.             if (GUI.Button(new Rect(position.x + 32 + (15f * EditorGUI.indentLevel), position.y + EditorGUIUtility.singleLineHeight * 1, 50, 15), "None"))
    101.             {
    102.                 theEnum = Convert.ChangeType(0, enumUnderlyingType);
    103.             }
    104.  
    105.             var layout = enumMaskAttribute.layout;
    106.  
    107.             if (layout == EnumMaskLayout.Vertical)
    108.             {
    109.                 //Draw the list vertically
    110.                 for (int i = 0; i < Enum.GetNames(fieldInfo.FieldType).Length; i++)
    111.                 {
    112.                     if (EditorGUI.Toggle(new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight * (2 + i), position.width, EditorGUIUtility.singleLineHeight), Enum.GetNames(fieldInfo.FieldType)[i], IsSet(i)))
    113.                     {
    114.                         ToggleIndex(i, true);
    115.                     }
    116.                     else
    117.                     {
    118.                         ToggleIndex(i, false);
    119.                     }
    120.                 }
    121.             }
    122.             else
    123.             {
    124.                 var enumNames = Enum.GetNames(fieldInfo.FieldType);
    125.  
    126.                 var style = new GUIStyle(GUI.skin.label) {
    127.                     alignment = TextAnchor.MiddleRight,
    128.                     clipping = TextClipping.Overflow
    129.                 };
    130.  
    131.                 //Draw the list horizontally
    132.                 var labelWidth = 50f;
    133.                 for (int i = 0; i < enumNames.Length; i++)
    134.                     labelWidth = Mathf.Max(labelWidth, GUI.skin.label.CalcSize(new GUIContent(enumNames[i])).x);
    135.                 var toggleWidth = labelWidth + 20;
    136.  
    137.                 var oldLabelWidth = EditorGUIUtility.labelWidth;
    138.                 var oldIndentLevel = EditorGUI.indentLevel; // Toggles kinda are broken at non-zero indent levels, as the indentation eats a part of the clickable rect.
    139.  
    140.                 EditorGUIUtility.labelWidth = labelWidth;
    141.                 EditorGUI.indentLevel = 0;
    142.  
    143.                 position.width = toggleWidth;
    144.                 position.y += + EditorGUIUtility.singleLineHeight * 2;
    145.                 var xBase = position.x + oldIndentLevel * 15f;
    146.                 for (int i = 0; i < enumNames.Length; i++)
    147.                 {
    148.                     position.x = xBase + (i * position.width);
    149.                     var togglePos = EditorGUI.PrefixLabel(position, new GUIContent(enumNames[i]), style);
    150.                     if (EditorGUI.Toggle(togglePos, IsSet(i)))
    151.                     {
    152.                         ToggleIndex(i, true);
    153.                     }
    154.                     else
    155.                     {
    156.                         ToggleIndex(i, false);
    157.                     }
    158.                 }
    159.  
    160.                 EditorGUIUtility.labelWidth = oldLabelWidth;
    161.                 EditorGUI.indentLevel = oldIndentLevel;
    162.             }
    163.         }
    164.  
    165.         property.intValue = (int) theEnum;
    166.     }
    167.  
    168.     /// <summary>
    169.     /// 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)
    170.     /// </summary>
    171.     object GetEnumValue(int _index)
    172.     {
    173.         return Convert.ChangeType(enumValues.GetValue(_index), enumUnderlyingType);
    174.     }
    175.  
    176.     /// <summary>
    177.     /// 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)
    178.     /// </summary>
    179.     /// <param name="_set">If true the flag will be set, if false the flag will be unset.</param>
    180.     void ToggleIndex(int _index, bool _set)
    181.     {
    182.         if(_set)
    183.         {
    184.             if(IsNoneElement(_index))
    185.             {
    186.                 theEnum = Convert.ChangeType(0, enumUnderlyingType);
    187.             }
    188.  
    189.             //enum = enum | val
    190.             theEnum = DoOrOperator(theEnum, GetEnumValue(_index), enumUnderlyingType);
    191.         }
    192.         else
    193.         {
    194.             if (IsNoneElement(_index) || IsAllElement(_index))
    195.             {
    196.                 return;
    197.             }
    198.  
    199.             object val = GetEnumValue(_index);
    200.             object notVal = DoNotOperator(val, enumUnderlyingType);
    201.  
    202.             //enum = enum & ~val
    203.             theEnum = DoAndOperator(theEnum, notVal, enumUnderlyingType);
    204.         }
    205.  
    206.     }
    207.  
    208.     /// <summary>
    209.     /// 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)
    210.     /// </summary>
    211.     bool IsSet(int _index)
    212.     {
    213.         object val = DoAndOperator(theEnum, GetEnumValue(_index), enumUnderlyingType);
    214.  
    215.         //We handle All and None elements differently, since they're "special"
    216.         if(IsAllElement(_index))
    217.         {
    218.             //If all other bits visible to the user (elements) are set, the "All" element checkbox has to be checked
    219.             //We don't do a simple AND operation because there might be missing bits.
    220.             //e.g. An enum with 6 elements including the "All" element. If we set all bits visible except the "All" bit,
    221.             //two bits might be unset. Since we want the "All" element checkbox to be checked when all other elements are set
    222.             //we have to make sure those two extra bits are also set.
    223.             bool allSet = true;
    224.             for (int i = 0; i < Enum.GetNames(fieldInfo.FieldType).Length; i++)
    225.             {
    226.                 if(i != _index && !IsNoneElement(i) && !IsSet(i))
    227.                 {
    228.                     allSet = false;
    229.                     break;
    230.                 }
    231.             }
    232.  
    233.             //Make sure all bits are set if all "visible bits" are set
    234.             if(allSet)
    235.             {
    236.                 theEnum = DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType);
    237.             }
    238.  
    239.             return allSet;
    240.         }
    241.         else if (IsNoneElement(_index))
    242.         {
    243.             //Just check the "None" element checkbox our enum's value is 0
    244.             return Convert.ChangeType(theEnum, enumUnderlyingType).Equals(Convert.ChangeType(0, enumUnderlyingType));
    245.         }
    246.  
    247.         return !val.Equals(Convert.ChangeType(0, enumUnderlyingType));
    248.     }
    249.  
    250.     /// <summary>
    251.     /// Call the bitwise OR operator (|) on _lhs and _rhs given their types.
    252.     /// Will basically return _lhs | _rhs
    253.     /// </summary>
    254.     /// <param name="_lhs">Left-hand side of the operation.</param>
    255.     /// <param name="_rhs">Right-hand side of the operation.</param>
    256.     /// <param name="_type">Type of the objects.</param>
    257.     /// <returns>Result of the operation</returns>
    258.     static object DoOrOperator(object _lhs, object _rhs, Type _type)
    259.     {
    260.         if(_type == typeof(int))
    261.         {
    262.             return ((int)_lhs) | ((int)_rhs);
    263.         }
    264.         else if (_type == typeof(uint))
    265.         {
    266.             return ((uint)_lhs) | ((uint)_rhs);
    267.         }
    268.         else if (_type == typeof(short))
    269.         {
    270.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    271.             return unchecked((short)((short)_lhs | (short)_rhs));
    272.         }
    273.         else if (_type == typeof(ushort))
    274.         {
    275.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    276.             return unchecked((ushort)((ushort)_lhs | (ushort)_rhs));
    277.         }
    278.         else if (_type == typeof(long))
    279.         {
    280.             return ((long)_lhs) | ((long)_rhs);
    281.         }
    282.         else if (_type == typeof(ulong))
    283.         {
    284.             return ((ulong)_lhs) | ((ulong)_rhs);
    285.         }
    286.         else if (_type == typeof(byte))
    287.         {
    288.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    289.             return unchecked((byte)((byte)_lhs | (byte)_rhs));
    290.         }
    291.         else if (_type == typeof(sbyte))
    292.         {
    293.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    294.             return unchecked((sbyte)((sbyte)_lhs | (sbyte)_rhs));
    295.         }
    296.         else
    297.         {
    298.             throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
    299.         }
    300.     }
    301.  
    302.     /// <summary>
    303.     /// Call the bitwise AND operator (&) on _lhs and _rhs given their types.
    304.     /// Will basically return _lhs & _rhs
    305.     /// </summary>
    306.     /// <param name="_lhs">Left-hand side of the operation.</param>
    307.     /// <param name="_rhs">Right-hand side of the operation.</param>
    308.     /// <param name="_type">Type of the objects.</param>
    309.     /// <returns>Result of the operation</returns>
    310.     static object DoAndOperator(object _lhs, object _rhs, Type _type)
    311.     {
    312.         if (_type == typeof(int))
    313.         {
    314.             return ((int)_lhs) & ((int)_rhs);
    315.         }
    316.         else if (_type == typeof(uint))
    317.         {
    318.             return ((uint)_lhs) & ((uint)_rhs);
    319.         }
    320.         else if (_type == typeof(short))
    321.         {
    322.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    323.             return unchecked((short)((short)_lhs & (short)_rhs));
    324.         }
    325.         else if (_type == typeof(ushort))
    326.         {
    327.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    328.             return unchecked((ushort)((ushort)_lhs & (ushort)_rhs));
    329.         }
    330.         else if (_type == typeof(long))
    331.         {
    332.             return ((long)_lhs) & ((long)_rhs);
    333.         }
    334.         else if (_type == typeof(ulong))
    335.         {
    336.             return ((ulong)_lhs) & ((ulong)_rhs);
    337.         }
    338.         else if (_type == typeof(byte))
    339.         {
    340.             return unchecked((byte)((byte)_lhs & (byte)_rhs));
    341.         }
    342.         else if (_type == typeof(sbyte))
    343.         {
    344.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    345.             return unchecked((sbyte)((sbyte)_lhs & (sbyte)_rhs));
    346.         }
    347.         else
    348.         {
    349.             throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
    350.         }
    351.     }
    352.  
    353.     /// <summary>
    354.     /// Call the bitwise NOT operator (~) on _lhs given its type.
    355.     /// Will basically return ~_lhs
    356.     /// </summary>
    357.     /// <param name="_lhs">Left-hand side of the operation.</param>
    358.     /// <param name="_type">Type of the object.</param>
    359.     /// <returns>Result of the operation</returns>
    360.     static object DoNotOperator(object _lhs, Type _type)
    361.     {
    362.         if (_type == typeof(int))
    363.         {
    364.             return ~(int)_lhs;
    365.         }
    366.         else if (_type == typeof(uint))
    367.         {
    368.             return ~(uint)_lhs;
    369.         }
    370.         else if (_type == typeof(short))
    371.         {
    372.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    373.             return unchecked((short)~(short)_lhs);
    374.         }
    375.         else if (_type == typeof(ushort))
    376.         {
    377.  
    378.             //ushort and short don't have bitwise operators, it is automatically converted to an int, so we convert it back
    379.             return unchecked((ushort)~(ushort)_lhs);
    380.         }
    381.         else if (_type == typeof(long))
    382.         {
    383.             return ~(long)_lhs;
    384.         }
    385.         else if (_type == typeof(ulong))
    386.         {
    387.             return ~(ulong)_lhs;
    388.         }
    389.         else if (_type == typeof(byte))
    390.         {
    391.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    392.             return (byte)~(byte)_lhs;
    393.         }
    394.         else if (_type == typeof(sbyte))
    395.         {
    396.             //byte and sbyte don't have bitwise operators, it is automatically converted to an int, so we convert it back
    397.             return unchecked((sbyte)~(sbyte)_lhs);
    398.         }
    399.         else
    400.         {
    401.             throw new System.ArgumentException("Type " + _type.FullName + " not supported.");
    402.         }
    403.     }
    404.  
    405.     /// <summary>
    406.     /// Check if the element of specified index is a "None" element (all bits unset, value = 0).
    407.     /// </summary>
    408.     /// <param name="_index">Index of the element.</param>
    409.     /// <returns>If the element has all bits unset or not.</returns>
    410.     bool IsNoneElement(int _index)
    411.     {
    412.         return GetEnumValue(_index).Equals(Convert.ChangeType(0, enumUnderlyingType));
    413.     }
    414.  
    415.     /// <summary>
    416.     /// Check if the element of specified index is an "All" element (all bits set, value = ~0).
    417.     /// </summary>
    418.     /// <param name="_index">Index of the element.</param>
    419.     /// <returns>If the element has all bits set or not.</returns>
    420.     bool IsAllElement(int _index)
    421.     {
    422.         object elemVal = GetEnumValue(_index);
    423.         return elemVal.Equals(DoNotOperator(Convert.ChangeType(0, enumUnderlyingType), enumUnderlyingType));
    424.     }
    425.  
    426.     private static object GetTargetObjectOfProperty(SerializedProperty prop)
    427.     {
    428.         var path = prop.propertyPath.Replace(".Array.data[", "[");
    429.         object obj = prop.serializedObject.targetObject;
    430.         var elements = path.Split('.');
    431.         foreach (var element in elements)
    432.         {
    433.             if (element.Contains("["))
    434.             {
    435.                 var elementName = element.Substring(0, element.IndexOf("["));
    436.                 var index = Convert.ToInt32(element.Substring(element.IndexOf("[")).Replace("[", "").Replace("]", ""));
    437.                 obj = GetValue_Imp(obj, elementName, index);
    438.             }
    439.             else
    440.             {
    441.                 obj = GetValue_Imp(obj, element);
    442.             }
    443.         }
    444.  
    445.         return obj;
    446.     }
    447.  
    448.     private static object GetValue_Imp(object source, string name)
    449.     {
    450.         if (source == null)
    451.             return null;
    452.         var type = source.GetType();
    453.  
    454.         while (type != null)
    455.         {
    456.             var f = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    457.             if (f != null)
    458.                 return f.GetValue(source);
    459.  
    460.             var p = type.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
    461.             if (p != null)
    462.                 return p.GetValue(source, null);
    463.  
    464.             type = type.BaseType;
    465.         }
    466.  
    467.         return null;
    468.     }
    469.  
    470.     private static object GetValue_Imp(object source, string name, int index)
    471.     {
    472.         var enumerable = GetValue_Imp(source, name) as IEnumerable;
    473.         if (enumerable == null)
    474.             return null;
    475.         var enm = enumerable.GetEnumerator();
    476.  
    477.         for (int i = 0; i <= index; i++)
    478.         {
    479.             if (!enm.MoveNext())
    480.                 return null;
    481.         }
    482.  
    483.         return enm.Current;
    484.     }
    485.  
    486.     private static SerializedProperty GetParentProperty(SerializedProperty prop) {
    487.         var path = prop.propertyPath;
    488.         var parentPathParts = path.Split('.');
    489.         string parentPath = "";
    490.         for (int i = 0; i < parentPathParts.Length - 1; i++) {
    491.             parentPath += parentPathParts[i];
    492.             if (i < parentPathParts.Length - 2)
    493.                 parentPath += ".";
    494.         }
    495.  
    496.         var parentProp = prop.serializedObject.FindProperty(parentPath);
    497.         if (parentProp == null) {
    498.             Debug.LogError("Couldn't find parent " + parentPath + ", child path is " + prop.propertyPath);
    499.         }
    500.  
    501.         return parentProp;
    502.     }
    503. }
     
    Last edited: Jun 8, 2020
    NotaNaN likes this.
  21. ItsaMeTuni

    ItsaMeTuni

    Joined:
    Jan 19, 2015
    Posts:
    44
    Nice!
     
  22. Krambolage

    Krambolage

    Joined:
    Jan 22, 2013
    Posts:
    25
    Hey there,

    Frst and foremost, I like the visual setup it's very convenient kudos to you :cool:

    I just used the file in Unity 2018.3.14f1
    and it just won't save the enum.
    If I get it right, the following lines should get the job done, right ?
    Code (CSharp):
    1. fieldInfo.SetValue(property.serializedObject.targetObject, theEnum);
    2. property.serializedObject.ApplyModifiedProperties();
    Although I suppos the problem comes from Unity, here's my setup (didn't change your file, didn't put it in any Editor directory) :

    - The enum itself, nothing fancy
    Code (CSharp):
    1. [Flags]
    2. public enum Proficiency
    3. {
    4.     // Weapons
    5.     OneHandSlash = 1<<0,
    6.     OneHandPuncture = 1<<1, // Etc...
    7. }
    - A piece of data, very simple too :
    Code (CSharp):
    1.  
    2. [Header ("Equipment skills"), EnumMask]
    3. [SyncVar] public Proficiency proficiency; // Syncvar from Mirror (@vis2k might help here ?), not Unet's
    4.  
    - Finally, testing (I set the prefab value to OneHandSlash, save the prefab, run the code, the equality below is false.
    Then I stop running the app, double check the prefab, the value actually is 0)
    Code (CSharp):
    1.  
    2. bool proficiency = ((requiredProficiency & player.proficiency) != 0);
    3.  
    So I've got no idea what's going on, mind giving some light here ?
    Thanks by advance :)
     
  23. Krambolage

    Krambolage

    Joined:
    Jan 22, 2013
    Posts:
    25
    Good news,
    Editing the enum in prefabs only works by making an instance out of it (ie dragging it into the world), edit the instance data, then apply the change :)

    Editing the value by "opening" the prefab in the project files won't work.
     
  24. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    Prefabs open in the project view has some slight nuances that makes it break. It might be that the property code is busted in those cases.
     
  25. odan-travis

    odan-travis

    Joined:
    Apr 12, 2019
    Posts:
    10
    (I put this up on github, makes it easier to track changes, original credit goes to OP obviously, I hope it's fine): https://github.com/Heishe/Unity-Enum-Mask-Attribute )

    The script (as far as I can see) doesn't mark the modified object as dirty in any way, meaning changes are lost in certain circumstances (for me, quitting the editor right after changing something means the changes are lost). This is fixed by adding

    Code (CSharp):
    1. Undo.RecordObject(property.serializedObject.targetObject, "description of what changed here...");

    whenever the object actually changes.

    As a side effect, the attribute now supports undo in the editor automatically. Editing prefabs also works correctly.

    The code in github is changed to reflect this fact and based on Baste's modified version, though I'm unsure if it's 100% bug free. For example, I cast the targetObject at the start of the OnGUI function to UnityEngine.Object, assuming that it's always actually of that type because function that seeks for it seems to iterate through UnityEngine.Objects.
     
    Last edited: Jun 8, 2020
  26. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    You're right and wrong.

    It's definitely missing some dirtying. But, since it's a custom property drawer, that gets handled externally, by the editor drawing the property.

    For that to work, though, the property needs to be written to. The proper way to do it is to replace these lines:

    Code (csharp):
    1. fieldInfo.SetValue(targetObject, theEnum);
    2. property.serializedObject.ApplyModifiedProperties();
    With this:

    Code (csharp):
    1. property.intValue = (int) theEnum;
    The old ApplyModifiedProperties was a bit absurd - we never modified the property, but still applied it. With just setting the .intValue, things work as expected - as long as you either don't have a CustomEditor, or the CustomEditor does ApplyModifiedProperties properly. You even get Undo.

    Made a PR!
     
    Last edited: Jun 8, 2020
  27. odan-travis

    odan-travis

    Joined:
    Apr 12, 2019
    Posts:
    10
    Thanks, I'll take a look at it.

    (update: merged)
     
    Last edited: Jun 8, 2020
  28. Limnage

    Limnage

    Joined:
    Sep 12, 2013
    Posts:
    41
    Apologies for bumping this old thread: Is this property drawer properly working with [SerializeReference]? I get

    ArgumentException: Field flags defined on type B is not a field on the target object which is of type A.

    When I have Class A, which is a ScriptableObject, which has a SerializeReference field of type B, which has a flag enum field that I annotated with this property.

    I used this (https://forum.unity.com/threads/serializereference-genericserializedreferenceinspectorui.813366/) to create the SerializeReference fields in the editor.
     
  29. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    2,517
    I also get the same problem here.
     
  30. odan-travis

    odan-travis

    Joined:
    Apr 12, 2019
    Posts:
    10
    I haven't tested it with SerializeReference. Unfortunately I don't have time to look at it/fix it atm, but if anyone feels up to it and does a merge request on github, I can merge it in (if it works, of course).
     
  31. awesomedata

    awesomedata

    Joined:
    Oct 8, 2014
    Posts:
    1,419
    It's great that this is still getting attention. D:

    It also sucks that @Unity hasn't implemented something to make Enums/bitmasks easier to work with...
    It would be nice to be able to rename Enums in the Editor.
     
    Last edited: Oct 16, 2020
    NotaNaN likes this.
  32. Siccity

    Siccity

    Joined:
    Dec 7, 2013
    Posts:
    255
    Doesn't serialize. All my changes are reverted on restart. I'm on 2020.3