├── Editor ├── SidekickIcon.png ├── d_SidekickIcon.png ├── Dropdowns.meta ├── ECSContext.cs.meta ├── SelectionInfo.cs.meta ├── ECSAccess │ ├── ECSAccessInner │ │ ├── Sidekick.ECSAccess.asmref │ │ ├── Sidekick.ECSAccess.asmref.meta │ │ ├── ECSAccess.cs.meta │ │ └── ECSAccess.cs │ ├── ECSAccessInner.meta │ ├── Sidekick.ECSAccess.MissingFallback.asmdef.meta │ └── Sidekick.ECSAccess.MissingFallback.asmdef ├── Helpers │ ├── ClassUtilities.cs.meta │ ├── TypeUtility.cs.meta │ ├── AttributeHelper.cs.meta │ ├── CollectionUtility.cs.meta │ ├── SidekickExtensions.cs.meta │ ├── AnimationHelper.cs.meta │ ├── SidekickEditorGUI.cs.meta │ ├── SidekickUtility.cs.meta │ ├── AnimationHelper.cs │ ├── SidekickExtensions.cs │ ├── AttributeHelper.cs │ ├── CollectionUtility.cs │ ├── SidekickUtility.cs │ ├── ClassUtilities.cs │ ├── SidekickEditorGUI.cs │ └── TypeUtility.cs ├── Dropdowns │ ├── TypeSelectDropdown.cs.meta │ ├── ECSSystemSelectDropdown.cs.meta │ ├── UnityObjectSelectDropdown.cs.meta │ ├── ECSSystemSelectDropdown.cs │ ├── UnityObjectSelectDropdown.cs │ └── TypeSelectDropdown.cs ├── ECSAccess.meta ├── Sidekick.Editor.asmdef.meta ├── Panes.meta ├── Helpers.meta ├── SidekickWindow.cs.meta ├── Panes │ ├── BasePane.cs.meta │ ├── EventPane.cs.meta │ ├── FieldPane.cs.meta │ ├── MethodPane.cs.meta │ ├── PropertyPane.cs.meta │ ├── VariablePane.cs.meta │ ├── BasePane.cs │ ├── EventPane.cs │ ├── FieldPane.cs │ ├── PropertyPane.cs │ ├── MethodPane.cs │ └── VariablePane.cs ├── PersistentData.cs.meta ├── ECSContext.cs ├── SidekickSettings.cs.meta ├── InspectionExclusions.cs.meta ├── Sidekick.Editor.asmdef ├── SidekickIcon.png.meta ├── InspectionExclusions.cs ├── PersistentData.cs ├── SelectionInfo.cs ├── SidekickSettings.cs ├── d_SidekickIcon.png.meta └── SidekickWindow.cs ├── package.json.meta ├── Editor.meta ├── LICENSE.md.meta ├── README.md.meta ├── package.json ├── LICENSE.md └── README.md /Editor/SidekickIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sabresaurus/Sidekick/HEAD/Editor/SidekickIcon.png -------------------------------------------------------------------------------- /Editor/d_SidekickIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sabresaurus/Sidekick/HEAD/Editor/d_SidekickIcon.png -------------------------------------------------------------------------------- /Editor/Dropdowns.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0bcc3d25e0e34d0b9531cd826241a7b1 3 | timeCreated: 1626105095 -------------------------------------------------------------------------------- /Editor/ECSContext.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: af9aeea6c86940cd9a057788c1d05b7d 3 | timeCreated: 1628962703 -------------------------------------------------------------------------------- /Editor/SelectionInfo.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c2df7b1eefb44f33bdfeefeb604bae18 3 | timeCreated: 1628189891 -------------------------------------------------------------------------------- /Editor/ECSAccess/ECSAccessInner/Sidekick.ECSAccess.asmref: -------------------------------------------------------------------------------- 1 | { 2 | "reference": "GUID:734d92eba21c94caba915361bd5ac177" 3 | } -------------------------------------------------------------------------------- /Editor/Helpers/ClassUtilities.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e3b1a10009aa4d0594aefc4e69c9c0d5 3 | timeCreated: 1626516372 -------------------------------------------------------------------------------- /Editor/Dropdowns/TypeSelectDropdown.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 74653a401d9849d69a9be9e29639cce7 3 | timeCreated: 1626101774 -------------------------------------------------------------------------------- /Editor/Dropdowns/ECSSystemSelectDropdown.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0af951b9382042a0a1f29801d3c4fe3c 3 | timeCreated: 1630093114 -------------------------------------------------------------------------------- /Editor/Dropdowns/UnityObjectSelectDropdown.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9bb9cd3c74824e2ab85b0398bebbeac5 3 | timeCreated: 1626105114 -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d3e9a3ecc70f2734596c8c95a0f5e372 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/ECSAccess.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4b13fbd7b242d694cbed626cab3b3111 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Sidekick.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a86b36e67faf9df4db004558524f5e6c 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/ECSAccess/ECSAccessInner.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0af1c8c39f3b941c58f7b378b6bba803 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c4b0530859219488fba5a3cfeb0904e2 3 | folderAsset: yes 4 | timeCreated: 1453304050 5 | licenseType: Pro 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/ECSAccess/Sidekick.ECSAccess.MissingFallback.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bf6a2e0c2c9ea48ec8a1a87e4d2a8882 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/Panes.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dbcb1de1736e94fbda76cffe81a38402 3 | folderAsset: yes 4 | timeCreated: 1464966494 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 25408219fbd644e9f9f39c26377c59a1 3 | timeCreated: 1532215304 4 | licenseType: Free 5 | DefaultImporter: 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 271516f7e079645bc93a84bb8bb972f2 3 | timeCreated: 1532215316 4 | licenseType: Free 5 | TextScriptImporter: 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Editor/ECSAccess/ECSAccessInner/Sidekick.ECSAccess.asmref.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9c1985e42eb4b2a4d84d792a7eb58329 3 | AssemblyDefinitionReferenceImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/Helpers.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fc020cfd712644fcdb60b827b6f74558 3 | folderAsset: yes 4 | timeCreated: 1520685499 5 | licenseType: Free 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Editor/SidekickWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2f7f686a9fc17c447986570906bc0b83 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ECSAccess/ECSAccessInner/ECSAccess.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8e407b4924a0fce459c2fa9f8e6558d0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Panes/BasePane.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1e30e047e66b049d7bd71c58da1b9ec2 3 | timeCreated: 1464968052 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/PersistentData.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bfa6987ec63f048e380d284136141d5c 3 | timeCreated: 1466785766 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Helpers/TypeUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 807a5f7a394194f168859a023bbb09dd 3 | timeCreated: 1463333952 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Panes/EventPane.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3d4ea97621609494eb2aae353778783e 3 | timeCreated: 1465316356 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Panes/FieldPane.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e86164037f63849f180c954892bbb1c6 3 | timeCreated: 1464966500 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Panes/MethodPane.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5d01fa019787b46edab261486d02c6a1 3 | timeCreated: 1464968052 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Panes/PropertyPane.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 96eb2defafbed4ba9b08e93feef6ee61 3 | timeCreated: 1464968052 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Panes/VariablePane.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a4da56c8de9614637b0d5a8241fd120d 3 | timeCreated: 1464968052 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Helpers/AttributeHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a2c0272b3dd1444ea9b6a280e3c2e262 3 | timeCreated: 1465576649 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/ECSContext.cs: -------------------------------------------------------------------------------- 1 | #if ECS_EXISTS 2 | using Unity.Entities; 3 | #endif 4 | 5 | namespace Sabresaurus.Sidekick 6 | { 7 | public class ECSContext 8 | { 9 | #if ECS_EXISTS 10 | public EntityManager EntityManager; 11 | public Entity Entity; 12 | public ComponentType ComponentType; 13 | #endif 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Editor/Helpers/CollectionUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3e257ae7ee0d34673b4adc146773a891 3 | timeCreated: 1468345689 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Helpers/SidekickExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a486b3b2dfe614879bef2e28a4116a12 3 | timeCreated: 1465575908 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/SidekickSettings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ec45aad77bb964429a06151381e44e5e 3 | timeCreated: 1523822122 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Editor/InspectionExclusions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: af22039572a534245a5b2fb195768983 3 | timeCreated: 1529799703 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Editor/Helpers/AnimationHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b4cd3d64691d14594905daa16ac78b14 3 | timeCreated: 1525301524 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Editor/Helpers/SidekickEditorGUI.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c3f16ca8c4f10b246b10a98ef2aab076 3 | timeCreated: 1524569927 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Editor/Helpers/SidekickUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 158917bcfc6d84eb1a7fafc3968b806c 3 | timeCreated: 1522626776 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.sabresaurus.sidekick", 3 | "version": "1.0.0-preview.10", 4 | "displayName": "Sidekick", 5 | "description": "Inspect private fields, properties, fire methods and more", 6 | "unity": "2020.3", 7 | "documentationUrl": "https://github.com/sabresaurus/Sidekick", 8 | "changelogUrl": "https://github.com/sabresaurus/Sidekick/commits/main", 9 | "author": { 10 | "name": "sabresaurus", 11 | "url": "https://github.com/sabresaurus" 12 | } 13 | } -------------------------------------------------------------------------------- /Editor/ECSAccess/Sidekick.ECSAccess.MissingFallback.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sidekick.ECSAccess.MissingFallback", 3 | "rootNamespace": "", 4 | "references": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": false, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [ 12 | "ECS_EXISTS" 13 | ], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Editor/Panes/BasePane.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Sabresaurus.Sidekick 4 | { 5 | public abstract class BasePane 6 | { 7 | protected static bool SearchMatches(string searchTerm, string candidateName) 8 | { 9 | if (string.IsNullOrEmpty(searchTerm)) return true; 10 | 11 | string[] split = searchTerm.Split(new[]{' '}, StringSplitOptions.RemoveEmptyEntries); 12 | 13 | foreach (string searchTermPart in split) 14 | { 15 | if (!candidateName.Contains(searchTermPart, StringComparison.InvariantCultureIgnoreCase)) 16 | return false; 17 | } 18 | return true; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Editor/Panes/EventPane.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Reflection; 4 | using UnityEditor; 5 | using UnityEngine; 6 | 7 | namespace Sabresaurus.Sidekick 8 | { 9 | public class EventPane : BasePane 10 | { 11 | // string methodOutput = ""; 12 | 13 | public void DrawEvents(Type componentType, object component, string searchTerm, FieldInfo[] events)//EventInfo[] events) 14 | { 15 | foreach (FieldInfo eventInfo in events) 16 | { 17 | if(!SearchMatches(searchTerm, eventInfo.Name)) 18 | { 19 | // Does not match search term, skip it 20 | continue; 21 | } 22 | 23 | if(eventInfo.FieldType.IsSubclassOf(typeof(Delegate))) 24 | { 25 | Delegate targetDelegate = (Delegate)eventInfo.GetValue(component); 26 | if(GUILayout.Button(eventInfo.Name)) 27 | { 28 | if(targetDelegate != null) 29 | { 30 | targetDelegate.DynamicInvoke(); 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Editor/Sidekick.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sidekick.Editor", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:d442c42f779284647ade94a5568b6139", 6 | "GUID:734d92eba21c94caba915361bd5ac177", 7 | "GUID:c38fb62397af04c51b143415e1db6d90", 8 | "GUID:a5baed0c9693541a5bd947d336ec7659", 9 | "GUID:d8b63aba1907145bea998dd612889d6b" 10 | ], 11 | "includePlatforms": [ 12 | "Editor" 13 | ], 14 | "excludePlatforms": [], 15 | "allowUnsafeCode": false, 16 | "overrideReferences": false, 17 | "precompiledReferences": [], 18 | "autoReferenced": true, 19 | "defineConstraints": [], 20 | "versionDefines": [ 21 | { 22 | "name": "com.unity.entities", 23 | "expression": "", 24 | "define": "ECS_EXISTS" 25 | }, 26 | { 27 | "name": "com.unity.mathematics", 28 | "expression": "", 29 | "define": "UNITY_MATH_EXISTS" 30 | } 31 | ], 32 | "noEngineReferences": false 33 | } -------------------------------------------------------------------------------- /Editor/Helpers/AnimationHelper.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System; 3 | 4 | namespace Sabresaurus.Sidekick 5 | { 6 | public static class AnimationHelper 7 | { 8 | private static float currentFrameTimestamp = 0; 9 | private static float currentFrameDelta = 0; 10 | 11 | private static bool animationActive = false; 12 | 13 | public static void UpdateTime() 14 | { 15 | currentFrameDelta = Time.realtimeSinceStartup - currentFrameTimestamp; 16 | currentFrameTimestamp = Time.realtimeSinceStartup; 17 | } 18 | 19 | public static float DeltaTime 20 | { 21 | get 22 | { 23 | // Cap delta time in case there's a long frame 24 | return Mathf.Min(currentFrameDelta, 0.1f); 25 | } 26 | } 27 | 28 | 29 | public static bool AnimationActive 30 | { 31 | get 32 | { 33 | return animationActive; 34 | } 35 | } 36 | 37 | public static void SetAnimationActive() 38 | { 39 | animationActive = true; 40 | } 41 | 42 | public static void ClearAnimationActive() 43 | { 44 | animationActive = false; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sabresaurus 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 | -------------------------------------------------------------------------------- /Editor/SidekickIcon.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a042d66b200724c508b2783e69b8e163 3 | timeCreated: 1464792571 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 2 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 0 11 | linearTexture: 1 12 | correctGamma: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapFadeDistanceStart: 1 16 | mipMapFadeDistanceEnd: 3 17 | bumpmap: 18 | convertToNormalMap: 0 19 | externalNormalMap: 0 20 | heightScale: 0.25 21 | normalMapFilter: 0 22 | isReadable: 0 23 | grayScaleToAlpha: 0 24 | generateCubemap: 0 25 | cubemapConvolution: 0 26 | cubemapConvolutionSteps: 7 27 | cubemapConvolutionExponent: 1.5 28 | seamlessCubemap: 0 29 | textureFormat: -3 30 | maxTextureSize: 2048 31 | textureSettings: 32 | filterMode: 1 33 | aniso: 1 34 | mipBias: -1 35 | wrapMode: 1 36 | nPOTScale: 0 37 | lightmap: 0 38 | rGBM: 0 39 | compressionQuality: 50 40 | allowsAlphaSplitting: 0 41 | spriteMode: 0 42 | spriteExtrude: 1 43 | spriteMeshType: 1 44 | alignment: 0 45 | spritePivot: {x: 0.5, y: 0.5} 46 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 47 | spritePixelsToUnits: 100 48 | alphaIsTransparency: 1 49 | spriteTessellationDetail: -1 50 | textureType: 2 51 | buildTargetSettings: [] 52 | spriteSheet: 53 | sprites: [] 54 | outline: [] 55 | spritePackingTag: 56 | userData: 57 | assetBundleName: 58 | assetBundleVariant: 59 | -------------------------------------------------------------------------------- /Editor/ECSAccess/ECSAccessInner/ECSAccess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Unity.Entities; 4 | 5 | namespace Sabresaurus.Sidekick 6 | { 7 | public unsafe class ECSAccess 8 | { 9 | public static object GetComponentData(EntityManager entityManager, Entity entity, ComponentType componentType) 10 | { 11 | EntityDataAccess* dataAccess = entityManager.GetCheckedEntityDataAccess(); 12 | EntityComponentStore* entityComponentStore = dataAccess->EntityComponentStore; 13 | byte* ptr = entityComponentStore->GetComponentDataWithTypeRO(entity, componentType.TypeIndex); 14 | 15 | return Marshal.PtrToStructure((IntPtr) ptr, componentType.GetManagedType()); 16 | } 17 | 18 | public static void SetComponentData(EntityManager entityManager, Entity entity, ComponentType componentType, object componentData) 19 | => SetComponentData(entityManager, entity, componentType.TypeIndex, componentData); 20 | 21 | public static void SetComponentData(EntityManager entityManager, Entity entity, int typeIndex, object componentData) 22 | { 23 | EntityDataAccess* dataAccess = entityManager.GetCheckedEntityDataAccess(); 24 | 25 | if (!dataAccess->IsInExclusiveTransaction) 26 | dataAccess->DependencyManager->CompleteReadAndWriteDependency(typeIndex); 27 | 28 | EntityComponentStore* entityComponentStore = dataAccess->EntityComponentStore; 29 | 30 | byte* ptr = entityComponentStore->GetComponentDataWithTypeRW(entity, typeIndex, entityComponentStore->GlobalSystemVersion); 31 | 32 | Marshal.StructureToPtr(componentData, (IntPtr) ptr, false); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Editor/Helpers/SidekickExtensions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections; 3 | using System; 4 | 5 | namespace Sabresaurus.Sidekick 6 | { 7 | public static class SidekickExtensions 8 | { 9 | public static RectOffset SetLeft(this RectOffset rectOffset, int newValue) 10 | { 11 | rectOffset.left = newValue; 12 | return rectOffset; 13 | } 14 | 15 | public static RectOffset SetRight(this RectOffset rectOffset, int newValue) 16 | { 17 | rectOffset.right = newValue; 18 | return rectOffset; 19 | } 20 | 21 | public static bool Contains(this string source, string value, StringComparison comparison) 22 | { 23 | return source.IndexOf(value, comparison) >= 0; 24 | } 25 | 26 | public static bool HasFlagByte(this Enum mask, Enum flags) // Same behavior than Enum.HasFlag in .NET 4 27 | { 28 | return ((byte)(IConvertible)mask & (byte)(IConvertible)flags) == (byte)(IConvertible)flags; 29 | } 30 | 31 | public static string RemoveStart(this string input, string toRemove) 32 | { 33 | if(input.StartsWith(toRemove, StringComparison.InvariantCultureIgnoreCase)) 34 | { 35 | input = input.Remove(0, toRemove.Length); 36 | } 37 | 38 | return input; 39 | } 40 | 41 | public static string RemoveEnd(this string input, string toRemove) 42 | { 43 | if(input.EndsWith(toRemove, StringComparison.InvariantCultureIgnoreCase)) 44 | { 45 | input = input.Remove(input.Length - toRemove.Length, toRemove.Length); 46 | } 47 | 48 | return input; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Editor/InspectionExclusions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | #if ECS_EXISTS 5 | using Unity.Entities; 6 | #endif 7 | using UnityEngine; 8 | 9 | namespace Sabresaurus.Sidekick 10 | { 11 | /// 12 | /// Specifies types for Sidekick to ignore when inspecting objects 13 | /// 14 | public static class InspectionExclusions 15 | { 16 | public static List GetExcludedTypes() 17 | { 18 | return new List() 19 | { 20 | // Built in exclusions 21 | typeof(System.Object), 22 | typeof(UnityEngine.Object), 23 | typeof(UnityEngine.Component), 24 | typeof(UnityEngine.Behaviour), 25 | typeof(UnityEngine.MonoBehaviour), 26 | typeof(UnityEngine.ScriptableObject), 27 | #if ECS_EXISTS 28 | typeof(ValueType), 29 | #endif 30 | // Add any custom exclusions here 31 | }; 32 | } 33 | 34 | public static bool IsPropertyExcluded(Type componentType, PropertyInfo property) 35 | { 36 | // Will instantiate at edit time 37 | if (componentType == typeof(MeshFilter) && property.Name == "mesh") return true; 38 | if ((componentType == typeof(Renderer) || componentType.IsSubclassOf(typeof(Renderer))) && property.Name == "material") return true; 39 | if ((componentType == typeof(Renderer) || componentType.IsSubclassOf(typeof(Renderer))) && property.Name == "materials") return true; 40 | 41 | // Will result in assertions if the matrix fails ValidTRS 42 | if (componentType == typeof(Matrix4x4) && property.Name == "rotation") return true; 43 | if (componentType == typeof(Matrix4x4) && property.Name == "lossyScale") return true; 44 | return false; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Editor/Helpers/AttributeHelper.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections; 3 | 4 | namespace Sabresaurus.Sidekick 5 | { 6 | public static class AttributeHelper 7 | { 8 | public static bool IsSerializable(object[] customAttributes) 9 | { 10 | if(customAttributes == null || customAttributes.Length == 0) 11 | { 12 | return false; 13 | } 14 | // Find any attributes that are SerializeField 15 | for (int i = 0; i < customAttributes.Length; i++) 16 | { 17 | if(customAttributes[i] is SerializeField) 18 | { 19 | return true; 20 | } 21 | } 22 | // No attributes found, assume it can't be serialized 23 | return false; 24 | } 25 | 26 | public static bool IsObsolete(object[] customAttributes) 27 | { 28 | if (customAttributes == null || customAttributes.Length == 0) 29 | { 30 | return false; 31 | } 32 | // Find any attributes that are Obsolete 33 | for (int i = 0; i < customAttributes.Length; i++) 34 | { 35 | if (customAttributes[i] is System.ObsoleteAttribute) 36 | { 37 | return true; 38 | } 39 | } 40 | // No attributes found, assume it's ok 41 | return false; 42 | } 43 | 44 | public static bool IsObsoleteWithError(object[] customAttributes) 45 | { 46 | if(customAttributes == null || customAttributes.Length == 0) 47 | { 48 | return false; 49 | } 50 | // Find any attributes that are Obsolete 51 | for (int i = 0; i < customAttributes.Length; i++) 52 | { 53 | if(customAttributes[i] is System.ObsoleteAttribute) 54 | { 55 | // If the attribute is marked to trigger an error 56 | if(((System.ObsoleteAttribute)customAttributes[i]).IsError) 57 | { 58 | return true; 59 | } 60 | } 61 | } 62 | // No attributes found, assume it's ok 63 | return false; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Editor/PersistentData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEngine; 5 | 6 | namespace Sabresaurus.Sidekick 7 | { 8 | [Serializable] 9 | public class MethodSetup : ISerializationCallbackReceiver 10 | { 11 | public string MethodIdentifier = ""; 12 | 13 | public object[] Values = new object[0]; 14 | 15 | public Type[] GenericArguments = new Type[0]; 16 | 17 | private string[] GenericArgumentsString; 18 | 19 | public void OnBeforeSerialize() 20 | { 21 | // Only simple types can be serialised, so convert the type to an assembly-qualified type name 22 | GenericArgumentsString = new string[GenericArguments.Length]; 23 | for (int i = 0; i < GenericArguments.Length; i++) 24 | { 25 | if (GenericArguments[i] != null) 26 | { 27 | GenericArgumentsString[i] = GenericArguments[i].AssemblyQualifiedName; 28 | } 29 | } 30 | } 31 | 32 | public void OnAfterDeserialize() 33 | { 34 | GenericArguments = new Type[GenericArgumentsString.Length]; 35 | for (int i = 0; i < GenericArgumentsString.Length; i++) 36 | { 37 | if (!string.IsNullOrEmpty(GenericArgumentsString[i])) 38 | { 39 | // Convert the assembly-qualified type name back to a proper type 40 | GenericArguments[i] = Type.GetType(GenericArgumentsString[i]); 41 | } 42 | } 43 | } 44 | } 45 | 46 | /// 47 | /// This class allows various utilities to store data that will persist through recompiles 48 | /// 49 | [Serializable] 50 | public class PersistentData 51 | { 52 | // MethodPane 53 | public List ExpandedMethods = new List(); 54 | public HashSet ExpandedFields = new HashSet(); 55 | } 56 | } -------------------------------------------------------------------------------- /Editor/Dropdowns/ECSSystemSelectDropdown.cs: -------------------------------------------------------------------------------- 1 | #if ECS_EXISTS 2 | using System; 3 | using Unity.Entities; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | 7 | namespace Sabresaurus.Sidekick 8 | { 9 | public class ECSSystemSelectDropdown : AdvancedDropdown 10 | { 11 | private readonly Action onObjectSelected; 12 | 13 | class ECSSystemDropdownItem : AdvancedDropdownItem 14 | { 15 | public ComponentSystemBase System { get; } 16 | 17 | public ECSSystemDropdownItem(ComponentSystemBase system) : base(GetDisplayName(system)) 18 | { 19 | System = system; 20 | } 21 | 22 | private static string GetDisplayName(ComponentSystemBase system) 23 | { 24 | return system.ToString(); 25 | } 26 | } 27 | 28 | public ECSSystemSelectDropdown(AdvancedDropdownState state, Action onObjectSelectedCallback) : base(state) 29 | { 30 | Vector2 customMinimumSize = minimumSize; 31 | customMinimumSize.y = 250; 32 | minimumSize = customMinimumSize; 33 | 34 | onObjectSelected = onObjectSelectedCallback; 35 | } 36 | 37 | protected override AdvancedDropdownItem BuildRoot() 38 | { 39 | AdvancedDropdownItem root = new AdvancedDropdownItem("Unity Objects"); 40 | 41 | foreach (World world in World.All) 42 | { 43 | foreach (ComponentSystemBase componentSystemBase in world.Systems) 44 | { 45 | root.AddChild(new ECSSystemDropdownItem(componentSystemBase)); 46 | } 47 | } 48 | 49 | return root; 50 | } 51 | 52 | protected override void ItemSelected(AdvancedDropdownItem item) 53 | { 54 | if (item is ECSSystemDropdownItem windowDropdownItem) 55 | { 56 | onObjectSelected(windowDropdownItem.System); 57 | } 58 | } 59 | } 60 | } 61 | #endif -------------------------------------------------------------------------------- /Editor/SelectionInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Sabresaurus.Sidekick 4 | { 5 | readonly struct SelectionInfo : IEquatable 6 | { 7 | // Note we need to be able to differentiate selecting a RuntimeType from wanting to see static methods on the type it represents 8 | public readonly Type Type; 9 | public readonly object Object; 10 | 11 | public SelectionInfo(Type type) 12 | { 13 | Type = type; 14 | Object = null; 15 | } 16 | 17 | public SelectionInfo(object o) 18 | { 19 | Object = o; 20 | Type = null; 21 | } 22 | 23 | public bool IsEmpty 24 | { 25 | get 26 | { 27 | if (Type != null) return false; 28 | 29 | // For UnityEngine.Object the backing object may be null so need to call the overriden equals operator to check that 30 | if (Object != null && !(Object is UnityEngine.Object castUnityObject && castUnityObject == null)) return false; 31 | 32 | return true; 33 | } 34 | } 35 | 36 | public string GetDisplayName() 37 | { 38 | if (Type != null) 39 | { 40 | return Type.Name; 41 | } 42 | 43 | if (Object != null) 44 | { 45 | return Object.ToString(); 46 | } 47 | 48 | return "Unknown"; 49 | } 50 | 51 | 52 | public bool Equals(SelectionInfo other) 53 | { 54 | return Type == other.Type && Equals(Object, other.Object); 55 | } 56 | 57 | public override bool Equals(object obj) 58 | { 59 | return obj is SelectionInfo other && Equals(other); 60 | } 61 | 62 | public override int GetHashCode() 63 | { 64 | unchecked 65 | { 66 | return ((Type != null ? Type.GetHashCode() : 0) * 397) ^ (Object != null ? Object.GetHashCode() : 0); 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Editor/SidekickSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace Sabresaurus.Sidekick 6 | { 7 | public static class SidekickSettings 8 | { 9 | public static bool HideAutoGenerated 10 | { 11 | get => EditorPrefs.GetBool("SidekickSettings.HideAutoGenerated", true); 12 | set => EditorPrefs.SetBool("SidekickSettings.HideAutoGenerated", value); 13 | } 14 | 15 | public static bool PreferUnityAttributes 16 | { 17 | get => EditorPrefs.GetBool("SidekickSettings.PreferUnityAttributes", false); 18 | set => EditorPrefs.SetBool("SidekickSettings.PreferUnityAttributes", value); 19 | } 20 | } 21 | 22 | // Register a SettingsProvider using IMGUI for the drawing framework: 23 | static class SidekickSettingsRegister 24 | { 25 | public const string SETTINGS_PATH = "Preferences/Sidekick"; 26 | private static readonly GUIContent HideAutoGenerated = new GUIContent("Hide Auto Generated", "Whether automatically generated get/set methods and backing fields are hidden"); 27 | private static readonly GUIContent PreferUnityAttributes = new GUIContent("Prefer Unity Attributes", ""); 28 | 29 | [SettingsProvider] 30 | public static SettingsProvider CreateMyCustomSettingsProvider() 31 | { 32 | var provider = new SettingsProvider(SETTINGS_PATH, SettingsScope.User) 33 | { 34 | // Create the SettingsProvider and initialize its drawing (IMGUI) function in place: 35 | guiHandler = _ => 36 | { 37 | SidekickSettings.HideAutoGenerated = EditorGUILayout.Toggle(HideAutoGenerated, SidekickSettings.HideAutoGenerated); 38 | SidekickSettings.PreferUnityAttributes = EditorGUILayout.Toggle(PreferUnityAttributes, SidekickSettings.PreferUnityAttributes); 39 | }, 40 | 41 | // Populate the search keywords to enable smart search filtering and label highlighting: 42 | keywords = new HashSet(new[] {HideAutoGenerated.text, PreferUnityAttributes.text}) 43 | }; 44 | 45 | return provider; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Editor/Helpers/CollectionUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System; 3 | 4 | namespace Sabresaurus.Sidekick 5 | { 6 | public static class CollectionUtility 7 | { 8 | public static IList Resize(IList list, bool isArray, Type fieldType, Type elementType, int newSize) 9 | { 10 | var oldSize = list != null ? list.Count : 0; 11 | 12 | // Unity's default behaviour when expanding a collection is to use the last element as the new element value 13 | object objectToAdd = null; 14 | if (newSize > oldSize) 15 | { 16 | if (oldSize >= 1) 17 | { 18 | objectToAdd = list[oldSize - 1]; 19 | } 20 | else 21 | { 22 | objectToAdd = Activator.CreateInstance(elementType); 23 | } 24 | } 25 | 26 | if (isArray) 27 | { 28 | Array newArray = Array.CreateInstance(elementType, newSize); 29 | if (oldSize != 0) 30 | { 31 | Array.Copy((Array)list, newArray, Math.Min(oldSize, newArray.Length)); 32 | } 33 | 34 | if (newSize > oldSize) 35 | { 36 | int toAdd = newSize - oldSize; 37 | for (int i = 0; i < toAdd; i++) 38 | { 39 | newArray.SetValue(objectToAdd, oldSize + i); 40 | } 41 | } 42 | 43 | return newArray; 44 | } 45 | else 46 | { 47 | if (oldSize == 0) 48 | { 49 | list = (IList) Activator.CreateInstance(fieldType); 50 | } 51 | if (newSize > oldSize) 52 | { 53 | int toAdd = newSize - oldSize; 54 | for (int i = 0; i < toAdd; i++) 55 | { 56 | list.Add(objectToAdd); 57 | } 58 | } 59 | else if (newSize < oldSize) 60 | { 61 | int toRemove = oldSize - newSize; 62 | for (int i = 0; i < toRemove; i++) 63 | { 64 | list.RemoveAt(oldSize - i - 1); 65 | } 66 | } 67 | 68 | return list; 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Editor/Panes/FieldPane.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System; 3 | using UnityEngine; 4 | 5 | namespace Sabresaurus.Sidekick 6 | { 7 | public class FieldPane : VariablePane 8 | { 9 | public void DrawFields(Type componentType, object component, ECSContext ecsContext, string searchTerm, FieldInfo[] fields) 10 | { 11 | foreach (FieldInfo field in fields) 12 | { 13 | string fieldName = field.Name; 14 | 15 | if(!SearchMatches(searchTerm, fieldName)) 16 | { 17 | // Does not match search term, skip it 18 | continue; 19 | } 20 | 21 | Type fieldType = field.FieldType; 22 | 23 | VariableAttributes variableAttributes = VariableAttributes.None; 24 | 25 | // See https://stackoverflow.com/a/10261848 26 | if (field.IsLiteral && !field.IsInitOnly) 27 | { 28 | variableAttributes = VariableAttributes.Constant; 29 | 30 | // Prevent SetValue as it will result in a FieldAccessException 31 | variableAttributes |= VariableAttributes.ReadOnly; 32 | } 33 | if (field.IsStatic) 34 | { 35 | variableAttributes |= VariableAttributes.Static; 36 | } 37 | 38 | string tooltip = TypeUtility.GetTooltip(field, variableAttributes); 39 | 40 | if ((variableAttributes & VariableAttributes.ReadOnly) != 0) 41 | { 42 | GUI.enabled = false; 43 | } 44 | 45 | DrawVariable(fieldType, fieldName, component != null ? field.GetValue(component) : null, tooltip, variableAttributes, field.GetCustomAttributes(), true, componentType, newValue => 46 | { 47 | if ((variableAttributes & VariableAttributes.ReadOnly) == 0) 48 | { 49 | field.SetValue(component, newValue); 50 | 51 | #if ECS_EXISTS 52 | if(ecsContext != null) 53 | { 54 | ECSAccess.SetComponentData(ecsContext.EntityManager, ecsContext.Entity, ecsContext.ComponentType, component); 55 | } 56 | #endif 57 | } 58 | }); 59 | 60 | if ((variableAttributes & VariableAttributes.ReadOnly) != 0) 61 | { 62 | GUI.enabled = true; 63 | } 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /Editor/d_SidekickIcon.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fb93cfdfbd26141e7ad504283fbe5643 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | vTOnly: 0 27 | grayScaleToAlpha: 0 28 | generateCubemap: 6 29 | cubemapConvolution: 0 30 | seamlessCubemap: 0 31 | textureFormat: 1 32 | maxTextureSize: 2048 33 | textureSettings: 34 | serializedVersion: 2 35 | filterMode: 1 36 | aniso: 1 37 | mipBias: 0 38 | wrapU: 1 39 | wrapV: 1 40 | wrapW: 0 41 | nPOTScale: 0 42 | lightmap: 0 43 | compressionQuality: 50 44 | spriteMode: 0 45 | spriteExtrude: 1 46 | spriteMeshType: 1 47 | alignment: 0 48 | spritePivot: {x: 0.5, y: 0.5} 49 | spritePixelsToUnits: 100 50 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 51 | spriteGenerateFallbackPhysicsShape: 1 52 | alphaUsage: 1 53 | alphaIsTransparency: 1 54 | spriteTessellationDetail: -1 55 | textureType: 2 56 | textureShape: 1 57 | singleChannelComponent: 0 58 | flipbookRows: 1 59 | flipbookColumns: 1 60 | maxTextureSizeSet: 0 61 | compressionQualitySet: 0 62 | textureFormatSet: 0 63 | ignorePngGamma: 0 64 | applyGammaDecoding: 0 65 | platformSettings: 66 | - serializedVersion: 3 67 | buildTarget: DefaultTexturePlatform 68 | maxTextureSize: 2048 69 | resizeAlgorithm: 0 70 | textureFormat: -1 71 | textureCompression: 0 72 | compressionQuality: 50 73 | crunchedCompression: 0 74 | allowsAlphaSplitting: 0 75 | overridden: 0 76 | androidETC2FallbackOverride: 0 77 | forceMaximumCompressionQuality_BC6H_BC7: 0 78 | - serializedVersion: 3 79 | buildTarget: Standalone 80 | maxTextureSize: 2048 81 | resizeAlgorithm: 0 82 | textureFormat: -1 83 | textureCompression: 0 84 | compressionQuality: 50 85 | crunchedCompression: 0 86 | allowsAlphaSplitting: 0 87 | overridden: 0 88 | androidETC2FallbackOverride: 0 89 | forceMaximumCompressionQuality_BC6H_BC7: 0 90 | spriteSheet: 91 | serializedVersion: 2 92 | sprites: [] 93 | outline: [] 94 | physicsShape: [] 95 | bones: [] 96 | spriteID: 97 | internalID: 0 98 | vertices: [] 99 | indices: 100 | edges: [] 101 | weights: [] 102 | secondaryTextures: [] 103 | spritePackingTag: 104 | pSDRemoveMatte: 0 105 | pSDShowRemoveMatteOption: 0 106 | userData: 107 | assetBundleName: 108 | assetBundleVariant: 109 | -------------------------------------------------------------------------------- /Editor/Panes/PropertyPane.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System.Collections; 4 | using System.Reflection; 5 | using System; 6 | 7 | namespace Sabresaurus.Sidekick 8 | { 9 | public class PropertyPane : VariablePane 10 | { 11 | public void DrawProperties(Type componentType, object component, string searchTerm, PropertyInfo[] properties) 12 | { 13 | foreach (var property in properties) 14 | { 15 | if(property.DeclaringType == typeof(Component) 16 | || property.DeclaringType == typeof(UnityEngine.Object)) 17 | { 18 | continue; 19 | } 20 | 21 | if(!SearchMatches(searchTerm, property.Name)) 22 | { 23 | // Does not match search term, skip it 24 | continue; 25 | } 26 | 27 | if (property.GetIndexParameters().Length != 0) 28 | { 29 | // Indexer, show it in Methods instead as it takes parameters 30 | continue; 31 | } 32 | 33 | MethodInfo getMethod = property.GetGetMethod(true); 34 | MethodInfo setMethod = property.GetSetMethod(true); 35 | 36 | object[] attributes = property.GetCustomAttributes(false); 37 | 38 | VariableAttributes variableAttributes = VariableAttributes.None; 39 | 40 | if (getMethod != null && getMethod.IsStatic || setMethod != null && setMethod.IsStatic) 41 | { 42 | variableAttributes |= VariableAttributes.Static; 43 | } 44 | 45 | if (setMethod == null) 46 | { 47 | variableAttributes |= VariableAttributes.ReadOnly; 48 | } 49 | 50 | if (getMethod == null) 51 | { 52 | variableAttributes |= VariableAttributes.WriteOnly; 53 | } 54 | 55 | string tooltip = TypeUtility.GetTooltip(property, variableAttributes); 56 | 57 | if (getMethod == null) 58 | { 59 | EditorGUILayout.LabelField(new GUIContent(property.Name, tooltip), new GUIContent("No get method", SidekickEditorGUI.ErrorIconSmall)); 60 | } 61 | else if (InspectionExclusions.IsPropertyExcluded(componentType, property)) 62 | { 63 | EditorGUILayout.LabelField(new GUIContent(property.Name, tooltip), new GUIContent("Excluded due to rule", SidekickEditorGUI.ErrorIconSmall, "See InspectionExclusions.cs")); 64 | } 65 | else if (AttributeHelper.IsObsoleteWithError(attributes)) 66 | { 67 | // Don't try to get the value of properties that error on access 68 | EditorGUILayout.LabelField(new GUIContent(property.Name, tooltip), new GUIContent("[Obsolete] error", SidekickEditorGUI.ErrorIconSmall)); 69 | } 70 | else 71 | { 72 | object oldValue = null; 73 | Exception error = null; 74 | try 75 | { 76 | oldValue = getMethod.Invoke(component, null); 77 | } 78 | catch (Exception e) 79 | { 80 | error = e; 81 | } 82 | 83 | if (error != null) 84 | { 85 | EditorGUILayout.LabelField(new GUIContent(property.Name, tooltip), new GUIContent(error.GetType().Name, SidekickEditorGUI.ErrorIconSmall)); 86 | } 87 | else 88 | { 89 | DrawVariable(property.PropertyType, property.Name, oldValue, tooltip, variableAttributes, property.GetCustomAttributes(), true, componentType, newValue => 90 | { 91 | setMethod?.Invoke(component, new[] {newValue}); 92 | }); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sidekick Inspection Tools for Unity 2 | 3 | [![openupm](https://img.shields.io/npm/v/com.sabresaurus.sidekick?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.sabresaurus.sidekick/) [![GitHub](https://img.shields.io/github/license/sabresaurus/Sidekick)](https://github.com/sabresaurus/Sidekick/blob/master/LICENSE.md) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-blue.svg)](http://makeapullrequest.com) 4 | 5 | 6 | 7 | Sidekick is a set of tools that allow you to edit fields, properties and invoke methods in Unity's editor. It extends Unity's philosophy of real-time run-time editing and inspection by allowing you to edit much more than just serialised fields. 8 | 9 | ## Edit fields and properties 10 | Inspect and edit fields and properties on components, including statics and many more than natively supported in Unity's inspector 11 | 12 | ## Inspect hidden objects 13 | 14 | Selection helpers allow you to select hidden assets that can't be selected in the Project window as well as runtime objects (such as Editor Windows, Scene Views, custom Editors, ECS Systems, etc) allowing you to debug and inspect all sorts of editor nuances. 15 | 16 | ## Modify Entities (ECS) 17 | 18 | Sidekick supports writing to the fields on ECS Entities, simply select an entity in the Entity Debugger with Sidekick open and the fields will be displayed and change be dynamically changed. 19 | 20 | ## Fire methods and events 21 | 22 | Fire arbitrary methods on components with support for parameters and return types 23 | 24 | # License 25 | 26 | Sidekick is licensed under MIT, see **LICENSE** for details. 27 | 28 | # Installation 29 | 30 |
31 | Add from OpenUPM | via scoped registry, recommended 32 | 33 | This package is available on OpenUPM: https://openupm.com/packages/com.sabresaurus.sidekick 34 | 35 | To add it the package to your project: 36 | 37 | - open `Edit/Project Settings/Package Manager` 38 | - add a new Scoped Registry: 39 | ``` 40 | Name: OpenUPM 41 | URL: https://package.openupm.com/ 42 | Scope(s): com.sabresaurus 43 | ``` 44 | - click Save 45 | - open Package Manager 46 | - click + 47 | - select Add from Git URL 48 | - paste `com.sabresaurus.sidekick` 49 | - click Add 50 |
51 | 52 |
53 | Add from GitHub | not recommended, no updates through UPM 54 | 55 | You can also add it directly from GitHub on Unity 2020.3+. Note that you won't be able to receive updates through Package Manager this way, you'll have to update manually. 56 | 57 | - open Package Manager 58 | - click + 59 | - select Add from Git URL 60 | - paste `https://github.com/sabresaurus/Sidekick.git` 61 | - click Add 62 | **or** 63 | - Edit your `Packages/manifest.json` file to contain `"com.sabresaurus.sidekick": "https://github.com/sabresaurus/Sidekick.git"`, 64 | 65 | To update the package with new changes, remove the lock from the `Packages/packages-lock.json` file. 66 |
67 | 68 | To open Sidekick go to `Window → Sidekick` 69 | 70 | # Remote Actions 71 | 72 | [Previously](https://github.com/sabresaurus/Sidekick/tree/pre-remote-removal) we were hoping to include to make Sidekick work with remote builds, this however massively complicated the simplicity of Sidekick and the project has been abandoned. The network code has been split out into [Remote Actions](https://github.com/sabresaurus/Remote-Actions) 73 | -------------------------------------------------------------------------------- /Editor/Helpers/SidekickUtility.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System; 3 | //using System.Linq; 4 | using System.Text; 5 | using UnityEditor; 6 | 7 | namespace Sabresaurus.Sidekick 8 | { 9 | public static class SidekickUtility 10 | { 11 | /// 12 | /// Find the path to where InspectorSidekick is in the project 13 | /// 14 | /// The installed path of InspectorSidekick. 15 | //public static string GetInstallPath() 16 | //{ 17 | // // Find all the scripts with CSGModel in their name 18 | // string[] guids = AssetDatabase.FindAssets("InspectorSidekick t:Script"); 19 | 20 | // foreach (string guid in guids) 21 | // { 22 | // // Find the path of the file 23 | // string path = AssetDatabase.GUIDToAssetPath(guid); 24 | 25 | // string suffix = "Editor/InspectorSidekick.cs"; 26 | // // If it is the target file, i.e. CSGModel.cs not CSGModelInspector 27 | // if(path.EndsWith(suffix)) 28 | // { 29 | // // Remove the suffix, to get for example Assets/SabreCSG 30 | // path = path.Remove(path.Length-suffix.Length, suffix.Length); 31 | 32 | // return path; 33 | // } 34 | // } 35 | 36 | // // None matched 37 | // return string.Empty; 38 | //} 39 | 40 | public static T EnumToolbar(T value, GUIStyle style = null, params GUILayoutOption[] options) where T : struct, IConvertible 41 | { 42 | if (!typeof(T).IsEnum) 43 | { 44 | throw new ArgumentException("EnumToolbar must be passed an enum"); 45 | } 46 | 47 | if(style == null) 48 | { 49 | style = GUI.skin.button; 50 | } 51 | 52 | string[] names = Enum.GetNames(typeof(T)); 53 | int oldValue = Array.IndexOf(names, value.ToString()); 54 | 55 | for (int i = 0; i < names.Length; i++) 56 | { 57 | names[i] = NicifyIdentifier(names[i]); 58 | } 59 | int newValue = GUILayout.Toolbar(oldValue, names, style, options); 60 | 61 | return (T)Enum.ToObject(typeof(T), newValue); 62 | } 63 | 64 | public static string NicifyIdentifier(string input) 65 | { 66 | input = input.RemoveStart("m_"); 67 | input = input.RemoveStart("s_"); 68 | input = input.RemoveStart("c_"); 69 | input = input.RemoveStart("_"); 70 | 71 | StringBuilder stringBuilder = new StringBuilder(); 72 | 73 | for (int i = 0; i < input.Length; i++) 74 | { 75 | char currentChar = input[i]; 76 | // If we've just started an uppercase (not at the start of the string) then prepend a space 77 | if(i > 0 && Char.IsUpper(currentChar) && !Char.IsUpper(input[i-1])) 78 | { 79 | stringBuilder.Append(' '); 80 | } 81 | // Make sure the first character is capitalised 82 | if(i == 0) 83 | { 84 | currentChar = Char.ToUpper(currentChar); 85 | } 86 | 87 | if (currentChar == '_') 88 | { 89 | stringBuilder.Append(' '); 90 | } 91 | else 92 | { 93 | stringBuilder.Append(currentChar); 94 | } 95 | } 96 | 97 | return stringBuilder.ToString(); 98 | } 99 | 100 | public static bool EventsMatch(Event event1, Event event2, bool ignoreShift, bool ignoreFunction) 101 | { 102 | EventModifiers modifiers1 = event1.modifiers; 103 | EventModifiers modifiers2 = event2.modifiers; 104 | 105 | // Ignore capslock from either modifier 106 | modifiers1 &= (~EventModifiers.CapsLock); 107 | modifiers2 &= (~EventModifiers.CapsLock); 108 | 109 | if(ignoreShift) 110 | { 111 | // Ignore shift from either modifier 112 | modifiers1 &= (~EventModifiers.Shift); 113 | modifiers2 &= (~EventModifiers.Shift); 114 | } 115 | 116 | // If key code and modifier match 117 | if(event1.keyCode == event2.keyCode 118 | && (modifiers1 == modifiers2)) 119 | { 120 | return true; 121 | } 122 | 123 | return false; 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /Editor/Dropdowns/UnityObjectSelectDropdown.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace Sabresaurus.Sidekick 9 | { 10 | public class UnityObjectSelectDropdown : AdvancedDropdown 11 | { 12 | private readonly Action onObjectSelected; 13 | 14 | class UnityObjectDropdownItem : AdvancedDropdownItem 15 | { 16 | public Object UnityObject { get; } 17 | 18 | public UnityObjectDropdownItem(Object unityObject) : base(GetDisplayName(unityObject)) 19 | { 20 | UnityObject = unityObject; 21 | } 22 | 23 | private static string GetDisplayName(Object unityObject) 24 | { 25 | if (unityObject is EditorWindow window) 26 | { 27 | return $"{window.titleContent.text} ({unityObject.GetType().FullName})"; 28 | } 29 | 30 | return $"{unityObject.name} ({unityObject.GetType().FullName})"; 31 | } 32 | } 33 | 34 | public UnityObjectSelectDropdown(AdvancedDropdownState state, Action onObjectSelectedCallback) : base(state) 35 | { 36 | Vector2 customMinimumSize = minimumSize; 37 | customMinimumSize.y = 250; 38 | minimumSize = customMinimumSize; 39 | 40 | onObjectSelected = onObjectSelectedCallback; 41 | } 42 | 43 | protected override AdvancedDropdownItem BuildRoot() 44 | { 45 | AdvancedDropdownItem root = new AdvancedDropdownItem("Unity Objects"); 46 | 47 | AdvancedDropdownItem editorWindowsItem = new AdvancedDropdownItem("Editor Windows"); 48 | root.AddChild(editorWindowsItem); 49 | 50 | var editorWindows= Resources.FindObjectsOfTypeAll().OrderBy(window => window.titleContent.text); 51 | 52 | foreach (EditorWindow editorWindow in editorWindows) 53 | { 54 | if (editorWindow.GetType().FullName == "UnityEditor.IMGUI.Controls.AdvancedDropdownWindow") 55 | { 56 | // No point selecting the current dropdown as the selection closes it making it null 57 | continue; 58 | } 59 | editorWindowsItem.AddChild(new UnityObjectDropdownItem(editorWindow)); 60 | } 61 | 62 | AdvancedDropdownItem editorsItem = new AdvancedDropdownItem("Editors"); 63 | root.AddChild(editorsItem); 64 | 65 | var editors = Resources.FindObjectsOfTypeAll().OrderBy(editor => editor.name); 66 | 67 | foreach (Editor editor in editors) 68 | { 69 | editorsItem.AddChild(new UnityObjectDropdownItem(editor)); 70 | } 71 | 72 | // This will include loaded assets, internal Unity objects and more 73 | var allUnityObjects = Resources.FindObjectsOfTypeAll(); 74 | 75 | 76 | 77 | AdvancedDropdownItem hardToAccessAssetsItem = new AdvancedDropdownItem("Hidden Assets"); 78 | root.AddChild(hardToAccessAssetsItem); 79 | 80 | AdvancedDropdownItem runtimeObjectsItem = new AdvancedDropdownItem("Runtime Objects"); 81 | root.AddChild(runtimeObjectsItem); 82 | 83 | foreach (var unityObject in allUnityObjects) 84 | { 85 | var assetPath = AssetDatabase.GetAssetPath(unityObject); 86 | 87 | if (!string.IsNullOrEmpty(assetPath)) 88 | { 89 | if (!assetPath.StartsWith("Packages/") && !assetPath.StartsWith("Assets/")) 90 | { 91 | hardToAccessAssetsItem.AddChild(new UnityObjectDropdownItem(unityObject)); 92 | } 93 | } 94 | else 95 | { 96 | if (!editorWindows.Contains(unityObject) && !editors.Contains(unityObject)) 97 | { 98 | runtimeObjectsItem.AddChild(new UnityObjectDropdownItem(unityObject)); 99 | } 100 | } 101 | } 102 | 103 | return root; 104 | } 105 | 106 | protected override void ItemSelected(AdvancedDropdownItem item) 107 | { 108 | if (item is UnityObjectDropdownItem windowDropdownItem) 109 | { 110 | onObjectSelected(windowDropdownItem.UnityObject); 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /Editor/Helpers/ClassUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | #if ECS_EXISTS 4 | using Unity.Entities; 5 | using Unity.Transforms; 6 | #endif 7 | using UnityEditor; 8 | using UnityEngine; 9 | using Object = UnityEngine.Object; 10 | 11 | namespace Sabresaurus.Sidekick 12 | { 13 | public static class ClassUtilities 14 | { 15 | public static GenericMenu GetMenu(object o, ECSContext inspectedECSContext) 16 | { 17 | var menu = new GenericMenu(); 18 | 19 | if (TypeUtility.IsNull(o)) 20 | { 21 | return menu; 22 | } 23 | 24 | if (o is MonoScript monoScript) 25 | { 26 | Type classType = monoScript.GetClass(); 27 | if (classType != null) 28 | { 29 | if (classType.IsSubclassOf(typeof(ScriptableObject))) 30 | { 31 | menu.AddItem(new GUIContent("Instantiate Asset"), false, InstantiateScript, classType); 32 | } 33 | else if (classType.IsSubclassOf(typeof(Component))) 34 | { 35 | menu.AddItem(new GUIContent("Instantiate In Scene"), false, InstantiateScript, classType); 36 | } 37 | } 38 | } 39 | 40 | if (o is GameObject gameObject) 41 | { 42 | menu.AddItem(new GUIContent("Set Name From First Script"), false, SetNameFromFirstScript, gameObject); 43 | } 44 | 45 | if (o is Object unityObject) 46 | { 47 | menu.AddItem(new GUIContent("Set Name From Type"), false, () => unityObject.name = o.GetType().Name); 48 | } 49 | 50 | if (o is Texture2D) 51 | { 52 | menu.AddItem(new GUIContent("Export Texture"), false, ExportTexture, o); 53 | } 54 | 55 | if (o is Component component) 56 | { 57 | menu.AddItem(new GUIContent("Remove Component"), false, () => 58 | { 59 | Undo.DestroyObjectImmediate(component); 60 | }); 61 | } 62 | 63 | #if ECS_EXISTS 64 | if (inspectedECSContext != null && o.GetType().GetInterfaces().Contains(typeof(IComponentData))) 65 | { 66 | menu.AddItem(new GUIContent("Remove Component"), false, () => 67 | { 68 | inspectedECSContext.EntityManager.RemoveComponent(inspectedECSContext.Entity, new ComponentType(o.GetType())); 69 | }); 70 | } 71 | 72 | if (o is Translation translation) 73 | { 74 | menu.AddItem(new GUIContent("Focus In Scene View"), false, () => 75 | { 76 | SceneView sceneView = SceneView.lastActiveSceneView; 77 | if (sceneView == null) 78 | sceneView = EditorWindow.GetWindow(); 79 | 80 | sceneView.pivot = translation.Value; 81 | sceneView.Show(); 82 | }); 83 | } 84 | if (o is LocalToWorld localToWorld) 85 | { 86 | menu.AddItem(new GUIContent("Focus In Scene View"), false, () => 87 | { 88 | SceneView sceneView = SceneView.lastActiveSceneView; 89 | if (sceneView == null) 90 | sceneView = EditorWindow.GetWindow(); 91 | 92 | sceneView.pivot = localToWorld.Position; 93 | sceneView.Show(); 94 | }); 95 | } 96 | #endif 97 | MonoScript targetMonoScript = GetMonoScriptForType(o.GetType()); 98 | if (targetMonoScript != null) 99 | { 100 | menu.AddItem(new GUIContent("Select Script"), false, _ => Selection.activeObject = targetMonoScript, targetMonoScript); 101 | menu.AddItem(new GUIContent("Edit Script"), false, _ => AssetDatabase.OpenAsset(targetMonoScript), targetMonoScript); 102 | } 103 | 104 | menu.AddItem(new GUIContent("Copy As JSON (Unity JsonUtility)"), false, CopyAsJSON, o); 105 | 106 | return menu; 107 | } 108 | 109 | static MonoScript GetMonoScriptForType(Type type) 110 | { 111 | var monoScripts = Resources.FindObjectsOfTypeAll(); 112 | 113 | return monoScripts.FirstOrDefault(monoScript => monoScript.GetClass() == type); 114 | } 115 | 116 | private static void InstantiateScript(object userData) 117 | { 118 | Type classType = (Type) userData; 119 | if(classType.IsSubclassOf(typeof(ScriptableObject))) 120 | { 121 | ScriptableObject asset = ScriptableObject.CreateInstance(classType); 122 | 123 | string fullPath = AssetDatabase.GenerateUniqueAssetPath("Assets/" + classType + ".asset"); 124 | 125 | AssetDatabase.CreateAsset(asset, fullPath); 126 | AssetDatabase.SaveAssets(); 127 | } 128 | else if (classType.IsSubclassOf(typeof(Component))) 129 | { 130 | new GameObject(classType.Name, classType); 131 | } 132 | } 133 | 134 | private static void SetNameFromFirstScript(object userData) 135 | { 136 | GameObject gameObject = (GameObject) userData; 137 | MonoBehaviour[] behaviours = gameObject.GetComponents(); 138 | // Attempt to name it after the first script 139 | if (behaviours.Length >= 1) 140 | { 141 | gameObject.name = behaviours[0].GetType().Name; 142 | } 143 | else 144 | { 145 | // No scripts found, so name after the first optional component 146 | Component[] components = gameObject.GetComponents(); 147 | if (components.Length >= 2) // Ignore Transform 148 | { 149 | gameObject.name = components[1].GetType().Name; 150 | } 151 | } 152 | } 153 | 154 | private static void CopyAsJSON(object userData) 155 | { 156 | string json = JsonUtility.ToJson(userData, true); 157 | EditorGUIUtility.systemCopyBuffer = json; 158 | } 159 | 160 | private static void ExportTexture(object texture) 161 | { 162 | if (texture is Texture2D texture2D) 163 | { 164 | string path = EditorUtility.SaveFilePanel("Save Texture", "Assets", texture2D.name + ".png", "png"); 165 | if (!string.IsNullOrEmpty(path)) 166 | { 167 | byte[] bytes = texture2D.EncodeToPNG(); 168 | System.IO.File.WriteAllBytes(path, bytes); 169 | AssetDatabase.Refresh(); 170 | } 171 | } 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /Editor/Dropdowns/TypeSelectDropdown.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEditor; 6 | using UnityEditor.Compilation; 7 | using UnityEditor.IMGUI.Controls; 8 | using UnityEngine; 9 | using Assembly = System.Reflection.Assembly; 10 | using PackageInfo = UnityEditor.PackageManager.PackageInfo; 11 | using UAssembly = UnityEditor.Compilation.Assembly; 12 | 13 | namespace Sabresaurus.Sidekick 14 | { 15 | public class TypeSelectDropdown : AdvancedDropdown 16 | { 17 | private readonly Action onTypeSelected; 18 | private readonly Type[] constraints; 19 | private readonly Type[] requiredInterfaces; 20 | 21 | class TypeDropdownItem : AdvancedDropdownItem 22 | { 23 | public Type Type { get; } 24 | 25 | public TypeDropdownItem(Type type) : base(type.Name) 26 | { 27 | Type = type; 28 | icon = (Texture2D) EditorGUIUtility.TrIconContent("dll Script Icon").image; 29 | } 30 | } 31 | 32 | public TypeSelectDropdown(AdvancedDropdownState state, Action onTypeSelectedCallback, Type[] constraints = null, Type[] requiredInterfaces = null) : base(state) 33 | { 34 | this.constraints = constraints ?? new Type[0]; 35 | this.requiredInterfaces = requiredInterfaces ?? new Type[0]; 36 | 37 | Vector2 customMinimumSize = minimumSize; 38 | customMinimumSize.y = 250; 39 | minimumSize = customMinimumSize; 40 | 41 | onTypeSelected = onTypeSelectedCallback; 42 | } 43 | 44 | private enum Location 45 | { 46 | Assets, 47 | Packages, 48 | Precompiled, 49 | Dynamic 50 | } 51 | 52 | private struct Group 53 | { 54 | public readonly Location Location; 55 | public readonly string Context; 56 | 57 | public Group(Location location, string context = null) 58 | { 59 | Location = location; 60 | Context = context; 61 | } 62 | 63 | public class Comparer : IEqualityComparer 64 | { 65 | public bool Equals(Group x, Group y) 66 | { 67 | return x.Location == y.Location && x.Context == y.Context; 68 | } 69 | 70 | public int GetHashCode(Group obj) 71 | { 72 | unchecked 73 | { 74 | return ((int) obj.Location * 397) ^ (obj.Context != null ? obj.Context.GetHashCode() : 0); 75 | } 76 | } 77 | } 78 | } 79 | 80 | private static Group GetGroup(Assembly assembly, Dictionary nameToAssembly) 81 | { 82 | if (assembly.IsDynamic) 83 | return new Group(Location.Dynamic); 84 | 85 | string assemblyName = assembly.GetName().Name; 86 | 87 | // Compiled 88 | if (nameToAssembly.TryGetValue(assemblyName, out UAssembly uAssembly)) 89 | { 90 | // Package Group 91 | string asmDefPath = CompilationPipeline.GetAssemblyDefinitionFilePathFromAssemblyName(uAssembly.name); 92 | 93 | if (!string.IsNullOrEmpty(asmDefPath)) 94 | { 95 | PackageInfo packageInfo = PackageInfo.FindForAssetPath(asmDefPath); 96 | 97 | if (packageInfo != null) 98 | return new Group(Location.Packages, GetPackageAuthor(packageInfo)); 99 | } 100 | 101 | return new Group(Location.Assets); 102 | } 103 | 104 | // Precompiled 105 | string location = assembly.Location; 106 | if (!string.IsNullOrEmpty(location)) 107 | { 108 | location = location.Replace('\\', '/'); 109 | 110 | if (location.Contains("/Library/PackageCache/")) 111 | { 112 | var substring = location.Substring(location.IndexOf("/Library/PackageCache/") + "/Library/PackageCache/".Length); 113 | if (substring.Contains("@")) 114 | { 115 | substring = substring.Substring(0, substring.IndexOf("@")); 116 | } 117 | 118 | location = "Packages/" + substring; 119 | } 120 | 121 | PackageInfo packageInfo = PackageInfo.FindForAssetPath(location); 122 | 123 | if (packageInfo != null) 124 | { 125 | return new Group(Location.Packages, GetPackageAuthor(packageInfo)); 126 | } 127 | 128 | if (location.Contains("/Data/MonoBleedingEdge/")) 129 | return new Group(Location.Precompiled, "System"); 130 | 131 | if (location.Contains("/Library/ScriptAssemblies")) 132 | return new Group(Location.Assets); 133 | 134 | return new Group(Location.Precompiled, "Unity"); 135 | } 136 | 137 | return new Group(Location.Dynamic); 138 | 139 | string GetPackageAuthor(PackageInfo packageInfo) 140 | { 141 | if (!string.IsNullOrEmpty(packageInfo.author.name)) 142 | { 143 | return packageInfo.author.name; 144 | } 145 | 146 | if (packageInfo.name.StartsWith("com.unity.")) 147 | { 148 | return "Unity"; 149 | } 150 | 151 | return "Other"; 152 | } 153 | } 154 | 155 | protected override AdvancedDropdownItem BuildRoot() 156 | { 157 | AdvancedDropdownItem root = new AdvancedDropdownItem("Assemblies"); 158 | Dictionary groupDropdowns = new Dictionary(new Group.Comparer()); 159 | 160 | var allAssemblies = AppDomain.CurrentDomain.GetAssemblies().OrderBy(assembly => assembly.FullName); 161 | // Populate Unity-compiled assemblies. 162 | // This includes assemblies in the Assets and Packages directories that are not plugins. 163 | UAssembly[] assemblies = CompilationPipeline.GetAssemblies(); 164 | Dictionary nameToAssembly = new Dictionary(); 165 | foreach (UAssembly assembly in assemblies) 166 | nameToAssembly.Add(assembly.name, assembly); 167 | 168 | // Append root locations 169 | foreach (Location location in Enum.GetValues(typeof(Location))) 170 | { 171 | var locationRoot = new AdvancedDropdownItem(location.ToString()); 172 | root.AddChild(locationRoot); 173 | groupDropdowns.Add(new Group(location), locationRoot); 174 | } 175 | 176 | foreach (Assembly assembly in allAssemblies) 177 | { 178 | Group group = GetGroup(assembly, nameToAssembly); 179 | 180 | Type[] types = assembly.GetTypes(); 181 | List filteredTypes = new List(); 182 | foreach (Type type in types) 183 | { 184 | bool requirementsMet = true; 185 | foreach (Type constraint in constraints) 186 | { 187 | if (!type.IsSubclassOf(constraint) && type != constraint) 188 | { 189 | requirementsMet = false; 190 | } 191 | } 192 | 193 | foreach (Type requiredInterface in requiredInterfaces) 194 | { 195 | if (!type.GetInterfaces().Contains(requiredInterface)) 196 | { 197 | requirementsMet = false; 198 | } 199 | } 200 | 201 | if (requirementsMet) 202 | { 203 | filteredTypes.Add(type); 204 | } 205 | } 206 | 207 | if (filteredTypes.Count != 0) 208 | { 209 | if (!groupDropdowns.TryGetValue(group, out AdvancedDropdownItem groupRoot)) 210 | { 211 | groupDropdowns.Add(group, groupRoot = new AdvancedDropdownItem(group.Context)); 212 | groupDropdowns[new Group(group.Location)].AddChild(groupRoot); 213 | } 214 | 215 | string assemblyName = assembly.GetName().Name; 216 | var assemblyDropdownItem = new AdvancedDropdownItem(assemblyName); 217 | groupRoot.AddChild(assemblyDropdownItem); 218 | 219 | foreach (Type type in filteredTypes) 220 | { 221 | assemblyDropdownItem.AddChild(new TypeDropdownItem(type)); 222 | } 223 | } 224 | } 225 | 226 | root.AddSeparator(); 227 | 228 | AdvancedDropdownItem initializeOnLoad = new AdvancedDropdownItem("Initialize On Load"); 229 | foreach (Type type in TypeCache.GetTypesWithAttribute()) 230 | { 231 | initializeOnLoad.AddChild(new TypeDropdownItem(type)); 232 | } 233 | 234 | foreach (MethodInfo method in TypeCache.GetMethodsWithAttribute()) 235 | { 236 | initializeOnLoad.AddChild(new TypeDropdownItem(method.DeclaringType)); 237 | } 238 | 239 | root.AddChild(initializeOnLoad); 240 | 241 | AdvancedDropdownItem runtimeInitializeOnLoad = new AdvancedDropdownItem("Runtime Initialize On Load"); 242 | foreach (MethodInfo method in TypeCache.GetMethodsWithAttribute()) 243 | { 244 | runtimeInitializeOnLoad.AddChild(new TypeDropdownItem(method.DeclaringType)); 245 | } 246 | 247 | root.AddChild(runtimeInitializeOnLoad); 248 | 249 | return root; 250 | } 251 | 252 | protected override void ItemSelected(AdvancedDropdownItem item) 253 | { 254 | if (item is TypeDropdownItem typeDropdownItem) 255 | { 256 | onTypeSelected(typeDropdownItem.Type); 257 | } 258 | } 259 | } 260 | } -------------------------------------------------------------------------------- /Editor/Helpers/SidekickEditorGUI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | #if ECS_EXISTS 5 | using Unity.Transforms; 6 | using Unity.Entities; 7 | #endif 8 | using UnityEditor; 9 | using UnityEngine; 10 | using Object = UnityEngine.Object; 11 | 12 | namespace Sabresaurus.Sidekick 13 | { 14 | public static class SidekickEditorGUI 15 | { 16 | private static MethodInfo beginLabelHighlight = typeof(EditorGUI).GetMethod("BeginLabelHighlight", BindingFlags.Static | BindingFlags.NonPublic); 17 | private static MethodInfo endLabelHighlight = typeof(EditorGUI).GetMethod("EndLabelHighlight", BindingFlags.Static | BindingFlags.NonPublic); 18 | 19 | private static Color splitterColor => EditorGUIUtility.isProSkin ? new Color(0.12f, 0.12f, 0.12f) : new Color(0.6f, 0.6f, 0.6f); 20 | 21 | public static readonly Texture BackIcon = EditorGUIUtility.TrIconContent("back").image; 22 | public static readonly Texture ForwardIcon = EditorGUIUtility.TrIconContent("forward").image; 23 | public static readonly Texture MoreOptions = EditorGUIUtility.TrIconContent("Toolbar Plus").image; 24 | 25 | public static readonly Texture ErrorIcon = EditorGUIUtility.TrIconContent("console.erroricon").image; 26 | public static readonly Texture WarningIcon = EditorGUIUtility.TrIconContent("console.warnicon").image; 27 | public static readonly Texture InfoIcon = EditorGUIUtility.TrIconContent("console.infoicon").image; 28 | 29 | public static readonly Texture ErrorIconSmall = EditorGUIUtility.TrIconContent("console.erroricon.sml").image; 30 | public static readonly Texture WarningIconSmall = EditorGUIUtility.TrIconContent("console.warnicon.sml").image; 31 | public static readonly Texture InfoIconSmall = EditorGUIUtility.TrIconContent("console.infoicon.sml").image; 32 | 33 | public static readonly Texture BlueDotIcon = EditorGUIUtility.TrIconContent("sv_icon_dot1_pix16_gizmo").image; 34 | public static readonly Texture TransformIcon = EditorGUIUtility.TrIconContent("Transform Icon").image; 35 | 36 | private static Texture2D staticBackgroundLightSkin; 37 | private static Texture2D staticBackgroundDarkSkin; 38 | 39 | private static readonly Color staticBackgroundDarkTintColor = new Color32(227, 227, 227, 255); 40 | private static readonly Color staticBackgroundLightTintColor = new Color32(240, 240, 240, 255); 41 | 42 | public static Color StaticBackgroundTintColor 43 | { 44 | get 45 | { 46 | if (EditorGUIUtility.isProSkin) 47 | { 48 | return staticBackgroundDarkTintColor; 49 | } 50 | else 51 | { 52 | return staticBackgroundLightTintColor; 53 | } 54 | } 55 | } 56 | 57 | public static Texture2D StaticBackground 58 | { 59 | get 60 | { 61 | if (EditorGUIUtility.isProSkin) 62 | { 63 | if (staticBackgroundDarkSkin == null) 64 | { 65 | staticBackgroundDarkSkin = new Texture2D(1, 1); 66 | staticBackgroundDarkSkin.SetPixel(0,0, new Color32(50, 50, 50, 255)); 67 | staticBackgroundDarkSkin.Apply(); 68 | } 69 | 70 | return staticBackgroundDarkSkin; 71 | } 72 | else 73 | { 74 | if (staticBackgroundLightSkin == null) 75 | { 76 | staticBackgroundLightSkin = new Texture2D(1, 1); 77 | staticBackgroundLightSkin.SetPixel(0,0, new Color32(186, 186, 186, 255)); 78 | staticBackgroundLightSkin.Apply(); 79 | } 80 | 81 | return staticBackgroundLightSkin; 82 | } 83 | } 84 | } 85 | 86 | 87 | 88 | public static void DrawTypeChainHeader(GUIContent label) 89 | { 90 | Rect contentRect = GUILayoutUtility.GetRect(1f, 17f); 91 | float xMax = contentRect.xMax; 92 | Rect labelRect = contentRect; 93 | labelRect.xMin += 16f; 94 | labelRect.xMax -= 8f; 95 | 96 | GUIStyle style = new GUIStyle(EditorStyles.boldLabel) 97 | { 98 | alignment = TextAnchor.MiddleRight, 99 | fontSize = 10 100 | }; 101 | Vector2 textSize = style.CalcSize(label); 102 | contentRect.xMax = Mathf.Max(contentRect.xMin, labelRect.xMax - textSize.x - 2); 103 | contentRect.yMin = (contentRect.yMax + contentRect.yMin) / 2f; 104 | contentRect.yMax = contentRect.yMin + 1; 105 | Color tColour = GUI.color; 106 | Color color = splitterColor; 107 | EditorGUI.DrawRect(contentRect, color); 108 | 109 | contentRect.xMax = xMax + 2; 110 | contentRect.xMin = labelRect.xMax + 2; 111 | EditorGUI.DrawRect(contentRect, color); 112 | 113 | GUI.color = tColour; 114 | EditorGUI.LabelField(labelRect, label, style); 115 | } 116 | 117 | public static void DrawSplitter(float alpha = 1) 118 | { 119 | Rect rect = GUILayoutUtility.GetRect(1f, 1f); 120 | rect.xMin = 0.0f; 121 | rect.width += 4f; 122 | if (Event.current.type != EventType.Repaint) 123 | return; 124 | Color drawColor = splitterColor; 125 | drawColor.a = alpha; 126 | EditorGUI.DrawRect(rect, drawColor); 127 | } 128 | 129 | public static Rect DrawVerticalLine(float height) 130 | { 131 | Rect rect = GUILayoutUtility.GetRect(1f, 1f, GUILayout.ExpandWidth(false)); 132 | rect.height = height; 133 | rect.yMin -= 8.0f; 134 | if (Event.current.type != EventType.Repaint) 135 | return rect; 136 | EditorGUI.DrawRect(rect, splitterColor); 137 | return rect; 138 | } 139 | 140 | public static void DrawHeaderBackground(Rect rect) 141 | { 142 | float colorChannel = !EditorGUIUtility.isProSkin ? 1f : 0.1f; 143 | Color color = new Color(colorChannel, colorChannel, colorChannel, 0.2f); 144 | //color = Color.blue; 145 | EditorGUI.DrawRect(rect, color); 146 | } 147 | 148 | public static bool Toggle(bool value, string title, params GUILayoutOption[] options) 149 | { 150 | value = GUILayout.Toggle(value, title, EditorStyles.toolbarButton, options); 151 | return value; 152 | } 153 | 154 | public static T EnumFlagsToggle(T value, T flag, string title, params GUILayoutOption[] options) where T : struct, IConvertible 155 | { 156 | bool present = ((Enum)(object)value).HasFlagByte((Enum)Enum.ToObject(value.GetType(), flag)); 157 | 158 | bool newPresent = GUILayout.Toggle(present, title, EditorStyles.toolbarButton, options); 159 | if (newPresent != present) 160 | { 161 | value = (T)(IConvertible)(byte)((byte)(IConvertible)value ^ (byte)(IConvertible)flag); 162 | } 163 | return value; 164 | } 165 | 166 | public static void BeginLabelHighlight(string searchContext) 167 | { 168 | BeginLabelHighlight(searchContext, (Color)new Color32(49, 105,172,255), Color.white); 169 | } 170 | 171 | public static void BeginLabelHighlight(string searchContext, Color searchHighlightSelectionColor, Color searchHighlightColor) 172 | { 173 | beginLabelHighlight.Invoke(null, new object[] 174 | { 175 | searchContext, searchHighlightSelectionColor, searchHighlightColor 176 | }); 177 | } 178 | 179 | public static void EndLabelHighlight() 180 | { 181 | endLabelHighlight.Invoke(null, null); 182 | } 183 | 184 | public static bool DetectClickInRect(Rect rect, int mouseButton = 0) 185 | { 186 | if (Event.current.type == EventType.MouseDown && Event.current.button == mouseButton && rect.Contains(Event.current.mousePosition)) 187 | { 188 | Event.current.Use(); 189 | return true; 190 | } 191 | 192 | return false; 193 | } 194 | 195 | public static void ButtonWithOptions( 196 | GUIContent content, 197 | out bool mainPressed, 198 | out bool optionsPressed) 199 | { 200 | mainPressed = false; 201 | optionsPressed = false; 202 | 203 | GUIStyle style = EditorStyles.toolbarDropDown; 204 | style.alignment = TextAnchor.MiddleCenter; 205 | 206 | Rect rect = GUILayoutUtility.GetRect(content, style); 207 | 208 | // Right click 209 | if (DetectClickInRect(rect, 1)) 210 | { 211 | optionsPressed = true; 212 | } 213 | 214 | // Left click on the right side drop down icon 215 | if (EditorGUI.DropdownButton(new Rect(rect.xMax - style.padding.right, rect.y, style.padding.right, rect.height), GUIContent.none, FocusType.Passive, GUIStyle.none)) 216 | { 217 | optionsPressed = true; 218 | } 219 | 220 | // Left click on main button 221 | if (GUI.Button(rect, content, style)) 222 | { 223 | mainPressed = true; 224 | } 225 | 226 | Rect rightRect = rect; 227 | rightRect.xMin = rect.xMax - 18; 228 | rightRect.width = 1; 229 | rightRect.height = 12; 230 | rightRect.y = (rect.height - 12) / 2; 231 | GUI.DrawTexture(rightRect, Texture2D.whiteTexture, ScaleMode.StretchToFill, true, 0, new Color32(0,0,0,38), Vector4.zero, Vector4.zero); 232 | } 233 | 234 | public static Texture GetIcon(object o, Type type) 235 | { 236 | GUIContent objectContent = EditorGUIUtility.ObjectContent(o as Object, type); 237 | if (objectContent.image != null) 238 | { 239 | return objectContent.image; 240 | } 241 | #if ECS_EXISTS 242 | if (type == typeof(Translation) 243 | || type == typeof(Rotation) 244 | || type == typeof(Scale) 245 | || type == typeof(LocalToWorld)) 246 | { 247 | return TransformIcon; 248 | } 249 | else if (type.GetInterfaces().Contains(typeof(IComponentData))) 250 | { 251 | return BlueDotIcon; 252 | } 253 | #endif 254 | return null; 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /Editor/Helpers/TypeUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEngine.Assertions; 7 | using Object = UnityEngine.Object; 8 | 9 | namespace Sabresaurus.Sidekick 10 | { 11 | public static class TypeUtility 12 | { 13 | public static object GetDefaultValue(Type type) 14 | { 15 | Assert.IsNotNull(type); 16 | 17 | return type.IsValueType ? Activator.CreateInstance(type) : null; 18 | } 19 | 20 | public static string GetVisibilityName(FieldInfo field) 21 | { 22 | string visibility; 23 | 24 | if (field.IsPublic) 25 | { 26 | visibility = "public"; 27 | } 28 | else if (field.IsAssembly) 29 | { 30 | visibility = "internal"; 31 | } 32 | else if (field.IsFamily) 33 | { 34 | visibility = "protected"; 35 | } 36 | else if (field.IsFamilyOrAssembly) 37 | { 38 | visibility = "protected internal"; 39 | } 40 | else if (field.IsFamilyAndAssembly) 41 | { 42 | visibility = "private protected"; 43 | } 44 | else 45 | { 46 | visibility = "private"; 47 | } 48 | 49 | return visibility; 50 | } 51 | 52 | public static string NameForType(Type type) 53 | { 54 | // See https://msdn.microsoft.com/en-us/library/ya5y69ds.aspx 55 | if (type == null) 56 | { 57 | return "null"; 58 | } 59 | else if(type == typeof(void)) 60 | { 61 | return "void"; 62 | } 63 | else if(type == typeof(Boolean)) 64 | { 65 | return "bool"; 66 | } 67 | else if(type == typeof(Byte)) 68 | { 69 | return "byte"; 70 | } 71 | else if(type == typeof(SByte)) 72 | { 73 | return "sbyte"; 74 | } 75 | else if(type == typeof(Char)) 76 | { 77 | return "char"; 78 | } 79 | else if(type == typeof(Decimal)) 80 | { 81 | return "decimal"; 82 | } 83 | else if(type == typeof(Double)) 84 | { 85 | return "double"; 86 | } 87 | else if(type == typeof(Single)) 88 | { 89 | return "float"; 90 | } 91 | else if(type == typeof(Int32)) 92 | { 93 | return "int"; 94 | } 95 | else if(type == typeof(UInt32)) 96 | { 97 | return "uint"; 98 | } 99 | else if(type == typeof(Int64)) 100 | { 101 | return "long"; 102 | } 103 | else if(type == typeof(UInt64)) 104 | { 105 | return "ulong"; 106 | } 107 | else if(type == typeof(System.Object)) 108 | { 109 | return "object"; 110 | } 111 | else if(type == typeof(Int16)) 112 | { 113 | return "short"; 114 | } 115 | else if(type == typeof(UInt16)) 116 | { 117 | return "ushort"; 118 | } 119 | else if(type == typeof(String)) 120 | { 121 | return "string"; 122 | } 123 | else 124 | { 125 | bool isArray = type.IsArray; 126 | bool isGenericList = IsGenericList(type); 127 | bool isGenericDictionary = IsGenericDictionary(type); 128 | if (isGenericDictionary) 129 | { 130 | Type[] elementTypes = GetElementTypes(type); 131 | string[] elementTypeNames = elementTypes.Select(type => NameForType(type)).ToArray(); 132 | return $"Dictionary<{elementTypeNames[0]},{elementTypeNames[1]}>"; 133 | } 134 | 135 | if (isArray || isGenericList) 136 | { 137 | Type elementType = GetFirstElementType(type); 138 | string elementTypeName = NameForType(elementType); 139 | if (isGenericList) 140 | { 141 | return "List<" + elementTypeName + ">"; 142 | } 143 | else 144 | { 145 | return elementTypeName + "[]"; 146 | } 147 | } 148 | 149 | return type.Name; 150 | } 151 | } 152 | public static bool IsGenericList(Type type) 153 | { 154 | // Check if it's a List 155 | if(type.IsGenericType) 156 | { 157 | Type genericDefinition = type.GetGenericTypeDefinition(); 158 | if(genericDefinition == typeof(List<>)) 159 | { 160 | return true; 161 | } 162 | } 163 | 164 | return false; 165 | } 166 | 167 | public static bool IsGenericDictionary(Type type) 168 | { 169 | // Check if it's a List 170 | if(type.IsGenericType) 171 | { 172 | Type genericDefinition = type.GetGenericTypeDefinition(); 173 | if(genericDefinition == typeof(Dictionary<,>)) 174 | { 175 | return true; 176 | } 177 | } 178 | 179 | return false; 180 | } 181 | 182 | public static Type GetFirstElementType(Type type) 183 | { 184 | if(type.IsArray) 185 | { 186 | return type.GetElementType(); 187 | } 188 | 189 | Type[] genericArguments = type.GetGenericArguments(); 190 | 191 | if(genericArguments.Length == 1) 192 | { 193 | return genericArguments[0]; 194 | } 195 | else 196 | { 197 | // Not array or list 198 | return type; 199 | } 200 | } 201 | 202 | public static Type[] GetElementTypes(Type type) 203 | { 204 | if(type.IsArray) 205 | { 206 | return new[] {type.GetElementType()}; 207 | } 208 | 209 | Type[] genericArguments = type.GetGenericArguments(); 210 | 211 | if(genericArguments != null) 212 | { 213 | return genericArguments; 214 | } 215 | 216 | return null; 217 | } 218 | 219 | public static bool IsBackingField(FieldInfo fieldInfo, Type parentType) 220 | { 221 | BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; 222 | 223 | string fieldName = fieldInfo.Name; 224 | // Backing fields typically are of the format k__BackingField 225 | if(fieldName.StartsWith("<") && fieldName.EndsWith(">k__BackingField")) 226 | { 227 | string strippedName = fieldName.Remove(fieldName.Length - ">k__BackingField".Length).Remove(0,1); 228 | 229 | // Make sure there's actually a property with the property name 230 | if(parentType.GetProperty(strippedName, bindingFlags) != null) 231 | { 232 | return true; 233 | } 234 | else 235 | { 236 | return false; 237 | } 238 | } 239 | else 240 | { 241 | return false; 242 | } 243 | } 244 | 245 | public static bool IsPropertyMethod(MethodInfo methodInfo, Type parentType) 246 | { 247 | BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; 248 | 249 | string methodName = methodInfo.Name; 250 | // The compiler generates methods for getters and setters with a prefix 251 | if(methodName.StartsWith("get_") || methodName.StartsWith("set_")) 252 | { 253 | // Check that no property exists with the name after the prefix 254 | // Don't use SpecialName here as compilers aren't required to populate it 255 | 256 | ParameterInfo[] parameterInfos = methodInfo.GetParameters(); 257 | 258 | Type propertyType; 259 | if (methodName.StartsWith("get_")) 260 | { 261 | if (parameterInfos.Length != 0) 262 | { 263 | return false; 264 | } 265 | propertyType = methodInfo.ReturnType; 266 | } 267 | else 268 | { 269 | if (parameterInfos.Length != 1) 270 | { 271 | return false; 272 | } 273 | propertyType = parameterInfos[0].ParameterType; 274 | } 275 | PropertyInfo propertyInfo = parentType.GetProperty(methodName.Substring(4), bindingFlags, null, propertyType, Type.EmptyTypes, null); 276 | if (propertyInfo == null) 277 | { 278 | return false; 279 | } 280 | 281 | if (propertyInfo.GetIndexParameters().Length != 0) 282 | { 283 | // Indexer, takes parameters unlike ordinary properties so don't treat it like a normal property 284 | return false; 285 | } 286 | 287 | return true; 288 | } 289 | 290 | return false; 291 | } 292 | 293 | /// 294 | /// If supplied with a List and newElementType of Bar it will convert it to List. Also supports arrays and will work if it's not a collection 295 | /// 296 | public static object ChangeElementType(object objectOrCollection, Type newElementType) 297 | { 298 | if (objectOrCollection is IList) 299 | { 300 | // TODO: Investigate if this array copying could be simplified 301 | IList sourceList = (IList)objectOrCollection; 302 | int count = sourceList.Count; 303 | if (objectOrCollection.GetType().IsArray) 304 | { 305 | Type arrayType = newElementType.MakeArrayType(); 306 | // Copying to an array 307 | object newArray = Activator.CreateInstance(arrayType, count); 308 | for (int i = 0; i < count; i++) 309 | { 310 | ((Array)newArray).SetValue(Convert.ChangeType(sourceList[i], newElementType), i); 311 | } 312 | return newArray; 313 | } 314 | else 315 | { 316 | Type listType = typeof(List<>).MakeGenericType(newElementType); 317 | 318 | object newList = Activator.CreateInstance(listType, 0); 319 | for (int i = 0; i < count; i++) 320 | { 321 | ((IList)newList).Add(Convert.ChangeType(sourceList[i], newElementType)); 322 | } 323 | return newList; 324 | } 325 | } 326 | else 327 | { 328 | return Convert.ChangeType(objectOrCollection, newElementType); 329 | } 330 | } 331 | 332 | public static string GetTooltip(FieldInfo field, VariablePane.VariableAttributes variableAttributes) 333 | { 334 | string tooltip = ""; 335 | object[] customAttributes = field.GetCustomAttributes(false); 336 | foreach (var customAttribute in customAttributes) 337 | { 338 | tooltip += $"[{customAttribute.GetType().Name.RemoveEnd("Attribute")}]\n"; 339 | } 340 | 341 | if ((variableAttributes & VariablePane.VariableAttributes.Constant) != 0) 342 | { 343 | tooltip += $"{GetVisibilityName(field)} const {NameForType(field.FieldType)} {field.Name}"; 344 | } 345 | else if ((variableAttributes & VariablePane.VariableAttributes.Static) != 0) 346 | { 347 | tooltip += $"{GetVisibilityName(field)} static {NameForType(field.FieldType)} {field.Name}"; 348 | } 349 | else 350 | { 351 | tooltip += $"{GetVisibilityName(field)} {NameForType(field.FieldType)} {field.Name}"; 352 | } 353 | 354 | return tooltip; 355 | } 356 | 357 | public static string GetTooltip(PropertyInfo propertyInfo, VariablePane.VariableAttributes variableAttributes) 358 | { 359 | string tooltip = ""; 360 | object[] customAttributes = propertyInfo.GetCustomAttributes(false); 361 | foreach (var customAttribute in customAttributes) 362 | { 363 | tooltip += $"[{customAttribute.GetType().Name.RemoveEnd("Attribute")}]\n"; 364 | } 365 | 366 | string methodsSuffix; 367 | if ((variableAttributes & VariablePane.VariableAttributes.ReadOnly) != 0) 368 | { 369 | methodsSuffix = "{ get; }"; 370 | } 371 | else if ((variableAttributes & VariablePane.VariableAttributes.WriteOnly) != 0) 372 | { 373 | methodsSuffix = "{ set; }"; 374 | } 375 | else 376 | { 377 | methodsSuffix = "{ get; set; }"; 378 | } 379 | 380 | if ((variableAttributes & VariablePane.VariableAttributes.Static) != 0) 381 | { 382 | tooltip += $"static {NameForType(propertyInfo.PropertyType)} {propertyInfo.Name} {methodsSuffix}"; 383 | } 384 | else 385 | { 386 | tooltip += $"{NameForType(propertyInfo.PropertyType)} {propertyInfo.Name} {methodsSuffix}"; 387 | } 388 | 389 | return tooltip; 390 | } 391 | 392 | public static string GetMethodIdentifier(MethodInfo methodInfo) 393 | { 394 | return methodInfo.DeclaringType.FullName + "." 395 | + methodInfo.Name 396 | + string.Join(",",methodInfo.GetParameters().Select(item => item.Name)) 397 | + string.Join(",",methodInfo.GetGenericArguments().Select(item => item.Name)); 398 | } 399 | 400 | public static bool IsNull(object o) 401 | { 402 | if (o == null) 403 | return true; 404 | 405 | // UnityEngine.Object overrides the == operator so we need to cast those objects to see if the backing 406 | // native object has been destroyed 407 | if (o is Object unityObject && unityObject == null) 408 | return true; 409 | 410 | return false; 411 | } 412 | 413 | public static bool IsNotNull(object o) 414 | { 415 | return !IsNull(o); 416 | } 417 | } 418 | } -------------------------------------------------------------------------------- /Editor/Panes/MethodPane.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEditor; 7 | using UnityEditor.IMGUI.Controls; 8 | using UnityEngine; 9 | using Object = UnityEngine.Object; 10 | 11 | namespace Sabresaurus.Sidekick 12 | { 13 | public class MethodPane : BasePane 14 | { 15 | Vector2 outputScrollPosition = Vector2.zero; 16 | private List outputObjects = new List(); 17 | 18 | float opacity = 0f; 19 | 20 | public void DrawMethods(Type componentType, object component, string searchTerm, MethodInfo[] methods) 21 | { 22 | GUIStyle labelStyle = new GUIStyle(GUI.skin.label) {alignment = TextAnchor.MiddleRight}; 23 | GUIStyle normalButtonStyle = new GUIStyle(GUI.skin.button) {alignment = TextAnchor.MiddleLeft}; 24 | normalButtonStyle.padding = normalButtonStyle.padding.SetLeft(100); 25 | 26 | List expandedMethods = SidekickWindow.Current.PersistentData.ExpandedMethods; 27 | 28 | GUIStyle expandButtonStyle = new GUIStyle(GUI.skin.button); 29 | RectOffset padding = expandButtonStyle.padding; 30 | padding.left = 0; 31 | padding.right = 1; 32 | expandButtonStyle.padding = padding; 33 | 34 | foreach (MethodInfo method in methods) 35 | { 36 | if(!SearchMatches(searchTerm, method.Name)) 37 | { 38 | // Does not match search term, skip it 39 | continue; 40 | } 41 | 42 | // object[] customAttributes = method.GetCustomAttributes(false); 43 | EditorGUILayout.BeginHorizontal(); 44 | ParameterInfo[] parameters = method.GetParameters(); 45 | 46 | if (method.ReturnType == typeof(void)) 47 | labelStyle.normal.textColor = Color.grey; 48 | else if (method.ReturnType.IsValueType) 49 | labelStyle.normal.textColor = new Color(0, 0, 1); 50 | else 51 | labelStyle.normal.textColor = new Color32(255, 130, 0, 255); 52 | 53 | labelStyle.fontSize = 10; 54 | 55 | var genericArguments = method.GetGenericArguments(); 56 | 57 | GUIContent buttonLabel = new GUIContent("", "Click to fire with defaults"); 58 | if (genericArguments.Length != 0) 59 | { 60 | string genericArgumentsDisplay = string.Join(", ", genericArguments.Select(item => item.Name)); 61 | buttonLabel.text = $"{method.Name} <{genericArgumentsDisplay}> {parameters.Length}"; 62 | } 63 | else 64 | { 65 | buttonLabel.text = $"{method.Name} {parameters.Length}"; 66 | } 67 | 68 | using (new EditorGUI.DisabledScope(method.IsGenericMethod)) 69 | { 70 | bool buttonClicked = GUILayout.Button(buttonLabel, normalButtonStyle); 71 | Rect lastRect = GUILayoutUtility.GetLastRect(); 72 | lastRect.xMax = normalButtonStyle.padding.left; 73 | GUI.Label(lastRect, TypeUtility.NameForType(method.ReturnType), labelStyle); 74 | 75 | if (buttonClicked) 76 | { 77 | object[] arguments = null; 78 | if (parameters.Length > 0) 79 | { 80 | arguments = new object[parameters.Length]; 81 | for (int i = 0; i < parameters.Length; i++) 82 | { 83 | arguments[i] = TypeUtility.GetDefaultValue(parameters[i].ParameterType); 84 | } 85 | } 86 | var output = FireMethod(method, component, arguments, null); 87 | outputObjects.AddRange(output); 88 | opacity = 1f; 89 | } 90 | } 91 | 92 | if (parameters.Length > 0 || genericArguments.Length > 0) 93 | { 94 | string methodIdentifier = TypeUtility.GetMethodIdentifier(method); 95 | 96 | bool wasExpanded = expandedMethods.Any(item => item.MethodIdentifier == methodIdentifier); 97 | string label = wasExpanded ? "▲" : "▼"; 98 | bool expanded = GUILayout.Toggle(wasExpanded, label, expandButtonStyle, GUILayout.Width(20)); 99 | if (expanded != wasExpanded) 100 | { 101 | if (expanded) 102 | { 103 | MethodSetup methodSetup = new MethodSetup() 104 | { 105 | MethodIdentifier = methodIdentifier, 106 | Values = new object[parameters.Length], 107 | GenericArguments = new Type[genericArguments.Length], 108 | }; 109 | expandedMethods.Add(methodSetup); 110 | } 111 | else 112 | { 113 | expandedMethods.RemoveAll(item => item.MethodIdentifier == methodIdentifier); 114 | } 115 | } 116 | 117 | EditorGUILayout.EndHorizontal(); 118 | if (expanded) 119 | { 120 | MethodSetup methodSetup = expandedMethods.FirstOrDefault(item => item.MethodIdentifier == methodIdentifier); 121 | 122 | if (methodSetup.Values.Length != parameters.Length) 123 | { 124 | methodSetup.Values = new object[parameters.Length]; 125 | } 126 | 127 | EditorGUI.indentLevel++; 128 | 129 | for (var i = 0; i < genericArguments.Length; i++) 130 | { 131 | Type genericArgument = genericArguments[i]; 132 | string displayLabel = genericArgument.Name; 133 | 134 | Type[] constraints = genericArgument.GetGenericParameterConstraints(); 135 | if (constraints.Length != 0) 136 | { 137 | displayLabel += $" ({string.Join(", ", constraints.Select(item => item.Name))})"; 138 | } 139 | 140 | EditorGUILayout.BeginHorizontal(); 141 | EditorGUILayout.LabelField(displayLabel, TypeUtility.NameForType(methodSetup.GenericArguments[i])); 142 | var popupRect = GUILayoutUtility.GetLastRect(); 143 | popupRect.width = EditorGUIUtility.currentViewWidth; 144 | 145 | var selectTypeButtonLabel = new GUIContent("Select"); 146 | if (GUILayout.Button(selectTypeButtonLabel, EditorStyles.miniButton)) 147 | { 148 | int index = i; 149 | TypeSelectDropdown dropdown = new TypeSelectDropdown(new AdvancedDropdownState(), type => methodSetup.GenericArguments[index] = type, constraints); 150 | dropdown.Show(popupRect); 151 | } 152 | 153 | EditorGUILayout.EndHorizontal(); 154 | } 155 | 156 | for (int i = 0; i < parameters.Length; i++) 157 | { 158 | int index = i; 159 | VariablePane.DrawVariable(parameters[i].ParameterType, parameters[i].Name, methodSetup.Values[i], "", VariablePane.VariableAttributes.None, null, false, null, newValue => 160 | { 161 | methodSetup.Values[index] = newValue; 162 | }); 163 | } 164 | 165 | EditorGUI.indentLevel--; 166 | 167 | EditorGUILayout.BeginHorizontal(); 168 | GUILayout.Space(30); 169 | 170 | bool anyGenericArgumentsMissing = methodSetup.GenericArguments.Any(item => item == null); 171 | 172 | using (new EditorGUI.DisabledScope(anyGenericArgumentsMissing)) 173 | { 174 | if (GUILayout.Button("Fire")) 175 | { 176 | var output = FireMethod(method, component, methodSetup.Values, methodSetup.GenericArguments); 177 | outputObjects.AddRange(output); 178 | opacity = 1f; 179 | } 180 | } 181 | 182 | EditorGUILayout.EndHorizontal(); 183 | 184 | GUILayout.Space(20); 185 | } 186 | } 187 | else 188 | { 189 | EditorGUILayout.EndHorizontal(); 190 | } 191 | } 192 | } 193 | 194 | List FireMethod(MethodInfo method, object component, object[] parameters, Type[] genericTypes) 195 | { 196 | if (method.ReturnType == typeof(IEnumerator) && component is MonoBehaviour monoBehaviour) 197 | { 198 | return new List {monoBehaviour.StartCoroutine(method.Name)}; 199 | } 200 | 201 | if(method.IsGenericMethod) 202 | { 203 | method = method.MakeGenericMethod(genericTypes); 204 | } 205 | object result = method.Invoke(component, parameters); 206 | List outputObjects = new List {result}; 207 | 208 | for (int i = 0; i < method.GetParameters().Length; i++) 209 | { 210 | if (method.GetParameters()[i].IsOut) 211 | { 212 | outputObjects.Add(parameters[i]); 213 | } 214 | } 215 | 216 | return outputObjects; 217 | } 218 | 219 | public void PostDraw() 220 | { 221 | SidekickEditorGUI.DrawSplitter(); 222 | GUILayout.Label("Output", EditorStyles.boldLabel); 223 | outputScrollPosition = EditorGUILayout.BeginScrollView(outputScrollPosition, GUILayout.MaxHeight(100)); 224 | foreach (var outputObject in outputObjects) 225 | { 226 | if(TypeUtility.IsNotNull(outputObject)) 227 | { 228 | string name = outputObject switch 229 | { 230 | Object unityObject => $"{unityObject.name}", 231 | _ => outputObject.ToString() 232 | }; 233 | 234 | if (GUILayout.Button($"Select {name} ({TypeUtility.NameForType(outputObject.GetType())})")) 235 | { 236 | SidekickWindow.Current.SetSelection(outputObject); 237 | } 238 | } 239 | else 240 | { 241 | using (new EditorGUI.DisabledScope(true)) 242 | { 243 | GUILayout.Button("null"); 244 | } 245 | } 246 | } 247 | 248 | if (opacity > 0) 249 | { 250 | Rect lastRect = GUILayoutUtility.GetLastRect(); 251 | Color baseColor = new Color(0, 0, 1, 0.3f * opacity); 252 | GUI.color = baseColor; 253 | GUI.DrawTexture(lastRect, EditorGUIUtility.whiteTexture); 254 | 255 | baseColor.a = 0.8f * opacity; 256 | GUI.color = baseColor; 257 | float lineThickness = 2; 258 | 259 | GUI.DrawTexture(new Rect(lastRect.xMin, lastRect.yMin, lineThickness, lastRect.height), EditorGUIUtility.whiteTexture); 260 | GUI.DrawTexture(new Rect(lastRect.xMax - lineThickness, lastRect.yMin, lineThickness, lastRect.height), EditorGUIUtility.whiteTexture); 261 | 262 | GUI.DrawTexture(new Rect(lastRect.xMin + lineThickness, lastRect.yMin, lastRect.width - lineThickness * 2, lineThickness), EditorGUIUtility.whiteTexture); 263 | GUI.DrawTexture(new Rect(lastRect.xMin + lineThickness, lastRect.yMax - lineThickness, lastRect.width - lineThickness * 2, lineThickness), EditorGUIUtility.whiteTexture); 264 | GUI.color = Color.white; 265 | opacity -= AnimationHelper.DeltaTime; 266 | 267 | AnimationHelper.SetAnimationActive(); 268 | } 269 | 270 | EditorGUILayout.EndScrollView(); 271 | } 272 | } 273 | } -------------------------------------------------------------------------------- /Editor/Panes/VariablePane.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | #if UNITY_MATH_EXISTS 7 | using Unity.Mathematics; 8 | #endif 9 | using UnityEditor; 10 | using UnityEditor.IMGUI.Controls; 11 | using UnityEngine; 12 | 13 | namespace Sabresaurus.Sidekick 14 | { 15 | public abstract class VariablePane : BasePane 16 | { 17 | [Flags] 18 | public enum VariableAttributes 19 | { 20 | None = 0, 21 | Static = 1 << 0, 22 | Constant = 1 << 1, 23 | ReadOnly = 1 << 2, 24 | WriteOnly = 1 << 3, 25 | } 26 | 27 | public static void DrawVariable(Type fieldType, string fieldName, object fieldValue, string tooltip, VariableAttributes variableAttributes, IEnumerable customAttributes, bool allowExtensions, Type contextType, Action changeCallback) 28 | { 29 | if ((variableAttributes & VariableAttributes.Static) != 0) 30 | { 31 | var style = new GUIStyle {normal = {background = SidekickEditorGUI.StaticBackground}}; 32 | EditorGUILayout.BeginVertical(style); 33 | } 34 | 35 | GUIStyle expandButtonStyle = new GUIStyle(GUI.skin.button); 36 | RectOffset padding = expandButtonStyle.padding; 37 | padding.left = 0; 38 | padding.right = 1; 39 | expandButtonStyle.padding = padding; 40 | 41 | fieldValue ??= TypeUtility.GetDefaultValue(fieldType); 42 | 43 | string displayName = SidekickUtility.NicifyIdentifier(fieldName); 44 | 45 | GUIContent label = new GUIContent(displayName, tooltip); 46 | 47 | bool isArray = fieldType.IsArray; 48 | bool isGenericList = TypeUtility.IsGenericList(fieldType); 49 | bool isGenericDictionary = TypeUtility.IsGenericDictionary(fieldType); 50 | 51 | if (isGenericDictionary) 52 | { 53 | EditorGUILayout.BeginHorizontal(); 54 | 55 | string expandedID = fieldType.FullName + fieldName; 56 | bool expanded = DrawHeader(expandedID, label, (variableAttributes & VariableAttributes.Static) != 0); 57 | 58 | int count = 0; 59 | if (fieldValue != null) 60 | { 61 | EditorGUI.BeginDisabledGroup(true); 62 | 63 | count = (int) fieldType.GetProperty("Count").GetValue(fieldValue); 64 | 65 | EditorGUILayout.IntField(count, GUILayout.Width(80)); 66 | EditorGUI.EndDisabledGroup(); 67 | } 68 | 69 | if (allowExtensions) 70 | { 71 | DrawExtensions(fieldValue, expandButtonStyle); 72 | } 73 | 74 | EditorGUILayout.EndHorizontal(); 75 | 76 | if (expanded) 77 | { 78 | EditorGUI.indentLevel++; 79 | 80 | if (fieldValue != null) 81 | { 82 | FieldInfo entriesArrayField = fieldType.GetField("entries", BindingFlags.NonPublic | BindingFlags.Instance); 83 | IList entriesArray = (IList) entriesArrayField.GetValue(fieldValue); 84 | Type elementType = TypeUtility.GetFirstElementType(entriesArrayField.FieldType); 85 | FieldInfo elementKeyFieldInfo = elementType.GetField("key", BindingFlags.Public | BindingFlags.Instance); 86 | FieldInfo elementValueFieldInfo = elementType.GetField("value", BindingFlags.Public | BindingFlags.Instance); 87 | int oldIndent = EditorGUI.indentLevel; 88 | EditorGUI.indentLevel = 0; 89 | for (int i = 0; i < count; i++) 90 | { 91 | object entry = entriesArray[i]; 92 | 93 | EditorGUILayout.BeginHorizontal(); 94 | 95 | object key = elementKeyFieldInfo.GetValue(entry); 96 | object value = elementValueFieldInfo.GetValue(entry); 97 | 98 | using (new EditorGUI.DisabledScope(true)) 99 | { 100 | DrawIndividualVariable(GUIContent.none, key.GetType(), key, null, out _, newValue => 101 | { 102 | /*list[index] = newValue;*/ 103 | }); 104 | } 105 | 106 | DrawIndividualVariable(GUIContent.none, value.GetType(), value, null, out var handled, newValue => 107 | { 108 | PropertyInfo indexer = fieldType.GetProperties().First(x => x.GetIndexParameters().Length > 0); 109 | indexer.SetValue(fieldValue, newValue, new[] {key}); 110 | }); 111 | 112 | EditorGUILayout.EndHorizontal(); 113 | } 114 | 115 | EditorGUI.indentLevel = oldIndent; 116 | } 117 | 118 | EditorGUI.indentLevel--; 119 | } 120 | 121 | EditorGUILayout.EndFoldoutHeaderGroup(); 122 | } 123 | else if (isArray || isGenericList) 124 | { 125 | Type elementType = TypeUtility.GetFirstElementType(fieldType); 126 | 127 | EditorGUILayout.BeginHorizontal(); 128 | 129 | string expandedID = fieldType.FullName + fieldName; 130 | bool expanded = DrawHeader(expandedID, label, (variableAttributes & VariableAttributes.Static) != 0); 131 | 132 | EditorGUI.BeginDisabledGroup((variableAttributes & VariableAttributes.ReadOnly) != 0); 133 | 134 | IList list = null; 135 | int previousSize = 0; 136 | 137 | if (fieldValue != null) 138 | { 139 | list = (IList) fieldValue; 140 | 141 | previousSize = list.Count; 142 | } 143 | 144 | int newSize = Mathf.Max(0, EditorGUILayout.IntField(previousSize, GUILayout.Width(80))); 145 | if (newSize != previousSize) 146 | { 147 | var newValue = CollectionUtility.Resize(list, isArray, fieldType, elementType, newSize); 148 | changeCallback(newValue); 149 | } 150 | 151 | if (allowExtensions) 152 | { 153 | DrawExtensions(fieldValue, expandButtonStyle); 154 | } 155 | 156 | EditorGUILayout.EndHorizontal(); 157 | 158 | if (expanded) 159 | { 160 | EditorGUI.indentLevel++; 161 | 162 | if (list != null) 163 | { 164 | for (int i = 0; i < list.Count; i++) 165 | { 166 | EditorGUILayout.BeginHorizontal(); 167 | 168 | int index = i; 169 | DrawIndividualVariable(new GUIContent("Element " + i), elementType, list[i], null, out var handled, newValue => { list[index] = newValue; }); 170 | 171 | if (allowExtensions) 172 | { 173 | DrawExtensions(list[i], expandButtonStyle); 174 | } 175 | 176 | EditorGUILayout.EndHorizontal(); 177 | } 178 | } 179 | 180 | EditorGUI.indentLevel--; 181 | } 182 | 183 | EditorGUILayout.EndFoldoutHeaderGroup(); 184 | } 185 | else 186 | { 187 | EditorGUI.BeginDisabledGroup((variableAttributes & VariableAttributes.ReadOnly) != 0); 188 | 189 | // Not a collection 190 | EditorGUILayout.BeginHorizontal(); 191 | 192 | DrawIndividualVariable(label, fieldType, fieldValue, customAttributes, out var handled, changeCallback); 193 | 194 | if (handled && allowExtensions) 195 | { 196 | DrawExtensions(fieldValue, expandButtonStyle); 197 | } 198 | 199 | EditorGUILayout.EndHorizontal(); 200 | 201 | if (!handled) 202 | { 203 | EditorGUI.EndDisabledGroup(); 204 | 205 | string expandedID = fieldType.FullName + fieldName; 206 | EditorGUILayout.BeginHorizontal(); 207 | bool expanded = DrawHeader(expandedID, label, (variableAttributes & VariableAttributes.Static) != 0); 208 | 209 | if (allowExtensions) 210 | { 211 | DrawExtensions(fieldValue, expandButtonStyle); 212 | } 213 | 214 | EditorGUILayout.EndHorizontal(); 215 | 216 | EditorGUI.BeginDisabledGroup((variableAttributes & VariableAttributes.ReadOnly) != 0); 217 | 218 | if (expanded) 219 | { 220 | EditorGUI.indentLevel++; 221 | if (fieldValue != null) 222 | { 223 | var fields = fieldType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 224 | 225 | foreach (var fieldInfo in fields) 226 | { 227 | GUIContent subLabel = new GUIContent(fieldInfo.Name); 228 | DrawIndividualVariable(subLabel, fieldInfo.FieldType, fieldInfo.GetValue(fieldValue), null, out _, newValue => { fieldInfo.SetValue(fieldValue, newValue); }); 229 | } 230 | } 231 | else 232 | { 233 | GUILayout.Label("Null"); 234 | } 235 | 236 | EditorGUI.indentLevel--; 237 | } 238 | 239 | EditorGUILayout.EndFoldoutHeaderGroup(); 240 | } 241 | } 242 | 243 | EditorGUI.EndDisabledGroup(); 244 | 245 | if ((variableAttributes & VariableAttributes.Static) != 0) 246 | { 247 | EditorGUILayout.EndVertical(); 248 | } 249 | } 250 | 251 | private static bool DrawHeader(string expandedID, GUIContent label, bool isStatic) 252 | { 253 | bool expanded = SidekickWindow.Current.PersistentData.ExpandedFields.Contains(expandedID); 254 | EditorGUI.BeginChangeCheck(); 255 | 256 | GUIStyle style = new GUIStyle(EditorStyles.foldoutHeader); 257 | 258 | Color oldColor = GUI.backgroundColor; 259 | if (isStatic) 260 | { 261 | GUI.backgroundColor = SidekickEditorGUI.StaticBackgroundTintColor; 262 | } 263 | 264 | expanded = EditorGUILayout.BeginFoldoutHeaderGroup(expanded, label, style); 265 | if (EditorGUI.EndChangeCheck()) 266 | { 267 | if (expanded) 268 | { 269 | SidekickWindow.Current.PersistentData.ExpandedFields.Add(expandedID); 270 | } 271 | else 272 | { 273 | SidekickWindow.Current.PersistentData.ExpandedFields.Remove(expandedID); 274 | } 275 | } 276 | 277 | if (isStatic) 278 | { 279 | style.normal.background = SidekickEditorGUI.StaticBackground; 280 | } 281 | 282 | GUI.backgroundColor = oldColor; 283 | return expanded; 284 | } 285 | 286 | private static void DrawExtensions(object fieldValue, GUIStyle expandButtonStyle) 287 | { 288 | bool wasGUIEnabled = GUI.enabled; 289 | GUI.enabled = TypeUtility.IsNotNull(fieldValue); 290 | 291 | if (GUILayout.Button(new GUIContent(SidekickEditorGUI.ForwardIcon, "Select This"), expandButtonStyle, GUILayout.Width(18), GUILayout.Height(18))) 292 | { 293 | SidekickWindow.Current.SetSelection(fieldValue); 294 | } 295 | 296 | Rect rect = GUILayoutUtility.GetRect(18, 18, expandButtonStyle, GUILayout.Width(18)); 297 | 298 | if (GUI.Button(rect, new GUIContent(SidekickEditorGUI.MoreOptions, "More Options"), expandButtonStyle)) 299 | { 300 | var menu = ClassUtilities.GetMenu(fieldValue, null); 301 | 302 | menu.DropDown(rect); 303 | } 304 | 305 | GUI.enabled = wasGUIEnabled; 306 | } 307 | 308 | private static void DrawIndividualVariable(GUIContent label, Type fieldType, object fieldValue, IEnumerable customAttributes, out bool handled, Action changeCallback) 309 | { 310 | EditorGUI.BeginChangeCheck(); 311 | handled = true; 312 | object newValue; 313 | 314 | RangeAttribute rangeAttribute = null; 315 | 316 | if (customAttributes != null) 317 | { 318 | foreach (var customAttribute in customAttributes) 319 | { 320 | if (customAttribute is RangeAttribute attribute) 321 | { 322 | rangeAttribute = attribute; 323 | } 324 | } 325 | } 326 | 327 | if (fieldType == typeof(int)) 328 | { 329 | if (SidekickSettings.PreferUnityAttributes && rangeAttribute != null) 330 | { 331 | newValue = EditorGUILayout.IntSlider(label, (int) fieldValue, (int) rangeAttribute.min, (int) rangeAttribute.max); 332 | } 333 | else 334 | { 335 | newValue = EditorGUILayout.IntField(label, (int) fieldValue); 336 | } 337 | } 338 | else if (fieldType == typeof(uint)) 339 | { 340 | long newLong = EditorGUILayout.LongField(label, (uint) fieldValue); 341 | // Replicate Unity's built in behaviour 342 | newValue = (uint) Mathf.Clamp(newLong, uint.MinValue, uint.MaxValue); 343 | } 344 | else if (fieldType == typeof(long)) 345 | { 346 | newValue = EditorGUILayout.LongField(label, (long) fieldValue); 347 | } 348 | else if (fieldType == typeof(ulong)) 349 | { 350 | // Note that Unity doesn't have a built in way to handle larger values than long.MaxValue (its inspector 351 | // doesn't work correctly with ulong in fact), so display it as a validated text field 352 | string newString = EditorGUILayout.TextField(label, ((ulong) fieldValue).ToString()); 353 | if (ulong.TryParse(newString, out ulong newULong)) 354 | { 355 | newValue = newULong; 356 | } 357 | else 358 | { 359 | newValue = fieldValue; 360 | } 361 | } 362 | else if (fieldType == typeof(byte)) 363 | { 364 | int newInt = EditorGUILayout.IntField(label, (byte) fieldValue); 365 | // Replicate Unity's built in behaviour 366 | newValue = (byte) Mathf.Clamp(newInt, byte.MinValue, byte.MaxValue); 367 | } 368 | else if (fieldType == typeof(sbyte)) 369 | { 370 | int newInt = EditorGUILayout.IntField(label, (sbyte) fieldValue); 371 | // Replicate Unity's built in behaviour 372 | newValue = (sbyte) Mathf.Clamp(newInt, sbyte.MinValue, sbyte.MaxValue); 373 | } 374 | else if (fieldType == typeof(ushort)) 375 | { 376 | int newInt = EditorGUILayout.IntField(label, (ushort) fieldValue); 377 | // Replicate Unity's built in behaviour 378 | newValue = (ushort) Mathf.Clamp(newInt, ushort.MinValue, ushort.MaxValue); 379 | } 380 | else if (fieldType == typeof(short)) 381 | { 382 | int newInt = EditorGUILayout.IntField(label, (short) fieldValue); 383 | // Replicate Unity's built in behaviour 384 | newValue = (short) Mathf.Clamp(newInt, short.MinValue, short.MaxValue); 385 | } 386 | else if (fieldType == typeof(string)) 387 | { 388 | newValue = EditorGUILayout.TextField(label, (string) fieldValue); 389 | } 390 | else if (fieldType == typeof(char)) 391 | { 392 | string newString = EditorGUILayout.TextField(label, new string((char) fieldValue, 1)); 393 | // Replicate Unity's built in behaviour 394 | if (newString.Length == 1) 395 | { 396 | newValue = newString[0]; 397 | } 398 | else 399 | { 400 | newValue = fieldValue; 401 | } 402 | } 403 | else if (fieldType == typeof(float)) 404 | { 405 | if (SidekickSettings.PreferUnityAttributes && rangeAttribute != null) 406 | { 407 | newValue = EditorGUILayout.Slider(label, (float) fieldValue, rangeAttribute.min, rangeAttribute.max); 408 | } 409 | else 410 | { 411 | newValue = EditorGUILayout.FloatField(label, (float) fieldValue); 412 | } 413 | } 414 | else if (fieldType == typeof(double)) 415 | { 416 | newValue = EditorGUILayout.DoubleField(label, (double) fieldValue); 417 | } 418 | else if (fieldType == typeof(bool)) 419 | { 420 | newValue = EditorGUILayout.Toggle(label, (bool) fieldValue); 421 | } 422 | else if (fieldType == typeof(Vector2)) 423 | { 424 | newValue = EditorGUILayout.Vector2Field(label, (Vector2) fieldValue); 425 | } 426 | else if (fieldType == typeof(Vector3)) 427 | { 428 | newValue = EditorGUILayout.Vector3Field(label, (Vector3) fieldValue); 429 | } 430 | else if (fieldType == typeof(Vector4)) 431 | { 432 | newValue = EditorGUILayout.Vector4Field(label, (Vector4) fieldValue); 433 | } 434 | else if (fieldType == typeof(Vector2Int)) 435 | { 436 | newValue = EditorGUILayout.Vector2IntField(label, (Vector2Int) fieldValue); 437 | } 438 | else if (fieldType == typeof(Vector3Int)) 439 | { 440 | newValue = EditorGUILayout.Vector3IntField(label, (Vector3Int) fieldValue); 441 | } 442 | else if (fieldType == typeof(Quaternion)) 443 | { 444 | Quaternion quaternion = (Quaternion) fieldValue; 445 | Vector4 vector = new Vector4(quaternion.x, quaternion.y, quaternion.z, quaternion.w); 446 | vector = EditorGUILayout.Vector4Field(label, vector); 447 | newValue = new Quaternion(vector.x, vector.y, vector.z, vector.z); 448 | } 449 | #if UNITY_MATH_EXISTS 450 | else if (fieldType == typeof(float2)) 451 | { 452 | newValue = (float2) EditorGUILayout.Vector2Field(label, (float2) fieldValue); 453 | } 454 | else if (fieldType == typeof(float3)) 455 | { 456 | newValue = (float3) EditorGUILayout.Vector3Field(label, (float3) fieldValue); 457 | } 458 | else if (fieldType == typeof(float4)) 459 | { 460 | newValue = (float4) EditorGUILayout.Vector4Field(label, (float4) fieldValue); 461 | } 462 | #endif 463 | else if (fieldType == typeof(Bounds)) 464 | { 465 | newValue = EditorGUILayout.BoundsField(label, (Bounds) fieldValue); 466 | } 467 | else if (fieldType == typeof(BoundsInt)) 468 | { 469 | newValue = EditorGUILayout.BoundsIntField(label, (BoundsInt) fieldValue); 470 | } 471 | else if (fieldType == typeof(Color)) 472 | { 473 | newValue = EditorGUILayout.ColorField(label, (Color) fieldValue); 474 | } 475 | else if (fieldType == typeof(Color32)) 476 | { 477 | newValue = (Color32) EditorGUILayout.ColorField(label, (Color32) fieldValue); 478 | } 479 | else if (fieldType == typeof(Gradient)) 480 | { 481 | newValue = EditorGUILayout.GradientField(new GUIContent(label), (Gradient) fieldValue); 482 | } 483 | else if (fieldType == typeof(AnimationCurve)) 484 | { 485 | newValue = EditorGUILayout.CurveField(label, (AnimationCurve) fieldValue); 486 | } 487 | else if (fieldType.IsSubclassOf(typeof(Enum))) 488 | { 489 | newValue = EditorGUILayout.EnumPopup(label, (Enum) fieldValue); 490 | Type underlyingType = Enum.GetUnderlyingType(fieldValue.GetType()); 491 | // Cast from the enum to the underlying type (e.g. byte) then to int 492 | object cast = Convert.ChangeType(newValue, underlyingType); 493 | cast = Convert.ChangeType(cast, typeof(int)); 494 | // Allow them to edit as an int then cast back 495 | newValue = Convert.ChangeType(EditorGUILayout.IntField((int) cast), underlyingType); 496 | } 497 | else if (fieldType == typeof(Rect)) 498 | { 499 | newValue = EditorGUILayout.RectField(label, (Rect) fieldValue); 500 | } 501 | else if (fieldType == typeof(RectInt)) 502 | { 503 | newValue = EditorGUILayout.RectIntField(label, (RectInt) fieldValue); 504 | } 505 | else if (fieldType.IsSubclassOf(typeof(UnityEngine.Object))) 506 | { 507 | newValue = EditorGUILayout.ObjectField(label, (UnityEngine.Object) fieldValue, fieldType, true); 508 | } 509 | else if (fieldType == typeof(Type)) 510 | { 511 | EditorGUILayout.BeginHorizontal(); 512 | EditorGUILayout.LabelField(label, new GUIContent(TypeUtility.NameForType((Type) fieldValue))); 513 | var popupRect = GUILayoutUtility.GetLastRect(); 514 | popupRect.width = EditorGUIUtility.currentViewWidth; 515 | 516 | var selectTypeButtonLabel = new GUIContent("Select"); 517 | if (GUILayout.Button(selectTypeButtonLabel, EditorStyles.miniButton)) 518 | { 519 | TypeSelectDropdown dropdown = new TypeSelectDropdown(new AdvancedDropdownState(), type => 520 | { 521 | // Async apply 522 | changeCallback?.Invoke(type); 523 | }); 524 | dropdown.Show(popupRect); 525 | } 526 | 527 | EditorGUILayout.EndHorizontal(); 528 | newValue = fieldValue; 529 | } 530 | else 531 | { 532 | handled = false; 533 | newValue = fieldValue; 534 | } 535 | 536 | if (EditorGUI.EndChangeCheck()) 537 | { 538 | changeCallback?.Invoke(newValue); 539 | } 540 | } 541 | } 542 | } -------------------------------------------------------------------------------- /Editor/SidekickWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Unity.Collections; 6 | #if ECS_EXISTS 7 | using Unity.Entities; 8 | using Unity.Entities.Editor; 9 | #endif 10 | using UnityEditor; 11 | using UnityEditor.IMGUI.Controls; 12 | using UnityEngine; 13 | using Object = UnityEngine.Object; 14 | 15 | namespace Sabresaurus.Sidekick 16 | { 17 | public class SidekickWindow : EditorWindow 18 | { 19 | private const int BACK_STACK_LIMIT = 50; 20 | 21 | enum InspectorMode 22 | { 23 | Fields, 24 | Properties, 25 | Methods, 26 | Events, 27 | } 28 | 29 | private string searchTerm = ""; 30 | 31 | readonly FieldPane fieldPane = new FieldPane(); 32 | readonly PropertyPane propertyPane = new PropertyPane(); 33 | readonly MethodPane methodPane = new MethodPane(); 34 | readonly EventPane eventPane = new EventPane(); 35 | 36 | static SidekickWindow current; 37 | 38 | private SearchField searchField; 39 | 40 | 41 | 42 | SelectionInfo activeSelection = new SelectionInfo(); 43 | 44 | private bool selectionLocked = false; 45 | 46 | readonly List backStack = new List(); 47 | readonly List forwardStack = new List(); 48 | 49 | PersistentData persistentData = new PersistentData(); 50 | InspectorMode mode = InspectorMode.Fields; 51 | Vector2 scrollPosition; 52 | 53 | private bool suppressNextSelectionDetection = false; 54 | 55 | readonly List> typesHidden = new List>() 56 | { 57 | new KeyValuePair(typeof(Transform), true), 58 | new KeyValuePair(typeof(GameObject), true), 59 | }; 60 | 61 | 62 | public static SidekickWindow Current => current; 63 | 64 | public PersistentData PersistentData => persistentData; 65 | 66 | [MenuItem("Window/Sidekick")] 67 | static void Init() 68 | { 69 | // Get existing open window or if none, make a new one: 70 | SidekickWindow sidekick = GetWindow(); 71 | sidekick.UpdateTitleContent(); 72 | } 73 | 74 | void OnEnable() 75 | { 76 | UpdateTitleContent(); 77 | 78 | searchField = new SearchField(); 79 | 80 | minSize = new Vector2(260, 100); 81 | 82 | OnSelectionChangeNonMessage(); 83 | 84 | Selection.selectionChanged += OnSelectionChangeNonMessage; 85 | EditorApplication.playModeStateChanged += _ => OnSelectionChangeNonMessage(); 86 | } 87 | 88 | private void ShowButton(Rect rect) 89 | { 90 | if (EditorGUI.Toggle(rect, selectionLocked, (GUIStyle)"IN LockButton") != selectionLocked) 91 | { 92 | selectionLocked = !selectionLocked; 93 | if (selectionLocked == false && Selection.activeObject != null && !activeSelection.Equals(new SelectionInfo(Selection.activeObject))) 94 | { 95 | SetSelection(Selection.activeObject); 96 | } 97 | } 98 | } 99 | 100 | void UpdateTitleContent() 101 | { 102 | titleContent = EditorGUIUtility.TrTextContentWithIcon("Sidekick", "Packages/com.sabresaurus.sidekick/Editor/SidekickIcon.png"); 103 | } 104 | 105 | void OnGUI() 106 | { 107 | // Flexible width for the label based on overall width 108 | EditorGUIUtility.labelWidth = Mathf.Round(EditorGUIUtility.currentViewWidth * 0.4f); 109 | // Use inline controls if there is enough horizontal room 110 | EditorGUIUtility.wideMode = EditorGUIUtility.currentViewWidth > 400; 111 | 112 | // Frame rate tracking 113 | if (Event.current.type == EventType.Repaint) 114 | { 115 | AnimationHelper.UpdateTime(); 116 | } 117 | 118 | current = this; 119 | 120 | CleanStacks(); 121 | 122 | DrawToolbar(); 123 | 124 | Type[] inspectedTypes = null; 125 | object[] inspectedContexts = null; 126 | ECSContext[] inspectedECSContexts = null; 127 | 128 | GUILayout.Space(9); 129 | 130 | string buttonPrefix = ""; 131 | 132 | #if ECS_EXISTS 133 | int selectionWrapWidth = 465; 134 | #else 135 | int selectionWrapWidth = 400; 136 | #endif 137 | if (EditorGUIUtility.currentViewWidth > selectionWrapWidth) 138 | { 139 | EditorGUILayout.BeginHorizontal(); 140 | GUILayout.Label("Selection Helpers"); 141 | } 142 | else 143 | { 144 | buttonPrefix = "Select "; 145 | } 146 | 147 | var popupRect = GUILayoutUtility.GetLastRect(); 148 | popupRect.width = EditorGUIUtility.currentViewWidth; 149 | 150 | if (GUILayout.Button(new GUIContent(buttonPrefix + "Type From Assembly"), EditorStyles.miniButton)) 151 | { 152 | TypeSelectDropdown dropdown = new TypeSelectDropdown(new AdvancedDropdownState(), SetSelection); 153 | dropdown.Show(popupRect); 154 | } 155 | 156 | if (GUILayout.Button(new GUIContent(buttonPrefix + "Loaded Unity Object"), EditorStyles.miniButton)) 157 | { 158 | UnityObjectSelectDropdown dropdown = new UnityObjectSelectDropdown(new AdvancedDropdownState(), SetSelection); 159 | dropdown.Show(popupRect); 160 | } 161 | 162 | #if ECS_EXISTS 163 | if (GUILayout.Button(new GUIContent(buttonPrefix + "ECS System"), EditorStyles.miniButton)) 164 | { 165 | ECSSystemSelectDropdown dropdown = new ECSSystemSelectDropdown(new AdvancedDropdownState(), SetSelection); 166 | dropdown.Show(popupRect); 167 | } 168 | #endif 169 | 170 | if (EditorGUIUtility.currentViewWidth > selectionWrapWidth) 171 | { 172 | EditorGUILayout.EndHorizontal(); 173 | } 174 | 175 | if (activeSelection.IsEmpty) 176 | { 177 | GUILayout.FlexibleSpace(); 178 | GUIStyle style = new GUIStyle(EditorStyles.wordWrappedLabel) {alignment = TextAnchor.MiddleCenter}; 179 | GUILayout.Label("No object selected.\n\nSelect something in Unity or use one of the selection helper buttons.", style); 180 | GUILayout.FlexibleSpace(); 181 | return; 182 | } 183 | 184 | if (activeSelection.Object != null) 185 | { 186 | if (activeSelection.Object is GameObject selectedGameObject) 187 | { 188 | List components = selectedGameObject.GetComponents().Cast().ToList(); 189 | components.RemoveAll(item => item == null); 190 | components.Insert(0, selectedGameObject); 191 | inspectedContexts = components.ToArray(); 192 | } 193 | #if ECS_EXISTS 194 | else if (activeSelection.Object is EntitySelectionProxy entitySelectionProxy) 195 | { 196 | EntityManager currentEntityManager = entitySelectionProxy.World.EntityManager; 197 | string name = currentEntityManager.GetName(entitySelectionProxy.Entity); 198 | 199 | if (string.IsNullOrEmpty(name)) 200 | { 201 | name = "Entity " + entitySelectionProxy.Entity.Index; 202 | } 203 | 204 | inspectedContexts = new object [1 + currentEntityManager.GetComponentCount(entitySelectionProxy.Entity)]; 205 | inspectedContexts[0] = activeSelection.Object; 206 | inspectedECSContexts = new ECSContext[1 + currentEntityManager.GetComponentCount(entitySelectionProxy.Entity)]; 207 | inspectedECSContexts[0] = new ECSContext {EntityManager = currentEntityManager, Entity = entitySelectionProxy.Entity}; 208 | 209 | NativeArray types = currentEntityManager.GetComponentTypes(entitySelectionProxy.Entity); 210 | for (var index = 0; index < types.Length; index++) 211 | { 212 | object componentData = ECSAccess.GetComponentData(currentEntityManager, entitySelectionProxy.Entity, types[index]); 213 | 214 | inspectedContexts[1 + index] = componentData; 215 | inspectedECSContexts[1 + index] = new ECSContext {EntityManager = currentEntityManager, Entity = entitySelectionProxy.Entity, ComponentType = types[index]}; 216 | } 217 | 218 | types.Dispose(); 219 | } 220 | #endif 221 | else 222 | { 223 | inspectedContexts = new[] {activeSelection.Object}; 224 | } 225 | 226 | inspectedTypes = inspectedContexts.Select(x => x.GetType()).ToArray(); 227 | } 228 | else 229 | { 230 | inspectedTypes = new[] {activeSelection.Type}; 231 | 232 | inspectedContexts = new Type[] {null}; 233 | } 234 | 235 | if (inspectedECSContexts == null) 236 | { 237 | inspectedECSContexts = new ECSContext[inspectedContexts.Length]; 238 | } 239 | 240 | GUILayout.Space(5); 241 | searchTerm = searchField.OnToolbarGUI(searchTerm); 242 | 243 | SidekickEditorGUI.BeginLabelHighlight(searchTerm); 244 | 245 | mode = SidekickUtility.EnumToolbar(mode); 246 | 247 | GUILayout.Space(5); 248 | scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); 249 | 250 | for (int i = 0; i < inspectedTypes.Length; i++) 251 | { 252 | Type type = inspectedTypes[i]; 253 | 254 | var inspectedContext = inspectedContexts[i]; 255 | var inspectedECSContext = inspectedECSContexts[i]; 256 | 257 | bool? activeOrEnabled = inspectedContext switch 258 | { 259 | GameObject gameObject => gameObject.activeSelf, 260 | Behaviour behaviour => behaviour.enabled, 261 | _ => null 262 | }; 263 | 264 | if (typesHidden.All(row => row.Key != type)) 265 | { 266 | typesHidden.Add(new KeyValuePair(type, false)); 267 | } 268 | 269 | int index = typesHidden.FindIndex(row => row.Key == type); 270 | 271 | string name; 272 | if (inspectedContexts[0] != null) 273 | { 274 | if (activeOrEnabled.HasValue) 275 | { 276 | name = " " + type.Name; 277 | } 278 | else 279 | { 280 | name = " " + type.Name; 281 | } 282 | 283 | if (i == 0 && inspectedContexts[i] is Object unityObject) 284 | { 285 | name += $" ({unityObject.name})"; 286 | } 287 | } 288 | else 289 | { 290 | name = type.Name + " (Class)"; 291 | } 292 | 293 | GUIContent content = new GUIContent(name, $"{type.FullName}, {type.Assembly.FullName}"); 294 | 295 | Rect foldoutRect = GUILayoutUtility.GetRect(GUIContent.none, EditorStyles.foldoutHeader); 296 | 297 | Rect toggleRect = foldoutRect; 298 | toggleRect.xMin += 36; 299 | toggleRect.width = 20; 300 | 301 | Rect iconRect = foldoutRect; 302 | iconRect.xMin += 16; 303 | iconRect.yMin += 1; 304 | iconRect.height = iconRect.width = 16; 305 | 306 | // Have to do this before BeginFoldoutHeaderGroup otherwise it'll consume the mouse down event 307 | if (activeOrEnabled.HasValue && SidekickEditorGUI.DetectClickInRect(toggleRect)) 308 | { 309 | switch (inspectedContexts[i]) 310 | { 311 | case GameObject gameObject: 312 | gameObject.SetActive(!gameObject.activeSelf); 313 | break; 314 | case Behaviour behaviour: 315 | behaviour.enabled = !behaviour.enabled; 316 | break; 317 | } 318 | } 319 | 320 | bool foldout = EditorGUI.BeginFoldoutHeaderGroup(foldoutRect, !typesHidden[index].Value, content, EditorStyles.foldoutHeader, rect => ClassUtilities.GetMenu(inspectedContext, inspectedECSContext).DropDown(rect)); 321 | 322 | Texture icon = SidekickEditorGUI.GetIcon(inspectedContexts[i], type); 323 | if (icon != null) 324 | { 325 | GUI.DrawTexture(iconRect, icon); 326 | } 327 | 328 | // Right click context menu 329 | if (SidekickEditorGUI.DetectClickInRect(foldoutRect, 1)) 330 | { 331 | ClassUtilities.GetMenu(inspectedContext, inspectedECSContext).DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); 332 | } 333 | 334 | if (activeOrEnabled.HasValue) 335 | { 336 | EditorGUI.Toggle(toggleRect, activeOrEnabled.Value); 337 | } 338 | 339 | EditorGUILayout.EndFoldoutHeaderGroup(); 340 | 341 | typesHidden[index] = new KeyValuePair(type, !foldout); 342 | 343 | if (!typesHidden[index].Value) 344 | { 345 | SidekickEditorGUI.DrawSplitter(0.5f); 346 | 347 | EditorGUI.indentLevel++; 348 | 349 | BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly; 350 | 351 | if (inspectedContext != null) // Is this an object instance? 352 | { 353 | bindingFlags |= BindingFlags.Instance; 354 | } 355 | 356 | var typeScope = type; 357 | 358 | while (typeScope != null) 359 | { 360 | if (InspectionExclusions.GetExcludedTypes().Contains(typeScope)) 361 | { 362 | break; 363 | } 364 | 365 | if (typeScope != type) 366 | { 367 | SidekickEditorGUI.DrawTypeChainHeader(new GUIContent(": " + typeScope.Name)); 368 | } 369 | 370 | FieldInfo[] fields = typeScope.GetFields(bindingFlags); 371 | PropertyInfo[] properties = typeScope.GetProperties(bindingFlags); 372 | MethodInfo[] methods = typeScope.GetMethods(bindingFlags); 373 | 374 | // Hide methods and backing fields that have been generated for properties 375 | if (SidekickSettings.HideAutoGenerated) 376 | { 377 | List methodList = new List(methods.Length); 378 | 379 | foreach (MethodInfo method in methods) 380 | { 381 | if (!TypeUtility.IsPropertyMethod(method, typeScope)) 382 | { 383 | methodList.Add(method); 384 | } 385 | } 386 | 387 | methods = methodList.ToArray(); 388 | 389 | List fieldList = new List(fields.Length); 390 | 391 | for (int j = 0; j < fields.Length; j++) 392 | { 393 | if (!TypeUtility.IsBackingField(fields[j], typeScope)) 394 | { 395 | fieldList.Add(fields[j]); 396 | } 397 | } 398 | 399 | fields = fieldList.ToArray(); 400 | } 401 | 402 | 403 | FieldInfo[] events = typeScope.GetFields(bindingFlags); 404 | 405 | if (mode == InspectorMode.Fields) 406 | { 407 | fieldPane.DrawFields(inspectedTypes[i], inspectedContexts[i], inspectedECSContext, searchTerm, fields); 408 | } 409 | else if (mode == InspectorMode.Properties) 410 | { 411 | propertyPane.DrawProperties(inspectedTypes[i], inspectedContexts[i], searchTerm, properties); 412 | } 413 | else if (mode == InspectorMode.Methods) 414 | { 415 | methodPane.DrawMethods(inspectedTypes[i], inspectedContexts[i], searchTerm, methods); 416 | } 417 | else if (mode == InspectorMode.Events) 418 | { 419 | eventPane.DrawEvents(inspectedTypes[i], inspectedContexts[i], searchTerm, events); 420 | } 421 | 422 | typeScope = typeScope.BaseType; 423 | } 424 | 425 | 426 | EditorGUI.indentLevel--; 427 | } 428 | 429 | SidekickEditorGUI.DrawSplitter(); 430 | } 431 | 432 | EditorGUILayout.Space(); 433 | EditorGUILayout.BeginHorizontal(); 434 | GUILayout.FlexibleSpace(); 435 | 436 | if(inspectedTypes[0] == typeof(GameObject) 437 | #if ECS_EXISTS 438 | || inspectedTypes[0] == typeof(EntitySelectionProxy) 439 | #endif 440 | ) 441 | { 442 | bool pressed = GUILayout.Button("Add Component", GUILayout.Width(230), GUILayout.Height(24)); 443 | var popupRect2 = GUILayoutUtility.GetLastRect(); 444 | popupRect2.width = EditorGUIUtility.currentViewWidth; 445 | if (pressed) 446 | { 447 | if (inspectedTypes[0] == typeof(GameObject)) 448 | { 449 | TypeSelectDropdown dropdown = new TypeSelectDropdown(new AdvancedDropdownState(), type => 450 | { 451 | 452 | ((GameObject) inspectedContexts[0]).AddComponent(type); 453 | }, new[] {typeof(Component)}); 454 | dropdown.Show(popupRect2); 455 | } 456 | #if ECS_EXISTS 457 | else if (inspectedTypes[0] == typeof(EntitySelectionProxy)) 458 | { 459 | TypeSelectDropdown dropdown = new TypeSelectDropdown(new AdvancedDropdownState(), type => 460 | { 461 | inspectedECSContexts[0].EntityManager.AddComponent(inspectedECSContexts[0].Entity, ComponentType.ReadWrite(type)); 462 | }, null, new []{typeof(IComponentData)}); 463 | dropdown.Show(popupRect2); 464 | } 465 | #endif 466 | } 467 | } 468 | 469 | GUILayout.FlexibleSpace(); 470 | EditorGUILayout.EndHorizontal(); 471 | 472 | EditorGUILayout.EndScrollView(); 473 | 474 | if (mode == InspectorMode.Methods) 475 | { 476 | methodPane.PostDraw(); 477 | } 478 | 479 | 480 | //if(AnimationHelper.AnimationActive) 481 | { 482 | // Cause repaint on next frame 483 | Repaint(); 484 | if (Event.current.type == EventType.Repaint) 485 | { 486 | //AnimationHelper.ClearAnimationActive(); 487 | } 488 | } 489 | 490 | SidekickEditorGUI.EndLabelHighlight(); 491 | } 492 | 493 | private void DrawToolbar() 494 | { 495 | GUIContent backContent = new GUIContent(SidekickEditorGUI.BackIcon, "Back"); 496 | GUIContent forwardContent = new GUIContent(SidekickEditorGUI.ForwardIcon, "Forward"); 497 | 498 | EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); 499 | 500 | HistoryButton(backContent, backStack, forwardStack); 501 | HistoryButton(forwardContent, forwardStack, backStack); 502 | 503 | GUI.enabled = true; 504 | 505 | // Spacer 506 | GUILayout.Label("", EditorStyles.toolbarButton, GUILayout.Width(6)); 507 | 508 | if (GUILayout.Button("Settings", EditorStyles.toolbarButton)) 509 | { 510 | SettingsService.OpenUserPreferences(SidekickSettingsRegister.SETTINGS_PATH); 511 | } 512 | 513 | 514 | EditorGUILayout.EndHorizontal(); 515 | } 516 | 517 | private void HistoryButton(GUIContent content, List stack, List otherStack) 518 | { 519 | GUI.enabled = (stack.Count > 0); 520 | SidekickEditorGUI.ButtonWithOptions(content, out bool mainPressed, out bool optionsPressed); 521 | 522 | if (mainPressed) 523 | { 524 | SwapStackElements(stack, otherStack); 525 | } 526 | 527 | if (optionsPressed) 528 | { 529 | GenericMenu genericMenu = new GenericMenu(); 530 | 531 | for (var index = 0; index < stack.Count; index++) 532 | { 533 | SelectionInfo selectionInfo = stack[index]; 534 | genericMenu.AddItem(new GUIContent($"{index} - {selectionInfo.GetDisplayName()}"), false, userData => 535 | { 536 | SwapStackElements(stack, otherStack, 1 + (int) userData); 537 | }, index); 538 | } 539 | 540 | genericMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); 541 | } 542 | } 543 | 544 | void SwapStackElements(List stack, List otherStack, int count = 1) 545 | { 546 | SelectionInfo stackPeek = stack[count - 1]; 547 | 548 | otherStack.Insert(0, activeSelection); 549 | 550 | for (int i = 0; i < count; i++) 551 | { 552 | var temp = stack[0]; 553 | stack.RemoveAt(0); 554 | if (i < count - 1) 555 | { 556 | otherStack.Insert(0, temp); 557 | } 558 | } 559 | 560 | activeSelection = stackPeek; 561 | 562 | if (stackPeek.Object is Object unityObject) 563 | { 564 | suppressNextSelectionDetection = true; 565 | Selection.activeObject = unityObject; 566 | } 567 | } 568 | 569 | public void SetSelection(object newSelection) 570 | { 571 | if (!activeSelection.IsEmpty && !backStack.FirstOrDefault().Equals(activeSelection)) 572 | { 573 | backStack.Insert(0, activeSelection); 574 | if (backStack.Count > BACK_STACK_LIMIT) 575 | { 576 | backStack.RemoveAt(backStack.Count - 1); 577 | } 578 | } 579 | 580 | forwardStack.Clear(); 581 | 582 | activeSelection = new SelectionInfo(newSelection); 583 | 584 | if (newSelection is Object unityObject) 585 | { 586 | Selection.activeObject = unityObject; 587 | } 588 | } 589 | 590 | public void SetSelection(Type newSelection) 591 | { 592 | if (!activeSelection.IsEmpty && !backStack.FirstOrDefault().Equals(activeSelection)) 593 | { 594 | backStack.Insert(0, activeSelection); 595 | if (backStack.Count > BACK_STACK_LIMIT) 596 | { 597 | backStack.RemoveAt(backStack.Count - 1); 598 | } 599 | } 600 | forwardStack.Clear(); 601 | 602 | activeSelection = new SelectionInfo(newSelection); 603 | } 604 | 605 | /// 606 | /// Make sure there's no deleted objects as we wouldn't be able to select them 607 | /// 608 | void CleanStacks() 609 | { 610 | forwardStack.RemoveAll(info => info.IsEmpty); 611 | backStack.RemoveAll(info => info.IsEmpty); 612 | } 613 | 614 | /// 615 | /// Note this is not the EditorWindow.OnSelectionChange message as that is only called when the window is 616 | /// focused. Instead we subscribe to selection changes on enable so that even if the window is not visible we 617 | /// can still track changes. 618 | /// 619 | void OnSelectionChangeNonMessage() 620 | { 621 | if (suppressNextSelectionDetection) 622 | { 623 | // Do nothing this time if we've just been manipulating selection ourselves 624 | suppressNextSelectionDetection = false; 625 | return; 626 | } 627 | 628 | if (selectionLocked) 629 | { 630 | return; 631 | } 632 | 633 | if (Selection.activeObject == null) 634 | { 635 | return; 636 | } 637 | 638 | SetSelection(Selection.activeObject); 639 | 640 | Repaint(); 641 | } 642 | } 643 | } --------------------------------------------------------------------------------