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