├── .gitignore ├── Editor ├── FoldoutEditor.cs └── Resources │ ├── foldout_arrow_closed.png │ └── foldout_arrow_open.png ├── Inspector └── FoldoutAttribute.cs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | [Ll]ibrary/ 2 | [Tt]emp/ 3 | [Oo]bj/ 4 | [Bb]uild/ 5 | [Bb]uilds/ 6 | [Ll]ogs/ 7 | 8 | # Uncomment this line if you wish to ignore the asset store tools plugin 9 | # [Aa]ssets/AssetStoreTools* 10 | 11 | # Visual Studio cache directory 12 | .vs/ 13 | 14 | # Gradle cache directory 15 | .gradle/ 16 | 17 | # Autogenerated VS/MD/Consulo solution and project files 18 | ExportedObj/ 19 | .consulo/ 20 | *.csproj 21 | *.unityproj 22 | *.sln 23 | *.suo 24 | *.tmp 25 | *.user 26 | *.userprefs 27 | *.pidb 28 | *.booproj 29 | *.svd 30 | *.pdb 31 | *.mdb 32 | *.opendb 33 | *.VC.db 34 | 35 | # Unity3D generated meta files 36 | *.pidb.meta 37 | *.pdb.meta 38 | *.mdb.meta 39 | 40 | # Unity3D generated file on crash reports 41 | sysinfo.txt 42 | 43 | # Builds 44 | *.apk 45 | *.unitypackage 46 | 47 | # Crashlytics generated file 48 | crashlytics-build.properties 49 | -------------------------------------------------------------------------------- /Editor/FoldoutEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using Object = UnityEngine.Object; 8 | 9 | [CustomEditor(typeof(Object), true, isFallback = true)] 10 | [CanEditMultipleObjects] 11 | public class FoldoutEditor : Editor 12 | { 13 | //===============================// 14 | // Members 15 | //===============================// 16 | 17 | Dictionary cacheFolds = new Dictionary(); 18 | List props = new List(); 19 | List methods = new List(); 20 | bool initialized; 21 | 22 | //===============================// 23 | // Logic 24 | //===============================// 25 | 26 | void OnEnable() 27 | { 28 | initialized = false; 29 | } 30 | 31 | void OnDisable() 32 | { 33 | //if (Application.wantsToQuit) 34 | //if (applicationIsQuitting) return; 35 | // if (Toolbox.isQuittingOrChangingScene()) return; 36 | if (target != null) 37 | foreach (var c in cacheFolds) 38 | { 39 | EditorPrefs.SetBool(string.Format($"{c.Value.atr.name}{c.Value.props[0].name}{target.GetInstanceID()}"), c.Value.expanded); 40 | c.Value.Dispose(); 41 | } 42 | } 43 | 44 | public override bool RequiresConstantRepaint() 45 | { 46 | return EditorFramework.needToRepaint; 47 | } 48 | 49 | public override void OnInspectorGUI() 50 | { 51 | serializedObject.Update(); 52 | 53 | Setup(); 54 | 55 | if (props.Count == 0) 56 | { 57 | DrawDefaultInspector(); 58 | return; 59 | } 60 | 61 | Body(); 62 | 63 | //Header(); 64 | 65 | serializedObject.ApplyModifiedProperties(); 66 | 67 | //void Header() 68 | //{ 69 | // using (new EditorGUI.DisabledScope("m_Script" == props[0].propertyPath)) 70 | // { 71 | // EditorGUILayout.Space(); 72 | // EditorGUILayout.PropertyField(props[0], true); 73 | // EditorGUILayout.Space(); 74 | // } 75 | //} 76 | 77 | void Body() 78 | { 79 | //EditorGUILayout.Space(); 80 | 81 | for (var i = 1; i < props.Count; i++) 82 | { 83 | // if (props[i].isArray) 84 | // { 85 | // DrawPropertySortableArray(props[i]); 86 | // } 87 | // else 88 | // { 89 | EditorGUILayout.PropertyField(props[i], true); 90 | //} 91 | } 92 | 93 | //EditorGUILayout.Space(); 94 | 95 | if (methods == null) return; 96 | foreach (MethodInfo memberInfo in methods) 97 | { 98 | this.UseButton(memberInfo); 99 | } 100 | 101 | foreach (var pair in cacheFolds) 102 | { 103 | this.UseVerticalLayout(() => Foldout(pair.Value), StyleFramework.box, pair.Value.atr.styled); 104 | EditorGUI.indentLevel = 0; 105 | } 106 | } 107 | 108 | void Foldout(CacheFoldProp cache) 109 | { 110 | cache.expanded = EditorGUILayout.Foldout(cache.expanded, cache.atr.name, true, StyleFramework.foldout); 111 | 112 | if (cache.expanded) 113 | { 114 | EditorGUI.indentLevel = cache.atr.styled ? 1 : 0; 115 | 116 | for (int i = 0; i < cache.props.Count; i++) 117 | { 118 | this.UseVerticalLayout(() => Child(i), StyleFramework.boxChild, cache.atr.styled); 119 | } 120 | } 121 | 122 | void Child(int i) 123 | { 124 | // if (cache.props[i].isArray) 125 | // { 126 | // DrawPropertySortableArray(cache.props[i]); 127 | // } 128 | // else 129 | // { 130 | EditorGUI.BeginDisabledGroup(cache.atr.readOnly); 131 | EditorGUILayout.PropertyField(cache.props[i], new GUIContent(ObjectNames.NicifyVariableName(cache.props[i].name)), true); 132 | EditorGUI.EndDisabledGroup(); 133 | //} 134 | } 135 | } 136 | 137 | void Setup() 138 | { 139 | EditorFramework.currentEvent = Event.current; 140 | if (!initialized) 141 | { 142 | // SetupButtons(); 143 | 144 | List objectFields; 145 | FoldoutAttribute prevFold = default; 146 | 147 | var length = EditorTypes.Get(target, out objectFields); 148 | 149 | for (var i = 0; i < length; i++) 150 | { 151 | #region FOLDERS 152 | 153 | var fold = Attribute.GetCustomAttribute(objectFields[i], typeof(FoldoutAttribute)) as FoldoutAttribute; 154 | CacheFoldProp c; 155 | if (fold == null) 156 | { 157 | if (prevFold != null && prevFold.foldEverything) 158 | { 159 | if (!cacheFolds.TryGetValue(prevFold.name, out c)) 160 | { 161 | cacheFolds.Add(prevFold.name, new CacheFoldProp { atr = prevFold, types = new HashSet { objectFields[i].Name } }); 162 | } 163 | else 164 | { 165 | c.types.Add(objectFields[i].Name); 166 | } 167 | } 168 | 169 | continue; 170 | } 171 | 172 | prevFold = fold; 173 | 174 | if (!cacheFolds.TryGetValue(fold.name, out c)) 175 | { 176 | var expanded = EditorPrefs.GetBool(string.Format($"{fold.name}{objectFields[i].Name}{target.GetInstanceID()}"), false); 177 | cacheFolds.Add(fold.name, new CacheFoldProp { atr = fold, types = new HashSet { objectFields[i].Name }, expanded = expanded }); 178 | } 179 | else c.types.Add(objectFields[i].Name); 180 | 181 | #endregion 182 | } 183 | 184 | var property = serializedObject.GetIterator(); 185 | var next = property.NextVisible(true); 186 | if (next) 187 | { 188 | do 189 | { 190 | HandleFoldProp(property); 191 | } while (property.NextVisible(false)); 192 | } 193 | 194 | initialized = true; 195 | } 196 | } 197 | 198 | // void SetupButtons() 199 | // { 200 | // var members = GetButtonMembers(target); 201 | // 202 | // foreach (var memberInfo in members) 203 | // { 204 | // var method = memberInfo as MethodInfo; 205 | // if (method == null) 206 | // { 207 | // continue; 208 | // } 209 | // 210 | // if (method.GetParameters().Length > 0) 211 | // { 212 | // continue; 213 | // } 214 | // 215 | // if (methods == null) methods = new List(); 216 | // methods.Add(method); 217 | // } 218 | // } 219 | } 220 | 221 | public void HandleFoldProp(SerializedProperty prop) 222 | { 223 | bool shouldBeFolded = false; 224 | 225 | foreach (var pair in cacheFolds) 226 | { 227 | if (pair.Value.types.Contains(prop.name)) 228 | { 229 | var pr = prop.Copy(); 230 | shouldBeFolded = true; 231 | pair.Value.props.Add(pr); 232 | 233 | break; 234 | } 235 | } 236 | 237 | if (shouldBeFolded == false) 238 | { 239 | var pr = prop.Copy(); 240 | props.Add(pr); 241 | } 242 | } 243 | 244 | // IEnumerable GetButtonMembers(object target) 245 | // { 246 | // return target.GetType() 247 | // .GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.NonPublic) 248 | // .Where(CheckButtonAttribute); 249 | // } 250 | 251 | // bool CheckButtonAttribute(MemberInfo memberInfo) 252 | // { 253 | // return Attribute.IsDefined(memberInfo, typeof(ButtonAttribute)); 254 | // } 255 | 256 | class CacheFoldProp 257 | { 258 | public HashSet types = new HashSet(); 259 | public List props = new List(); 260 | public FoldoutAttribute atr; 261 | public bool expanded; 262 | 263 | public void Dispose() 264 | { 265 | props.Clear(); 266 | types.Clear(); 267 | atr = null; 268 | } 269 | } 270 | } 271 | 272 | static class EditorUIHelper 273 | { 274 | public static void UseVerticalLayout(this Editor e, Action action, GUIStyle style, bool styled) 275 | { 276 | if (styled) EditorGUILayout.BeginVertical(style); 277 | else EditorGUILayout.BeginVertical(); 278 | action(); 279 | EditorGUILayout.EndVertical(); 280 | } 281 | 282 | public static void UseButton(this Editor e, MethodInfo m) 283 | { 284 | if (GUILayout.Button(m.Name)) 285 | { 286 | m.Invoke(e.target, null); 287 | } 288 | } 289 | } 290 | 291 | static class StyleFramework 292 | { 293 | public static GUIStyle box; 294 | public static GUIStyle boxChild; 295 | public static GUIStyle foldout; 296 | public static GUIStyle button; 297 | public static GUIStyle text; 298 | 299 | static StyleFramework() 300 | { 301 | bool pro = EditorGUIUtility.isProSkin; 302 | 303 | var uiTex_in = Resources.Load("foldout_arrow_closed"); 304 | var uiTex_in_on = Resources.Load("foldout_arrow_open"); 305 | 306 | var c_on = pro ? Color.white : new Color(51 / 255f, 102 / 255f, 204 / 255f, 1); 307 | 308 | button = new GUIStyle(EditorStyles.miniButton); 309 | button.font = Font.CreateDynamicFontFromOSFont(new[] { "Terminus (TTF) for Windows", "Calibri" }, 17); 310 | 311 | text = new GUIStyle(EditorStyles.label); 312 | text.richText = true; 313 | text.contentOffset = new Vector2(0, 5); 314 | text.font = Font.CreateDynamicFontFromOSFont(new[] { "Terminus (TTF) for Windows", "Calibri" }, 14); 315 | 316 | foldout = new GUIStyle(EditorStyles.foldout); 317 | 318 | foldout.overflow = new RectOffset(-10, 0, 3, 0); 319 | foldout.padding = new RectOffset(15, 0, 0, 0); 320 | 321 | foldout.active.textColor = c_on; 322 | foldout.active.background = uiTex_in; 323 | foldout.onActive.textColor = c_on; 324 | foldout.onActive.background = uiTex_in_on; 325 | 326 | foldout.focused.textColor = c_on; 327 | foldout.focused.background = uiTex_in; 328 | foldout.onFocused.textColor = c_on; 329 | foldout.onFocused.background = uiTex_in_on; 330 | 331 | foldout.hover.textColor = c_on; 332 | foldout.hover.background = uiTex_in; 333 | 334 | foldout.onHover.textColor = c_on; 335 | foldout.onHover.background = uiTex_in_on; 336 | 337 | box = new GUIStyle(GUI.skin.box); 338 | box.padding = new RectOffset(20, 0, 5, 5); 339 | 340 | boxChild = new GUIStyle(GUI.skin.box); 341 | boxChild.active.textColor = c_on; 342 | boxChild.active.background = uiTex_in; 343 | boxChild.onActive.textColor = c_on; 344 | boxChild.onActive.background = uiTex_in_on; 345 | 346 | boxChild.focused.textColor = c_on; 347 | boxChild.focused.background = uiTex_in; 348 | boxChild.onFocused.textColor = c_on; 349 | boxChild.onFocused.background = uiTex_in_on; 350 | 351 | EditorStyles.foldout.active.textColor = c_on; 352 | EditorStyles.foldout.active.background = uiTex_in; 353 | EditorStyles.foldout.onActive.textColor = c_on; 354 | EditorStyles.foldout.onActive.background = uiTex_in_on; 355 | 356 | EditorStyles.foldout.focused.textColor = c_on; 357 | EditorStyles.foldout.focused.background = uiTex_in; 358 | EditorStyles.foldout.onFocused.textColor = c_on; 359 | EditorStyles.foldout.onFocused.background = uiTex_in_on; 360 | 361 | EditorStyles.foldout.hover.textColor = c_on; 362 | EditorStyles.foldout.hover.background = uiTex_in; 363 | 364 | EditorStyles.foldout.onHover.textColor = c_on; 365 | EditorStyles.foldout.onHover.background = uiTex_in_on; 366 | } 367 | 368 | public static string FirstLetterToUpperCase(this string s) 369 | { 370 | if (string.IsNullOrEmpty(s)) 371 | return string.Empty; 372 | 373 | var a = s.ToCharArray(); 374 | a[0] = char.ToUpper(a[0]); 375 | return new string(a); 376 | } 377 | 378 | public static IList GetTypeTree(this Type t) 379 | { 380 | var types = new List(); 381 | while (t.BaseType != null) 382 | { 383 | types.Add(t); 384 | t = t.BaseType; 385 | } 386 | 387 | return types; 388 | } 389 | } 390 | 391 | static class EditorTypes 392 | { 393 | public static Dictionary> fields = new Dictionary>(FastComparable.Default); 394 | 395 | public static int Get(Object target, out List objectFields) 396 | { 397 | var t = target.GetType(); 398 | 399 | var bindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; 400 | objectFields = GetMembersInclPrivateBase(t, bindingFlags).ToList(); 401 | 402 | return objectFields.Count; 403 | } 404 | 405 | public static FieldInfo[] GetMembersInclPrivateBase(Type t, BindingFlags flags) 406 | { 407 | var memberList = new List(); 408 | memberList.AddRange(t.GetFields(flags)); 409 | Type currentType = t; 410 | while ((currentType = currentType.BaseType) != null) 411 | memberList.AddRange(currentType.GetFields(flags)); 412 | return memberList.ToArray(); 413 | } 414 | } 415 | 416 | class FastComparable : IEqualityComparer 417 | { 418 | public static FastComparable Default = new FastComparable(); 419 | 420 | public bool Equals(int x, int y) 421 | { 422 | return x == y; 423 | } 424 | 425 | public int GetHashCode(int obj) 426 | { 427 | return obj.GetHashCode(); 428 | } 429 | } 430 | 431 | [InitializeOnLoad] 432 | public static class EditorFramework 433 | { 434 | internal static bool needToRepaint; 435 | 436 | internal static Event currentEvent; 437 | internal static float t; 438 | 439 | static EditorFramework() 440 | { 441 | EditorApplication.update += Updating; 442 | } 443 | 444 | static void Updating() 445 | { 446 | CheckMouse(); 447 | 448 | if (needToRepaint) 449 | { 450 | t += Time.deltaTime; 451 | 452 | if (t >= 0.3f) 453 | { 454 | t -= 0.3f; 455 | needToRepaint = false; 456 | } 457 | } 458 | } 459 | 460 | static void CheckMouse() 461 | { 462 | var ev = currentEvent; 463 | if (ev == null) return; 464 | 465 | if (ev.type == EventType.MouseMove) 466 | needToRepaint = true; 467 | } 468 | } -------------------------------------------------------------------------------- /Editor/Resources/foldout_arrow_closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliano-marinelli/UnityFoldoutDecorator/c711eb212965bb30fcf8ca8562a437eec80efbf5/Editor/Resources/foldout_arrow_closed.png -------------------------------------------------------------------------------- /Editor/Resources/foldout_arrow_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuliano-marinelli/UnityFoldoutDecorator/c711eb212965bb30fcf8ca8562a437eec80efbf5/Editor/Resources/foldout_arrow_open.png -------------------------------------------------------------------------------- /Inspector/FoldoutAttribute.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | public class FoldoutAttribute : PropertyAttribute 4 | { 5 | public string name; 6 | public bool foldEverything; 7 | public bool readOnly; 8 | public bool styled; 9 | 10 | /// Adds the property to the specified foldout group. 11 | /// Name of the foldout group. 12 | /// Toggle to put all properties to the specified group 13 | /// Toggle to put all properties to the specified group 14 | public FoldoutAttribute(string name, bool foldEverything = true, bool readOnly = false, bool styled = false) 15 | { 16 | this.foldEverything = foldEverything; 17 | this.name = name; 18 | this.readOnly = readOnly; 19 | this.styled = styled; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Giuliano Marinelli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityFoldoutDecorator 2 | Foldout as a Decorator for group variables in Unity Script Inspector with options for: 3 | 4 | * change foldout name. 5 | * apply foldout to one or to many variables 6 | * set variables as read only 7 | * use styled or default editor versions 8 | 9 | ![Preview](https://user-images.githubusercontent.com/5109640/132052832-8a50bbc2-5c90-4037-b926-cc6da6cf6c2e.png) 10 | 11 | ## How to 12 | 13 | ### Install 14 | Download and copy the **Editor** and **Inspector** folders into your Project Assets. 15 | 16 | ### Use 17 | 18 | Just put the decorator above the variable and set the options you want: 19 | ```csharp 20 | [Foldout("Name", foldEverything = true, styled = true, readOnly = true)] 21 | ``` 22 | 23 | Example (same as image above): 24 | ```csharp 25 | public class MyScript : MonoBehaviour 26 | { 27 | [Foldout("One styled with read only", foldEverything = false, styled = true, readOnly = true)] 28 | public float speed = 1f; 29 | public float retractSpeed = 1f; 30 | 31 | [Foldout("Styled ones", styled = true)] 32 | public float climbSpeed = 1f; 33 | public float nodeDistance = 2f; 34 | public float minDistance = 2f; 35 | 36 | [Foldout("Only this", false)] 37 | public int maxAmountNodes = 20; 38 | public GameObject nodePrefab; 39 | 40 | [Foldout("This and this", false)] 41 | public LayerMask hookableLayer; 42 | [Foldout("This and this", false)] 43 | public LayerMask collibleLayer; 44 | 45 | [Foldout("A group")] 46 | public Vector2 direction; 47 | public GameObject player; 48 | public GameObject spawn; 49 | 50 | [Foldout("Read only things", readOnly = true)] 51 | public bool done = false; 52 | public bool connected = false; 53 | public bool retracting = false; 54 | public bool climbing = false; 55 | } 56 | ``` 57 | --------------------------------------------------------------------------------