├── Plugins ├── InspectPlus │ ├── InspectPlus.Runtime.asmdef │ ├── README.txt │ ├── Editor.meta │ ├── InspectPlus.Runtime.asmdef.meta │ ├── README.txt.meta │ ├── Editor │ │ ├── InspectPlus.Editor.asmdef.meta │ │ ├── Utilities.cs.meta │ │ ├── DebugModeEntry.cs.meta │ │ ├── VariableGetter.cs.meta │ │ ├── InspectPlusSettings.cs.meta │ │ ├── InspectPlusWindow.cs.meta │ │ ├── SerializedClipboard.cs.meta │ │ ├── MenuItems.cs.meta │ │ ├── SerializablePropertyExtensions.cs.meta │ │ ├── BasketWindow.cs.meta │ │ ├── InputDialog.cs.meta │ │ ├── PasteBinWindow.cs.meta │ │ ├── TypeWrapper.cs.meta │ │ ├── CustomProjectWindow.cs.meta │ │ ├── ObjectBrowserWindow.cs.meta │ │ ├── ObjectDiffWindow.cs.meta │ │ ├── PasteBinTooltip.cs.meta │ │ ├── CustomHierarchyWindow.cs.meta │ │ ├── PasteBinContextWindow.cs.meta │ │ ├── ComponentGroupCopyPasteWindow.cs.meta │ │ ├── InspectPlus.Editor.asmdef │ │ ├── TypeWrapper.cs │ │ ├── InputDialog.cs │ │ ├── PasteBinTooltip.cs │ │ ├── InspectPlusSettings.cs │ │ ├── VariableGetter.cs │ │ ├── ObjectBrowserWindow.cs │ │ ├── PasteBinContextWindow.cs │ │ ├── ComponentGroupCopyPasteWindow.cs │ │ ├── Utilities.cs │ │ ├── DebugModeEntry.cs │ │ ├── PasteBinWindow.cs │ │ ├── BasketWindow.cs │ │ ├── CustomHierarchyWindow.cs │ │ └── CustomProjectWindow.cs │ ├── Other.meta │ └── Other │ │ ├── SceneFavoritesHolder.cs.meta │ │ └── SceneFavoritesHolder.cs └── InspectPlus.meta ├── .github ├── Images │ ├── Basket.png │ ├── DebugMode.png │ ├── DiffWindow.png │ ├── InspectType.gif │ ├── CopyProperties.png │ ├── FavoritesList.png │ ├── IsolatedFolder.png │ ├── PasteFromBin.png │ ├── Screenshot_Old.png │ ├── InspectPlusWindow.png │ ├── IsolatedHierarchy.png │ └── CopyMultipleComponents.gif └── README.md ├── LICENSE.txt.meta ├── package.json.meta ├── Plugins.meta ├── package.json └── LICENSE.txt /Plugins/InspectPlus/InspectPlus.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "InspectPlus.Runtime" 3 | } 4 | -------------------------------------------------------------------------------- /.github/Images/Basket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/Basket.png -------------------------------------------------------------------------------- /.github/Images/DebugMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/DebugMode.png -------------------------------------------------------------------------------- /.github/Images/DiffWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/DiffWindow.png -------------------------------------------------------------------------------- /.github/Images/InspectType.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/InspectType.gif -------------------------------------------------------------------------------- /.github/Images/CopyProperties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/CopyProperties.png -------------------------------------------------------------------------------- /.github/Images/FavoritesList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/FavoritesList.png -------------------------------------------------------------------------------- /.github/Images/IsolatedFolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/IsolatedFolder.png -------------------------------------------------------------------------------- /.github/Images/PasteFromBin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/PasteFromBin.png -------------------------------------------------------------------------------- /.github/Images/Screenshot_Old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/Screenshot_Old.png -------------------------------------------------------------------------------- /.github/Images/InspectPlusWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/InspectPlusWindow.png -------------------------------------------------------------------------------- /.github/Images/IsolatedHierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/IsolatedHierarchy.png -------------------------------------------------------------------------------- /.github/Images/CopyMultipleComponents.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasirkula/UnityInspectPlus/HEAD/.github/Images/CopyMultipleComponents.gif -------------------------------------------------------------------------------- /Plugins/InspectPlus/README.txt: -------------------------------------------------------------------------------- 1 | = Inspect+ (v2.1.0) = 2 | 3 | Documentation: https://github.com/yasirkula/UnityInspectPlus 4 | E-mail: yasirkula@gmail.com -------------------------------------------------------------------------------- /LICENSE.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5c207cf1ec4303f428670b60a837b365 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 295ce88604dd2c9458eded0a97248eb4 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1d3ac4b7817179b44b5b0e158cf6ac8c 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/InspectPlus.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: db7f13a558fd7554bbcc86d40e4e5eb7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a90ec479c42a9ef4083371125594dbc7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/InspectPlus.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 87c9414dc782f82419a783f415f5ee19 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/README.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 23a864ec2bc41d84cbb953b10dd73b56 3 | timeCreated: 1563308465 4 | licenseType: Free 5 | TextScriptImporter: 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/InspectPlus.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f79e68d4935263c44a9451a81a8759a2 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Other.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3bb3d3df673972c45ad0cdec7c5bb22b 3 | folderAsset: yes 4 | timeCreated: 1571948004 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/Utilities.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a9d4fd784a6d59f488fd66ad1762bda6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/DebugModeEntry.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6f46de5e6b04f1347bfaa3086d43d517 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/VariableGetter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cc77df5f8984cf7468f62a6bd65e8fd1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/InspectPlusSettings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d7273ea0be8115043a05674b824ee154 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/InspectPlusWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 697217bf12c7ce44c8557aff1093a749 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/SerializedClipboard.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bb7afcb570c39c04b8163dd5aee546e4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Other/SceneFavoritesHolder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 13e8a564be926574fad674834ab13d6b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/MenuItems.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9e02f41f9df3ce84d956ec426fd91b4b 3 | timeCreated: 1604519902 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/SerializablePropertyExtensions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a972856e61619d548a718647194f3be9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/BasketWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7e3d37958bd88e34ca38f290aa521acb 3 | timeCreated: 1641661059 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/InputDialog.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 023869e58397f4048b37c26020a762b5 3 | timeCreated: 1732609727 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/PasteBinWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 92fa7d0db9ee6de40a5bb4d9283e6a6e 3 | timeCreated: 1582219473 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/TypeWrapper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 601dcc85f7fd8db45b89909555dab1bc 3 | timeCreated: 1732605659 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/CustomProjectWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 507f59f2ac5c29b45bc8c11ea72dd6c5 3 | timeCreated: 1474880986 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/ObjectBrowserWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f3ed1a5a17dc0104f922d827d8a99c81 3 | timeCreated: 1582191773 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/ObjectDiffWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 550e7fb68096d744e8aa02d7b0ed863b 3 | timeCreated: 1617274040 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/PasteBinTooltip.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dab374f4d28d7c94e9058c76f8a0a039 3 | timeCreated: 1604654868 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/CustomHierarchyWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 69a3f5517e7b1a248a1a374189b1ba7b 3 | timeCreated: 1471510735 4 | licenseType: Pro 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/PasteBinContextWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a03e5691381150145b9b5a20b59728c2 3 | timeCreated: 1601118263 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/ComponentGroupCopyPasteWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fae2291dbfb04574383593c3c63dd8ce 3 | timeCreated: 1632345177 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/InspectPlus.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "InspectPlus.Editor", 3 | "references": [ 4 | "InspectPlus.Runtime" 5 | ], 6 | "includePlatforms": [ 7 | "Editor" 8 | ], 9 | "excludePlatforms": [], 10 | "allowUnsafeCode": false, 11 | "overrideReferences": false, 12 | "precompiledReferences": [], 13 | "autoReferenced": true, 14 | "defineConstraints": [], 15 | "versionDefines": [], 16 | "noEngineReferences": false 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.yasirkula.inspectplus", 3 | "displayName": "Inspect+", 4 | "version": "2.1.0", 5 | "documentationUrl": "https://github.com/yasirkula/UnityInspectPlus", 6 | "changelogUrl": "https://github.com/yasirkula/UnityInspectPlus/releases", 7 | "licensesUrl": "https://github.com/yasirkula/UnityInspectPlus/blob/master/LICENSE.txt", 8 | "description": "This plugin helps you view an object's Inspector in a separate tab/window, copy&paste the values of variables in the Inspector and inspect all variables of an object (including non-serializable and static variables) in an enhanced Debug mode." 9 | } 10 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Other/SceneFavoritesHolder.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using UnityEngine.SceneManagement; 3 | using UnityEditor; 4 | using UnityEditor.SceneManagement; 5 | #endif 6 | using UnityEngine; 7 | 8 | namespace InspectPlusNamespace 9 | { 10 | /// 11 | /// No longer used because it's saved in scene and therefore isn't user-specific. Use "Window/Inspect+/Basket" instead. 12 | /// 13 | [ExecuteInEditMode] 14 | public class SceneFavoritesHolder : MonoBehaviour 15 | { 16 | #if UNITY_EDITOR 17 | private void Awake() 18 | { 19 | EditorApplication.delayCall += () => 20 | { 21 | if( this != null && !Application.isPlaying ) 22 | { 23 | Scene scene = gameObject.scene; 24 | DestroyImmediate( gameObject ); 25 | EditorSceneManager.MarkSceneDirty( scene ); 26 | Debug.Log( "(Inspect+) Removed deprecated SceneFavoritesHolder GameObject from scene: " + scene.name ); 27 | } 28 | }; 29 | } 30 | #endif 31 | } 32 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/TypeWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace InspectPlusNamespace 5 | { 6 | public class TypeWrapper : ScriptableObject, ISerializationCallbackReceiver 7 | { 8 | public Type Type { get; private set; } 9 | 10 | [SerializeField, HideInInspector] 11 | private string typeName; 12 | 13 | public static TypeWrapper Create( Type type ) 14 | { 15 | TypeWrapper result = CreateInstance(); 16 | result.name = type.Name + " Statics"; 17 | result.Type = type; 18 | result.hideFlags = HideFlags.DontSaveInBuild | HideFlags.DontSaveInEditor; 19 | return result; 20 | } 21 | 22 | void ISerializationCallbackReceiver.OnBeforeSerialize() 23 | { 24 | if( Type != null ) 25 | typeName = Type.AssemblyQualifiedName; 26 | } 27 | 28 | void ISerializationCallbackReceiver.OnAfterDeserialize() 29 | { 30 | if( !string.IsNullOrEmpty( typeName ) ) 31 | Type = Type.GetType( typeName ); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Süleyman Yasir KULA 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 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/InputDialog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | 6 | namespace InspectPlusNamespace 7 | { 8 | public abstract class InputDialog : EditorWindow 9 | { 10 | private string description; 11 | protected T value; 12 | private Action onResult; 13 | private Action onCancel; 14 | 15 | private bool initialized; 16 | private Vector2 scrollPosition; 17 | 18 | protected void Initialize( string description, T value, Action onResult, Action onCancel ) 19 | { 20 | this.description = description; 21 | this.value = value; 22 | this.onResult = onResult; 23 | this.onCancel = onCancel; 24 | 25 | titleContent = GUIContent.none; 26 | minSize = new Vector2( 100f, 50f ); 27 | 28 | ShowAuxWindow(); 29 | Focus(); 30 | } 31 | 32 | protected void OnDisable() 33 | { 34 | onCancel?.Invoke(); 35 | onResult = null; 36 | onCancel = null; 37 | } 38 | 39 | private void OnGUI() 40 | { 41 | Event ev = Event.current; 42 | bool inputSubmitted = ev.type == EventType.KeyDown && ev.character == '\n'; 43 | 44 | scrollPosition = EditorGUILayout.BeginScrollView( scrollPosition ); 45 | 46 | if( !initialized ) 47 | GUILayout.BeginVertical(); 48 | 49 | GUILayout.Label( description, EditorStyles.wordWrappedLabel ); 50 | 51 | GUI.SetNextControlName( "InputD" ); 52 | OnInputGUI(); 53 | 54 | if( GUILayout.Button( "OK" ) || inputSubmitted ) 55 | { 56 | onResult?.Invoke( value ); 57 | onResult = null; 58 | onCancel = null; 59 | 60 | Close(); 61 | } 62 | 63 | if( !initialized ) 64 | { 65 | GUILayout.EndVertical(); 66 | 67 | float preferredHeight = GUILayoutUtility.GetLastRect().height; 68 | if( preferredHeight > 10f ) 69 | { 70 | position = new Rect( position.position, new Vector2( position.width, preferredHeight + 15f ) ); 71 | initialized = true; 72 | 73 | EditorGUI.FocusTextInControl( "InputD" ); 74 | GUIUtility.ExitGUI(); 75 | } 76 | } 77 | 78 | EditorGUILayout.EndScrollView(); 79 | } 80 | 81 | protected abstract void OnInputGUI(); 82 | } 83 | 84 | public class StringInputDialog : InputDialog 85 | { 86 | public static void Show( string description, string value, Action onResult, Action onCancel = null ) 87 | { 88 | CreateInstance().Initialize( description, value, onResult, onCancel ); 89 | } 90 | 91 | protected override void OnInputGUI() 92 | { 93 | value = EditorGUILayout.TextField( GUIContent.none, value ); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/PasteBinTooltip.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace InspectPlusNamespace 6 | { 7 | public class PasteBinTooltip : EditorWindow 8 | { 9 | private static PasteBinTooltip mainWindow; 10 | private static string tooltip; 11 | 12 | private static GUIStyle m_style; 13 | internal static GUIStyle Style 14 | { 15 | get 16 | { 17 | if( m_style == null ) 18 | m_style = (GUIStyle) typeof( EditorStyles ).GetProperty( "tooltip", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ).GetValue( null, null ); 19 | 20 | return m_style; 21 | } 22 | } 23 | 24 | public static void Show( Rect sourcePosition, string tooltip ) 25 | { 26 | // Don't lose focus to the previous window (in this case, PasteBinContextWindow which automatically closes when it loses focus) 27 | EditorWindow prevFocusedWindow = focusedWindow; 28 | 29 | if (!mainWindow) 30 | { 31 | mainWindow = CreateInstance(); 32 | mainWindow.ShowPopup(); 33 | } 34 | 35 | Vector2 preferredSize = Style.CalcSize( new GUIContent( tooltip ) ) + Style.contentOffset + new Vector2( Style.padding.horizontal + Style.margin.horizontal, Style.padding.vertical + Style.margin.vertical ); 36 | Rect preferredPosition; 37 | 38 | Rect positionLeft = new Rect( sourcePosition.position - new Vector2( preferredSize.x, 0f ), preferredSize ); 39 | Rect screenFittedPositionLeft = Utilities.GetScreenFittedRect(positionLeft, mainWindow); 40 | 41 | Vector2 positionOffset = positionLeft.position - screenFittedPositionLeft.position; 42 | Vector2 sizeOffset = positionLeft.size - screenFittedPositionLeft.size; 43 | if( positionOffset.sqrMagnitude <= 400f && sizeOffset.sqrMagnitude <= 400f ) 44 | preferredPosition = screenFittedPositionLeft; 45 | else 46 | { 47 | Rect positionRight = new Rect( sourcePosition.position + new Vector2( sourcePosition.width, 0f ), preferredSize ); 48 | Rect screenFittedPositionRight = Utilities.GetScreenFittedRect(positionRight, mainWindow); 49 | 50 | Vector2 positionOffset2 = positionRight.position - screenFittedPositionRight.position; 51 | Vector2 sizeOffset2 = positionRight.size - screenFittedPositionRight.size; 52 | if( positionOffset2.magnitude + sizeOffset2.magnitude < positionOffset.magnitude + sizeOffset.magnitude ) 53 | preferredPosition = screenFittedPositionRight; 54 | else 55 | preferredPosition = screenFittedPositionLeft; 56 | } 57 | 58 | PasteBinTooltip.tooltip = tooltip; 59 | mainWindow.minSize = preferredPosition.size; 60 | mainWindow.position = preferredPosition; 61 | mainWindow.Repaint(); 62 | 63 | if( prevFocusedWindow ) 64 | prevFocusedWindow.Focus(); 65 | } 66 | 67 | public static void Hide() 68 | { 69 | if( mainWindow ) 70 | { 71 | mainWindow.Close(); 72 | mainWindow = null; 73 | } 74 | } 75 | 76 | private void OnGUI() 77 | { 78 | // If somehow the tooltip isn't automatically closed, allow closing it by clicking on it 79 | if( Event.current.type == EventType.MouseDown ) 80 | { 81 | Hide(); 82 | GUIUtility.ExitGUI(); 83 | } 84 | 85 | GUI.Label( new Rect( Vector2.zero, position.size ), tooltip, Style ); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # Unity Inspect+ Plugin 2 | 3 | This plugin helps you view an object's Inspector in a separate tab/window, copy&paste the values of variables in the Inspector, inspect all variables of an object (including non-serializable and static variables) in an enhanced Debug mode and more. 4 | 5 | **Discord:** https://discord.gg/UJJt549AaV 6 | 7 | **[GitHub Sponsors ☕](https://github.com/sponsors/yasirkula)** 8 | 9 | ## INSTALLATION 10 | 11 | There are 4 ways to install this plugin: 12 | 13 | - import [InspectPlus.unitypackage](https://github.com/yasirkula/UnityInspectPlus/releases) via *Assets-Import Package* 14 | - clone/[download](https://github.com/yasirkula/UnityInspectPlus/archive/master.zip) this repository and move the *Plugins* folder to your Unity project's *Assets* folder 15 | - *(via Package Manager)* click the + button and install the package from the following git URL: 16 | - `https://github.com/yasirkula/UnityInspectPlus.git` 17 | - *(via [OpenUPM](https://openupm.com))* after installing [openupm-cli](https://github.com/openupm/openupm-cli), run the following command: 18 | - `openupm add com.yasirkula.inspectplus` 19 | 20 | ## HOW TO 21 | 22 | - You can open the Inspect+ window in a number of ways: 23 | - right clicking an object in *Project* or *Hierarchy* windows 24 | - right clicking an *Object* variable in the Inspector 25 | - right clicking a component in the Inspector 26 | - selecting **Window/Inspect+/New Window** menu item 27 | - calling the `InspectPlusNamespace.InspectPlusWindow.Inspect` functions from your editor scripts 28 | 29 | ![screenshot](Images/InspectPlusWindow.png) 30 | 31 | - You can right click an object in the **History** list to add it to the **Favorites** list 32 | - You can drag&drop objects to the History and Favorites lists to quickly fill these lists 33 | - You can right click the icons of the History and Favorites lists to quickly select an object from these lists 34 | 35 | ![screenshot](Images/FavoritesList.png) 36 | 37 | - You can right click variables or components in the Inspector to copy&paste their values. This supports all variable types: primitives, scene objects, assets, managed references, arrays, serializable objects and etc. Paste operation is quite flexible, as well; you can paste different vector types to each other and paste any component to another with no type restrictions, as long as these components have some variables with the same name. Note that copy/paste menu won't show up for variables that are not drawn with *SerializedProperty* 38 | 39 | ![screenshot](Images/CopyProperties.png) 40 | 41 | - You can right click a component and copy multiple components attached to that GameObject at once. Then, you can right click another component and paste multiple components at once 42 | 43 | ![screenshot](Images/CopyMultipleComponents.gif) 44 | 45 | - You can right click an object in Hierarchy and copy its complete hierarchy (with or without its children). Then, you can paste these objects to another Unity project's hierarchy; Unity versions don't need to match (however, assets that don't exist on the other project will become missing references) 46 | - You can right click the Inspect+ tab to enable **Debug mode**: you can inspect all variables of an object in this mode, including static, readonly and non-serializable variables 47 | 48 | ![screenshot](Images/DebugMode.png) 49 | 50 | - Select **Window/Inspect+/Inspect Type** to inspect a Type's static variables (e.g. *UnityEngine.Time*) (tip: if you put an '\*' at the end of the type name, the first Object instance of the given Type will be inspected instead) 51 | 52 | ![screenshot](Images/InspectType.gif) 53 | 54 | - You can right click an object in Hierarchy and select the **Isolated Hierarchy** option to open a Hierarchy window that displays only that object's children 55 | 56 | ![screenshot](Images/IsolatedHierarchy.png) 57 | 58 | - You can open a folder with Inspect+ to see its contents in an isolated Project view 59 | 60 | ![screenshot](Images/IsolatedFolder.png) 61 | 62 | - You can open Paste Bin via **Window/Inspect+/Paste Bin**: this window lists the copied variables and is shared between all Unity projects (so, copying a value in Project A will make that value available in Project B). You can also right click variables, components or materials in the Inspector and select **Paste Values From Bin** to quickly select and paste a value from Paste Bin 63 | 64 | ![screenshot](Images/PasteFromBin.png) 65 | 66 | - You can open Basket via **Window/Inspect+/Basket**: this window stores the objects that you drag&drop inside it. You can right click the window's tab to save its contents to a file (on Unity 2019.1 or earlier, scene object contents aren't saved) 67 | 68 | ![screenshot](Images/Basket.png) 69 | 70 | - You can open Object Diff Window via **Window/Inspect+/Diff Window**: this window lets you see the differences between two objects in your project (diff of two GameObjects won't include their child GameObjects) 71 | 72 | ![screenshot](Images/DiffWindow.png) 73 | -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/InspectPlusSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using UnityEditor; 4 | using UnityEditorInternal; 5 | using UnityEngine; 6 | 7 | namespace InspectPlusNamespace 8 | { 9 | public class InspectPlusSettings : ScriptableObject 10 | { 11 | private const string SAVE_PATH = "UserSettings/InspectPlusSettings.asset"; 12 | 13 | private static InspectPlusSettings m_instance; 14 | public static InspectPlusSettings Instance 15 | { 16 | get 17 | { 18 | if( m_instance == null ) 19 | { 20 | if( File.Exists( SAVE_PATH ) ) 21 | m_instance = InternalEditorUtility.LoadSerializedFileAndForget( SAVE_PATH )[0] as InspectPlusSettings; 22 | else 23 | m_instance = CreateInstance(); 24 | 25 | m_instance.name = typeof( InspectPlusSettings ).Name; 26 | m_instance.hideFlags = HideFlags.DontSave; 27 | } 28 | 29 | return m_instance; 30 | } 31 | } 32 | 33 | public List FavoriteAssets = new List(); 34 | 35 | [Space] 36 | [Tooltip( "Determines whether Favorites and History lists should be drawn horizontally or vertically" )] 37 | public bool CompactFavoritesAndHistoryLists = true; 38 | 39 | [Tooltip( "New windows should show Favorites list by default (if Favorites is not empty)" )] 40 | public bool ShowFavoritesByDefault = true; 41 | [Tooltip( "New windows should show History list by default (if History is not empty)" )] 42 | public bool ShowHistoryByDefault = true; 43 | 44 | [Space] 45 | [Tooltip( "If enabled, a new Unity tab will be created for objects when 'Open In New Tab' is clicked. Otherwise, these objects will be added to the active Inspect+ window's history list" )] 46 | public bool OpenNewTabsAsUnityTabs = true; 47 | [Tooltip("If enabled, new tabs will automatically be opened in Debug mode.")] 48 | public bool OpenNewTabsInDebugMode; 49 | 50 | [Space] 51 | [Tooltip( "Height of the Favorites list" )] 52 | public float FavoritesHeight = 42f; 53 | [Tooltip( "Height of the History list" )] 54 | public float HistoryHeight = 42f; 55 | [Tooltip( "Height of the compact Favorites and History lists" )] 56 | public float CompactListHeight = 28f; 57 | 58 | [Space] 59 | [HideInInspector] 60 | public ObjectBrowserWindow.SortType FavoritesSortType = ObjectBrowserWindow.SortType.Name; 61 | [HideInInspector] 62 | public ObjectBrowserWindow.SortType HistorySortType = ObjectBrowserWindow.SortType.None; 63 | 64 | [Space] 65 | [Tooltip( "Refresh and repaint interval of the Inspector in Normal mode" )] 66 | public float NormalModeRefreshInterval = 0.5f; 67 | [Tooltip( "Refresh interval of the Inspector in Debug mode" )] 68 | public float DebugModeRefreshInterval = 0.5f; 69 | 70 | [Space] 71 | [Tooltip( "When an asset or scene object's path is calculated, its path relative to the Object that the copy operation was performed on will also be calculated. During a paste operation, this path will be used first for smart paste operations (e.g. imagine objects A and B having children named C. When a variable on A is copied and that variable points to A.C, after pasting that variable to B, the value will be resolved to B.C instead of A.C)" )] 72 | public bool SmartCopyPaste = false; 73 | 74 | [Space] 75 | [Tooltip( "While inspecting a folder, selecting files/folders inside that folder will update Unity's selection, as well" )] 76 | public bool SyncProjectWindowSelection = true; 77 | [Tooltip( "While inspecting an object's Isolated Hierarchy, selecting child objects inside that hierarchy will update Unity's selection, as well" )] 78 | public bool SyncIsolatedHierarchyWindowSelection = true; 79 | [Tooltip( "Selecting objects in Basket window will update Unity's selection, as well" )] 80 | public bool SyncBasketSelection = true; 81 | 82 | [Space] 83 | [Tooltip( "Selecting an object in Favorites or History will highlight the object in Hierarchy/Project" )] 84 | public bool AutomaticallyPingSelectedObject = true; 85 | [Tooltip( "Clearing the History via context menu will delete the currently inspected object's History entry, as well" )] 86 | public bool ClearingHistoryRemovesActiveObject = false; 87 | 88 | private double autoSaveTime; 89 | 90 | /// 91 | /// Without this constructor, is reset after domain reload (causing duplicate instances in memory). 92 | /// 93 | protected InspectPlusSettings() 94 | { 95 | m_instance = this; 96 | } 97 | 98 | protected void OnEnable() 99 | { 100 | for( int i = FavoriteAssets.Count - 1; i >= 0; i-- ) 101 | { 102 | if( !FavoriteAssets[i] ) 103 | FavoriteAssets.RemoveAt( i ); 104 | } 105 | 106 | /// After domain reload, is invoked just before . Don't save settings after every domain reload, so reset here. 107 | autoSaveTime = 0; 108 | EditorApplication.update -= OnEditorUpdate; 109 | 110 | // If a settings asset in Assets folder is selected (they were saved in Assets on older versions of Inspect+), delete it 111 | string path = AssetDatabase.GetAssetPath( this ); 112 | if( !string.IsNullOrEmpty( path ) && path.StartsWith( "Assets/" ) ) 113 | { 114 | Debug.LogWarning( "Inspect+ settings are now loaded from \"" + SAVE_PATH + "\". Deleting obsolete asset: \"" + AssetDatabase.GetAssetPath( this ) + "\"" ); 115 | AssetDatabase.DeleteAsset( path ); 116 | m_instance = null; 117 | } 118 | } 119 | 120 | /// 121 | /// Since this asset is no longer serialized in Assets folder (), it isn't auto-saved by AssetDatabase. So we need to save it manually when a change is made. 122 | /// A timer is used to avoid excessive auto-saving while a value is rapidly changing (e.g. changing a float variable by dragging its name). 123 | /// 124 | protected void OnValidate() 125 | { 126 | if( autoSaveTime == 0 ) 127 | EditorApplication.update += OnEditorUpdate; 128 | 129 | autoSaveTime = EditorApplication.timeSinceStartup + 2; 130 | } 131 | 132 | private void OnEditorUpdate() 133 | { 134 | if( EditorApplication.timeSinceStartup >= autoSaveTime ) 135 | { 136 | EditorApplication.update -= OnEditorUpdate; 137 | Save(); 138 | } 139 | } 140 | 141 | public void Save() 142 | { 143 | autoSaveTime = 0; 144 | Directory.CreateDirectory( Path.GetDirectoryName( SAVE_PATH ) ); 145 | InternalEditorUtility.SaveToSerializedFileAndForget( new[] { this }, SAVE_PATH, true ); 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/VariableGetter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Text; 4 | 5 | namespace InspectPlusNamespace 6 | { 7 | // Delegates to get/set the value of a variable (field, property or method) 8 | public delegate object VariableGetVal( object obj ); 9 | public delegate void VariableSetVal( object obj, object value ); 10 | 11 | // Custom struct to hold a variable's description and its getter function 12 | public readonly struct VariableGetterHolder : IComparable 13 | { 14 | public readonly string description; 15 | public readonly Type type; 16 | private readonly string name; 17 | private readonly VariableGetVal getter; 18 | private readonly VariableSetVal setter; 19 | 20 | public VariableGetterHolder( string description, Type type, VariableGetVal getter, VariableSetVal setter ) 21 | { 22 | this.description = description; 23 | this.type = type; 24 | this.name = description; 25 | this.getter = getter; 26 | this.setter = setter; 27 | } 28 | 29 | public VariableGetterHolder( FieldInfo fieldInfo, VariableGetVal getter, VariableSetVal setter ) 30 | { 31 | type = fieldInfo.FieldType; 32 | 33 | StringBuilder sb = Utilities.stringBuilder; 34 | sb.Length = 0; 35 | sb.Append( "(F" ); 36 | 37 | if( fieldInfo.IsPublic ) 38 | sb.Append( "+)" ); 39 | else if( fieldInfo.IsFamily || fieldInfo.IsAssembly ) 40 | sb.Append( "#)" ); 41 | else 42 | sb.Append( "-)" ); 43 | 44 | if( fieldInfo.IsStatic ) 45 | sb.Append( "(S)" ); 46 | if( Attribute.IsDefined( fieldInfo, typeof( ObsoleteAttribute ) ) ) 47 | sb.Append( "(O)" ); 48 | 49 | sb.Append( " " ).Append( fieldInfo.Name ).Append( " (" ).AppendType( type ).Append( ")" ); 50 | 51 | this.description = sb.ToString(); 52 | this.name = fieldInfo.Name; 53 | this.getter = getter; 54 | this.setter = setter; 55 | } 56 | 57 | public VariableGetterHolder( PropertyInfo propertyInfo, VariableGetVal getter, VariableSetVal setter ) 58 | { 59 | type = propertyInfo.PropertyType; 60 | 61 | StringBuilder sb = Utilities.stringBuilder; 62 | sb.Length = 0; 63 | sb.Append( "(P" ); 64 | 65 | MethodInfo getMethod = propertyInfo.GetGetMethod( true ); 66 | if( getMethod.IsPublic ) 67 | sb.Append( "+)" ); 68 | else if( getMethod.IsFamily || getMethod.IsAssembly ) 69 | sb.Append( "#)" ); 70 | else 71 | sb.Append( "-)" ); 72 | 73 | if( getMethod.IsStatic ) 74 | sb.Append( "(S)" ); 75 | if( Attribute.IsDefined( propertyInfo, typeof( ObsoleteAttribute ) ) ) 76 | sb.Append( "(O)" ); 77 | 78 | sb.Append( " " ).Append( propertyInfo.Name ).Append( " (" ).AppendType( type ).Append( ")" ); 79 | 80 | this.description = sb.ToString(); 81 | this.name = propertyInfo.Name; 82 | this.getter = getter; 83 | this.setter = setter; 84 | } 85 | 86 | public VariableGetterHolder( MethodInfo methodInfo, VariableGetVal getter ) 87 | { 88 | type = methodInfo.ReturnType; 89 | 90 | StringBuilder sb = Utilities.stringBuilder; 91 | sb.Length = 0; 92 | sb.Append( "(M" ); 93 | 94 | if( methodInfo.IsPublic ) 95 | sb.Append( "+)" ); 96 | else if( methodInfo.IsFamily || methodInfo.IsAssembly ) 97 | sb.Append( "#)" ); 98 | else 99 | sb.Append( "-)" ); 100 | 101 | if( methodInfo.IsStatic ) 102 | sb.Append( "(S)" ); 103 | if( Attribute.IsDefined( methodInfo, typeof( ObsoleteAttribute ) ) ) 104 | sb.Append( "(O)" ); 105 | 106 | sb.Append( " " ).Append( methodInfo.Name ).Append( "() (" ).AppendType( type ).Append( ")" ); 107 | 108 | this.description = sb.ToString(); 109 | this.name = methodInfo.Name; 110 | this.getter = getter; 111 | this.setter = null; 112 | } 113 | 114 | public readonly object Get( object obj ) 115 | { 116 | return getter( obj ); 117 | } 118 | 119 | public readonly void Set( object obj, object value ) 120 | { 121 | if( setter != null ) 122 | setter( obj, value ); 123 | } 124 | 125 | readonly int IComparable.CompareTo( VariableGetterHolder other ) 126 | { 127 | return name.CompareTo( other.name ); 128 | } 129 | } 130 | 131 | // Credit: http://stackoverflow.com/questions/724143/how-do-i-create-a-delegate-for-a-net-property 132 | public interface IPropertyWrapper 133 | { 134 | object GetValue( object source ); 135 | void SetValue( object source, object value ); 136 | } 137 | 138 | // A wrapper class for properties to get/set their values more efficiently 139 | public class PropertyWrapper : IPropertyWrapper where TObject : class 140 | { 141 | private readonly Func getter; 142 | private readonly Action setter; 143 | 144 | public PropertyWrapper( MethodInfo getterMethod, MethodInfo setterMethod ) 145 | { 146 | getter = (Func) Delegate.CreateDelegate( typeof( Func ), getterMethod ); 147 | if( setterMethod != null ) 148 | setter = (Action) Delegate.CreateDelegate( typeof( Action ), setterMethod ); 149 | } 150 | 151 | public object GetValue( object obj ) 152 | { 153 | try 154 | { 155 | return getter( (TObject) obj ); 156 | } 157 | catch 158 | { 159 | // Property getters may return various kinds of exceptions 160 | // if their backing fields are not initialized (yet) 161 | return null; 162 | } 163 | } 164 | 165 | public void SetValue( object obj, object value ) 166 | { 167 | if( setter != null ) 168 | setter( (TObject) obj, (TValue) value ); 169 | } 170 | } 171 | 172 | // PropertyWrapper for static properties 173 | public class PropertyWrapper : IPropertyWrapper 174 | { 175 | private readonly Func getter; 176 | private readonly Action setter; 177 | 178 | public PropertyWrapper( MethodInfo getterMethod, MethodInfo setterMethod ) 179 | { 180 | getter = (Func) Delegate.CreateDelegate( typeof( Func ), getterMethod ); 181 | if( setterMethod != null ) 182 | setter = (Action) Delegate.CreateDelegate( typeof( Action ), setterMethod ); 183 | } 184 | 185 | public object GetValue( object obj ) 186 | { 187 | try 188 | { 189 | return getter(); 190 | } 191 | catch 192 | { 193 | return null; 194 | } 195 | } 196 | 197 | public void SetValue( object obj, object value ) 198 | { 199 | if( setter != null ) 200 | setter( (TValue) value ); 201 | } 202 | } 203 | 204 | // Using constant values in VariableGetterHolder 205 | public readonly struct ConstantValueGetter 206 | { 207 | private readonly object value; 208 | 209 | public ConstantValueGetter( object value ) 210 | { 211 | this.value = value; 212 | } 213 | 214 | public readonly object GetValue( object obj ) 215 | { 216 | return value; 217 | } 218 | } 219 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/ObjectBrowserWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace InspectPlusNamespace 6 | { 7 | public class ObjectBrowserWindow : EditorWindow 8 | { 9 | public class NameComparer : IComparer 10 | { 11 | public int Compare( Object x, Object y ) 12 | { 13 | return EditorUtility.NaturalCompare( x.name, y.name ); 14 | } 15 | } 16 | 17 | public class TypeComparer : IComparer 18 | { 19 | public int Compare( Object x, Object y ) 20 | { 21 | if( x == y ) 22 | return 0; 23 | 24 | System.Type type1 = x.GetType(); 25 | System.Type type2 = y.GetType(); 26 | 27 | Texture2D preview1 = AssetPreview.GetMiniTypeThumbnail( type1 ); 28 | Texture2D preview2 = AssetPreview.GetMiniTypeThumbnail( type2 ); 29 | 30 | // 1. Compare Type thumbnails 31 | int result; 32 | if( preview1 != null && preview2 != null ) 33 | { 34 | result = preview1.GetEntityId().CompareTo(preview2.GetEntityId()); 35 | if( result != 0 ) 36 | return result; 37 | } 38 | 39 | preview1 = AssetPreview.GetMiniThumbnail( x ); 40 | preview2 = AssetPreview.GetMiniThumbnail( y ); 41 | 42 | // 2. Compare object thumbnails 43 | if( preview1 != null && preview2 != null ) 44 | { 45 | result = preview1.GetEntityId().CompareTo(preview2.GetEntityId()); 46 | if( result != 0 ) 47 | return result; 48 | } 49 | 50 | // 3. Compare Type names 51 | result = type1.Name.CompareTo( type2.Name ); 52 | if( result != 0 ) 53 | return result; 54 | 55 | // 4. Compare object names 56 | return EditorUtility.NaturalCompare( x.name, y.name ); 57 | } 58 | } 59 | 60 | public enum SortType { None = 0, Name = 1, Type = 2 }; 61 | 62 | private static readonly Color activeButtonColorLightSkin = new Color32( 245, 170, 10, 255 ); 63 | private static readonly Color activeButtonColorDarkSkin = new Color32( 100, 65, 0, 255 ); 64 | private static readonly Color nonFavoriteObjectIconColor = new Color32( 32, 32, 32, 255 ); 65 | 66 | private GUIStyle buttonStyle; 67 | 68 | private List objects; 69 | private Object mainObject; 70 | internal HashSet favoriteObjects; 71 | 72 | private SortType sortType; 73 | 74 | private GUIContent addToFavoritesIcon, removeFromFavoritesIcon; 75 | private Vector2 scrollPosition; 76 | 77 | public delegate bool ObjectClickedDelegate( Object obj ); 78 | private ObjectClickedDelegate onObjectLeftClicked; 79 | private ObjectClickedDelegate onObjectRightClicked; 80 | private ObjectClickedDelegate onObjectRemoved; 81 | 82 | public delegate void FavoriteStateChangedDelegate( Object obj, bool isFavorite ); 83 | private FavoriteStateChangedDelegate onObjectFavoriteStateChanged; 84 | 85 | public delegate void WindowClosedDelegate( SortType sortType ); 86 | private WindowClosedDelegate onWindowClosed; 87 | 88 | public void Initialize( List objects, HashSet favoriteObjects, Object mainObject, SortType sortType, ObjectClickedDelegate onObjectLeftClicked, ObjectClickedDelegate onObjectRightClicked, ObjectClickedDelegate onObjectRemoved, FavoriteStateChangedDelegate onObjectFavoriteStateChanged, WindowClosedDelegate onWindowClosed ) 89 | { 90 | this.objects = objects; 91 | this.favoriteObjects = favoriteObjects; 92 | this.mainObject = mainObject; 93 | this.sortType = sortType; 94 | this.onObjectLeftClicked = onObjectLeftClicked; 95 | this.onObjectRightClicked = onObjectRightClicked; 96 | this.onObjectRemoved = onObjectRemoved; 97 | this.onObjectFavoriteStateChanged = onObjectFavoriteStateChanged; 98 | this.onWindowClosed = onWindowClosed; 99 | 100 | addToFavoritesIcon = new GUIContent( EditorGUIUtility.IconContent( "Favorite Icon" ).image, "Add to Favorites" ); 101 | removeFromFavoritesIcon = new GUIContent( addToFavoritesIcon.image, "Remove from Favorites" ); 102 | 103 | SortObjects(); 104 | } 105 | 106 | private void OnDestroy() 107 | { 108 | try 109 | { 110 | if( onWindowClosed != null ) 111 | onWindowClosed( sortType ); 112 | } 113 | finally 114 | { 115 | objects = null; 116 | favoriteObjects = null; 117 | mainObject = null; 118 | buttonStyle = null; 119 | addToFavoritesIcon = null; 120 | removeFromFavoritesIcon = null; 121 | onObjectLeftClicked = null; 122 | onObjectRightClicked = null; 123 | onObjectRemoved = null; 124 | onObjectFavoriteStateChanged = null; 125 | onWindowClosed = null; 126 | } 127 | } 128 | 129 | private void OnGUI() 130 | { 131 | if( objects == null ) 132 | return; 133 | 134 | if( buttonStyle == null ) 135 | buttonStyle = new GUIStyle( EditorStyles.label ) { padding = new RectOffset( 0, 0, 0, 0 ) }; 136 | 137 | Rect rect = new Rect( Vector2.zero, position.size ); 138 | 139 | // Draw borders around the window 140 | if( Event.current.type == EventType.Repaint ) 141 | EditorStyles.helpBox.Draw( rect, false, false, false, false ); 142 | 143 | rect.height -= EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; 144 | scrollPosition = GUI.BeginScrollView( rect, scrollPosition, new Rect( 0f, 0f, rect.width, objects.Count * ( EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing ) + 5f ), false, false, GUIStyle.none, GUI.skin.verticalScrollbar ); 145 | 146 | Rect favoritesIconRect = new Rect( EditorGUIUtility.standardVerticalSpacing, EditorGUIUtility.standardVerticalSpacing, EditorGUIUtility.singleLineHeight, EditorGUIUtility.singleLineHeight ); 147 | Rect buttonRect = new Rect( favoritesIconRect.x + favoritesIconRect.width + 4f, favoritesIconRect.y, rect.width - 2f * EditorGUIUtility.standardVerticalSpacing - favoritesIconRect.width - 4f, favoritesIconRect.height ); 148 | 149 | Color contentColor = GUI.contentColor; 150 | 151 | for( int i = 0; i < objects.Count; i++ ) 152 | { 153 | if( ReferenceEquals( objects[i], null ) ) 154 | { 155 | objects.RemoveAt( i ); 156 | GUIUtility.ExitGUI(); 157 | } 158 | 159 | if( objects[i] == mainObject ) 160 | { 161 | Rect backgroundRect = buttonRect; 162 | backgroundRect.y -= EditorGUIUtility.standardVerticalSpacing * 0.5f; 163 | backgroundRect.height += EditorGUIUtility.standardVerticalSpacing; 164 | 165 | EditorGUI.DrawRect( backgroundRect, EditorGUIUtility.isProSkin ? activeButtonColorDarkSkin : activeButtonColorLightSkin ); 166 | } 167 | 168 | bool isObjectFavorite = favoriteObjects.Contains( objects[i] ); 169 | if( isObjectFavorite ) 170 | GUI.contentColor = Color.white; 171 | else 172 | GUI.contentColor = nonFavoriteObjectIconColor; 173 | 174 | if( GUI.Button( favoritesIconRect, isObjectFavorite ? removeFromFavoritesIcon : addToFavoritesIcon, buttonStyle ) ) 175 | { 176 | if( isObjectFavorite ) 177 | favoriteObjects.Remove( objects[i] ); 178 | else 179 | favoriteObjects.Add( objects[i] ); 180 | 181 | if( onObjectFavoriteStateChanged != null ) 182 | onObjectFavoriteStateChanged( objects[i], !isObjectFavorite ); 183 | } 184 | 185 | GUI.contentColor = contentColor; 186 | 187 | if( GUI.Button( buttonRect, EditorGUIUtility.ObjectContent( objects[i], objects[i].GetType() ), buttonStyle ) ) 188 | { 189 | if( Event.current.button == 0 ) 190 | { 191 | if( onObjectLeftClicked == null || onObjectLeftClicked( objects[i] ) ) 192 | { 193 | Close(); 194 | GUIUtility.ExitGUI(); 195 | } 196 | } 197 | else if( Event.current.button == 1 ) 198 | { 199 | if( onObjectRightClicked != null ) 200 | onObjectRightClicked( objects[i] ); 201 | } 202 | else if( Event.current.button == 2 ) 203 | { 204 | if( onObjectRemoved != null && onObjectRemoved( objects[i] ) ) 205 | { 206 | RemoveObjectFromList( objects[i] ); 207 | GUIUtility.ExitGUI(); 208 | } 209 | } 210 | } 211 | 212 | favoritesIconRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; 213 | buttonRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; 214 | } 215 | 216 | GUI.EndScrollView(); 217 | 218 | rect.y += rect.height; 219 | rect.height = EditorGUIUtility.singleLineHeight; 220 | 221 | rect.x += EditorGUIUtility.standardVerticalSpacing; 222 | rect.width -= 2f * EditorGUIUtility.standardVerticalSpacing; 223 | 224 | float originalLabelWidth = EditorGUIUtility.labelWidth; 225 | EditorGUIUtility.labelWidth = 75f; 226 | 227 | EditorGUI.BeginChangeCheck(); 228 | 229 | sortType = (SortType) EditorGUI.EnumPopup( rect, "Sort by:", sortType ); 230 | if( EditorGUI.EndChangeCheck() ) 231 | SortObjects(); 232 | 233 | EditorGUIUtility.labelWidth = originalLabelWidth; 234 | } 235 | 236 | public void RemoveObjectFromList( Object obj ) 237 | { 238 | if( obj != null && objects != null && objects.Remove( obj ) ) 239 | { 240 | if( objects.Count == 0 ) 241 | Close(); 242 | else 243 | { 244 | Vector2 size = position.size; 245 | size.y -= EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; 246 | minSize = size; 247 | maxSize = size; 248 | } 249 | } 250 | } 251 | 252 | private void SortObjects() 253 | { 254 | if( objects == null ) 255 | return; 256 | 257 | if( sortType == SortType.Name ) 258 | objects.Sort( new NameComparer() ); 259 | else if( sortType == SortType.Type ) 260 | objects.Sort( new TypeComparer() ); 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/PasteBinContextWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor; 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | 6 | namespace InspectPlusNamespace 7 | { 8 | public class PasteBinContextWindow : EditorWindow 9 | { 10 | public enum PasteType { Normal, ComponentAsNew, CompleteGameObject, ComponentGroup, AssetFiles }; 11 | 12 | internal const string SMART_PASTE_TOOLTIP = "Imagine objects A and B having children named C. When Smart Paste is enabled and A is pasted to B, if A.someVariable points to A.C, B.someVariable will point to B.C instead of A.C"; 13 | 14 | private readonly GUIContent smartPasteOnButtonLabel = new GUIContent( "Smart Paste ON", SMART_PASTE_TOOLTIP ); 15 | private readonly GUIContent smartPasteOffButtonLabel = new GUIContent( "Smart Paste OFF", SMART_PASTE_TOOLTIP ); 16 | 17 | private readonly List clipboard = new List( 4 ); 18 | private readonly List clipboardValues = new List( 4 ); 19 | 20 | private SerializedProperty targetProperty; 21 | private Object[] targetObjects; 22 | 23 | private PasteType pasteType; 24 | 25 | private GUIStyle backgroundStyle; 26 | private bool shouldRepositionSelf = true; 27 | private bool shouldResizeSelf = false; 28 | private bool shouldShowSmartPasteButton = false; 29 | 30 | private int hoveredClipboardIndex = -1; 31 | private Vector2? prevMousePos; 32 | 33 | private Vector2 scrollPosition; 34 | 35 | private float PreferredWidth 36 | { 37 | get 38 | { 39 | if( clipboard.Count == 0 ) 40 | return 250f; 41 | 42 | float width = 100f; 43 | for( int i = 0; i < clipboard.Count; i++ ) 44 | { 45 | float _width = EditorStyles.boldLabel.CalcSize( new GUIContent( clipboard[i].Label ) ).x + 50f; 46 | if( _width > width ) 47 | width = _width; 48 | } 49 | 50 | // When width is smaller than ~250, horizontal scrollbar will show up 51 | return Mathf.Max( width, 300f ); 52 | } 53 | } 54 | 55 | public void Initialize( SerializedProperty property ) 56 | { 57 | targetProperty = property; 58 | targetObjects = null; 59 | pasteType = PasteType.Normal; 60 | 61 | Object context = property.serializedObject.targetObject; 62 | List clipboardRaw = PasteBinWindow.GetSerializedClipboards(); 63 | for( int i = 0; i < clipboardRaw.Count; i++ ) 64 | { 65 | object value = clipboardRaw[i].RootValue.GetClipboardObject( context ); 66 | if( targetProperty.CanPasteValue( value, false ) ) 67 | { 68 | clipboard.Add( clipboardRaw[i] ); 69 | clipboardValues.Add( value ); 70 | 71 | if( !shouldShowSmartPasteButton ) 72 | { 73 | switch( clipboardRaw[i].RootType ) 74 | { 75 | case SerializedClipboard.IPObjectType.Array: 76 | case SerializedClipboard.IPObjectType.AssetReference: 77 | case SerializedClipboard.IPObjectType.GenericObject: 78 | case SerializedClipboard.IPObjectType.ManagedReference: 79 | case SerializedClipboard.IPObjectType.SceneObjectReference: 80 | shouldShowSmartPasteButton = true; 81 | break; 82 | } 83 | } 84 | } 85 | } 86 | 87 | OnInitialize(); 88 | } 89 | 90 | public void Initialize( Object[] objects, PasteType pasteType ) 91 | { 92 | targetProperty = null; 93 | targetObjects = objects; 94 | this.pasteType = pasteType; 95 | 96 | List clipboardRaw = PasteBinWindow.GetSerializedClipboards(); 97 | for( int i = 0; i < clipboardRaw.Count; i++ ) 98 | { 99 | switch( pasteType ) 100 | { 101 | case PasteType.Normal: 102 | if( !clipboardRaw[i].CanPasteToObject( objects[0] ) ) 103 | continue; 104 | 105 | shouldShowSmartPasteButton = true; break; 106 | case PasteType.ComponentAsNew: 107 | if( !clipboardRaw[i].CanPasteAsNewComponent( (Component) objects[0] ) ) 108 | continue; 109 | 110 | shouldShowSmartPasteButton = true; break; 111 | case PasteType.CompleteGameObject: if( !clipboardRaw[i].CanPasteCompleteGameObject( (GameObject) objects[0] ) ) continue; break; 112 | case PasteType.ComponentGroup: if( !clipboardRaw[i].CanPasteComponentGroup( (GameObject) objects[0] ) ) continue; break; 113 | case PasteType.AssetFiles: if( !clipboardRaw[i].CanPasteAssetFiles( objects ) ) continue; break; 114 | } 115 | 116 | clipboard.Add( clipboardRaw[i] ); 117 | clipboardValues.Add( clipboardRaw[i].RootValue.GetClipboardObject( null ) ); 118 | } 119 | 120 | OnInitialize(); 121 | } 122 | 123 | private void OnInitialize() 124 | { 125 | position = new Rect( new Vector2( -9999f, -9999f ), new Vector2( PreferredWidth, 9999f ) ); 126 | ShowPopup(); 127 | Focus(); 128 | } 129 | 130 | private void OnEnable() 131 | { 132 | wantsMouseMove = wantsMouseEnterLeaveWindow = true; 133 | wantsLessLayoutEvents = false; 134 | 135 | EditorApplication.update -= CheckWindowFocusRegularly; 136 | EditorApplication.update += CheckWindowFocusRegularly; 137 | } 138 | 139 | private void OnDisable() 140 | { 141 | EditorApplication.update -= CheckWindowFocusRegularly; 142 | PasteBinTooltip.Hide(); 143 | } 144 | 145 | private void CheckWindowFocusRegularly() 146 | { 147 | // Happens in rare cases 148 | if( !this ) 149 | EditorApplication.update -= CheckWindowFocusRegularly; 150 | else if( focusedWindow != this || EditorApplication.isCompiling ) 151 | Close(); 152 | } 153 | 154 | private void OnGUI() 155 | { 156 | if( backgroundStyle == null ) 157 | backgroundStyle = new GUIStyle( PasteBinTooltip.Style ) { margin = new RectOffset( 0, 0, 0, 0 ), padding = new RectOffset( 0, 0, 0, 0 ) }; 158 | 159 | Event ev = Event.current; 160 | 161 | Color backgroundColor = GUI.backgroundColor; 162 | GUI.backgroundColor = Color.Lerp( backgroundColor, new Color( 0.5f, 0.5f, 0.5f, 1f ), 0.325f ); 163 | 164 | GUILayout.BeginVertical( backgroundStyle ); 165 | 166 | GUI.backgroundColor = backgroundColor; 167 | 168 | scrollPosition = EditorGUILayout.BeginScrollView( scrollPosition ); 169 | 170 | GUILayout.BeginVertical(); 171 | 172 | if( !shouldShowSmartPasteButton ) 173 | GUILayout.Label( "Select value to paste:", EditorStyles.boldLabel ); 174 | else 175 | { 176 | GUILayout.BeginHorizontal(); 177 | GUILayout.Label( "Select value to paste:", EditorStyles.boldLabel ); 178 | 179 | EditorGUI.BeginChangeCheck(); 180 | InspectPlusSettings.Instance.SmartCopyPaste = GUILayout.Toggle( InspectPlusSettings.Instance.SmartCopyPaste, InspectPlusSettings.Instance.SmartCopyPaste ? smartPasteOnButtonLabel : smartPasteOffButtonLabel, GUI.skin.button ); 181 | if( EditorGUI.EndChangeCheck() ) 182 | { 183 | InspectPlusSettings.Instance.Save(); 184 | 185 | if( targetProperty != null ) 186 | { 187 | // Refresh values 188 | Object context = targetProperty.serializedObject.targetObject; 189 | for( int i = 0; i < clipboard.Count; i++ ) 190 | { 191 | SerializedClipboard.IPObjectType type = clipboard[i].RootType; 192 | if( type == SerializedClipboard.IPObjectType.AssetReference || type == SerializedClipboard.IPObjectType.SceneObjectReference ) 193 | clipboardValues[i] = clipboard[i].RootValue.GetClipboardObject( context ); 194 | } 195 | } 196 | } 197 | 198 | GUILayout.EndHorizontal(); 199 | } 200 | 201 | int hoveredClipboardIndex = -1; 202 | 203 | if( clipboard.Count == 0 ) 204 | GUILayout.Label( "Nothing to paste here..." ); 205 | else 206 | { 207 | // Traverse the list in reverse order so that the newest SerializedClipboards will be at the top of the list 208 | for( int i = clipboard.Count - 1; i >= 0; i-- ) 209 | { 210 | PasteBinWindow.DrawClipboardOnGUI( clipboard[i], clipboardValues[i], this.hoveredClipboardIndex == i, false ); 211 | 212 | if( hoveredClipboardIndex < 0 && ( ev.type == EventType.MouseDown || ev.type == EventType.MouseMove ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 213 | hoveredClipboardIndex = i; 214 | } 215 | } 216 | 217 | if( ev.type == EventType.MouseMove && this.hoveredClipboardIndex != hoveredClipboardIndex ) 218 | OnHoveredClipboardChanged( hoveredClipboardIndex ); 219 | 220 | if( ev.type == EventType.MouseDown && hoveredClipboardIndex >= 0 ) 221 | { 222 | int mouseButton = ev.button; 223 | ev.Use(); 224 | 225 | if( mouseButton == 0 ) 226 | { 227 | PasteClipboard( hoveredClipboardIndex ); 228 | GUIUtility.ExitGUI(); 229 | } 230 | else if( mouseButton == 1 ) 231 | { 232 | GenericMenu menu = new GenericMenu(); 233 | menu.AddItem( new GUIContent( "Paste" ), false, PasteClipboard, hoveredClipboardIndex ); 234 | menu.AddItem( new GUIContent( "Delete" ), false, RemoveClipboard, hoveredClipboardIndex ); 235 | menu.ShowAsContext(); 236 | 237 | GUIUtility.ExitGUI(); 238 | } 239 | else 240 | RemoveClipboard( hoveredClipboardIndex ); 241 | } 242 | 243 | GUILayout.EndVertical(); 244 | 245 | if( shouldRepositionSelf || shouldResizeSelf ) 246 | { 247 | float preferredHeight = GUILayoutUtility.GetLastRect().height; 248 | if( preferredHeight > 10f ) 249 | { 250 | Vector2 size = new Vector2( position.width, preferredHeight + 15f ); 251 | 252 | if( shouldRepositionSelf ) 253 | position = Utilities.GetScreenFittedRect(new Rect(GUIUtility.GUIToScreenPoint(ev.mousePosition) - size * 0.5f, size), this); 254 | else if( shouldResizeSelf ) 255 | position = new Rect( position.position, size ); 256 | 257 | shouldRepositionSelf = false; 258 | shouldResizeSelf = false; 259 | 260 | GUIUtility.ExitGUI(); 261 | } 262 | } 263 | 264 | EditorGUILayout.EndScrollView(); 265 | GUILayout.EndVertical(); 266 | 267 | // Make the window draggable 268 | if( ev.type == EventType.MouseDown ) 269 | prevMousePos = GUIUtility.GUIToScreenPoint( ev.mousePosition ); 270 | else if( ev.type == EventType.MouseDrag && prevMousePos.HasValue ) 271 | { 272 | Vector2 mousePos = GUIUtility.GUIToScreenPoint( ev.mousePosition ); 273 | Rect _position = position; 274 | _position.position += mousePos - prevMousePos.Value; 275 | position = _position; 276 | 277 | prevMousePos = mousePos; 278 | ev.Use(); 279 | } 280 | else if( ev.type == EventType.MouseUp ) 281 | prevMousePos = null; 282 | else if( ev.type == EventType.MouseLeaveWindow ) 283 | PasteBinTooltip.Hide(); 284 | } 285 | 286 | private void OnHoveredClipboardChanged( int hoveredClipboardIndex ) 287 | { 288 | this.hoveredClipboardIndex = hoveredClipboardIndex; 289 | if( hoveredClipboardIndex < 0 || !clipboard[hoveredClipboardIndex].HasTooltip ) 290 | PasteBinTooltip.Hide(); 291 | else 292 | PasteBinTooltip.Show( position, clipboard[hoveredClipboardIndex].LabelContent.tooltip ); 293 | 294 | Repaint(); 295 | } 296 | 297 | private void PasteClipboard( object obj ) 298 | { 299 | int index = (int) obj; 300 | 301 | if( targetProperty != null ) 302 | targetProperty.PasteValue( clipboard[index] ); 303 | else if( targetObjects != null ) 304 | { 305 | if( pasteType == PasteType.AssetFiles ) 306 | clipboard[index].PasteAssetFiles( targetObjects ); 307 | else if( pasteType == PasteType.ComponentGroup ) 308 | CreateInstance().Initialize( (SerializedClipboard.IPComponentGroup) clipboard[index].RootValue, targetObjects ); 309 | else 310 | { 311 | for( int j = 0; j < targetObjects.Length; j++ ) 312 | { 313 | switch( pasteType ) 314 | { 315 | case PasteType.Normal: clipboard[index].PasteToObject( targetObjects[j] ); break; 316 | case PasteType.ComponentAsNew: clipboard[index].PasteAsNewComponent( (Component) targetObjects[j] ); break; 317 | case PasteType.CompleteGameObject: clipboard[index].PasteCompleteGameObject( (GameObject) targetObjects[j], Event.current == null || ( !Event.current.control && !Event.current.command && !Event.current.shift ) ); break; // Don't preserve objects' world space positions if CTRL or Shift key are held 318 | } 319 | } 320 | } 321 | } 322 | else 323 | Debug.LogError( "Both the SerializedProperty and the target Objects are null!" ); 324 | 325 | Close(); 326 | } 327 | 328 | private void RemoveClipboard( object obj ) 329 | { 330 | int index = (int) obj; 331 | if( index >= clipboard.Count ) 332 | return; 333 | 334 | PasteBinWindow.RemoveClipboard( clipboard[index] ); 335 | 336 | clipboard.RemoveAt( index ); 337 | clipboardValues.RemoveAt( index ); 338 | 339 | hoveredClipboardIndex = -1; 340 | PasteBinTooltip.Hide(); 341 | 342 | shouldResizeSelf = true; 343 | Repaint(); 344 | } 345 | } 346 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/ComponentGroupCopyPasteWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor; 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | using SerializedComponent = InspectPlusNamespace.SerializedClipboard.IPComponentGroup; 6 | using ComponentGroupClipboard = InspectPlusNamespace.SerializablePropertyExtensions.ComponentGroupClipboard; 7 | 8 | namespace InspectPlusNamespace 9 | { 10 | // This class is mostly a stripped version of PasteBinContextWindow 11 | public class ComponentGroupCopyPasteWindow : EditorWindow 12 | { 13 | private readonly GUIContent smartPasteOnButtonLabel = new GUIContent( "Smart Paste ON", PasteBinContextWindow.SMART_PASTE_TOOLTIP ); 14 | private readonly GUIContent smartPasteOffButtonLabel = new GUIContent( "Smart Paste OFF", PasteBinContextWindow.SMART_PASTE_TOOLTIP ); 15 | 16 | private Component[] targetComponents; 17 | 18 | private SerializedComponent targetSerializedComponentGroup; 19 | private SerializedComponent.ComponentInfo[] targetSerializedComponents; 20 | private Object[] targetGameObjectsToPasteTo; 21 | private bool[] componentSelectedStates; 22 | 23 | private GUIStyle backgroundStyle; 24 | private bool shouldRepositionSelf = true; 25 | 26 | private int hoveredComponentIndex = -1; 27 | private Vector2? prevMousePos; 28 | 29 | private readonly GUIContent componentGUIContent = new GUIContent(); 30 | private readonly GUIContent cutGUIContent = new GUIContent( "Cut", "Selected components will be copied and then destroyed" ); 31 | 32 | private Vector2 scrollPosition; 33 | 34 | private float PreferredWidth 35 | { 36 | get 37 | { 38 | if( targetComponents != null ) 39 | return 400f; 40 | 41 | if( targetSerializedComponents == null || targetSerializedComponents.Length == 0 ) 42 | return 250f; 43 | 44 | float width = 100f; 45 | for( int i = 0; i < targetSerializedComponents.Length; i++ ) 46 | { 47 | float _width = EditorStyles.boldLabel.CalcSize( new GUIContent( targetSerializedComponents[i].Component.Label ) ).x + 50f; 48 | if( _width > width ) 49 | width = _width; 50 | } 51 | 52 | // When width is smaller than ~350, horizontal scrollbar will show up 53 | return Mathf.Max( width, 350f ); 54 | } 55 | } 56 | 57 | public void Initialize( Component[] components ) 58 | { 59 | targetComponents = components; 60 | targetSerializedComponentGroup = null; 61 | targetSerializedComponents = null; 62 | targetGameObjectsToPasteTo = null; 63 | componentSelectedStates = new bool[targetComponents.Length]; 64 | 65 | OnInitialize(); 66 | } 67 | 68 | public void Initialize( SerializedComponent serializedComponentGroup, Object[] pasteTargets ) 69 | { 70 | targetComponents = null; 71 | targetSerializedComponentGroup = serializedComponentGroup; 72 | targetSerializedComponents = SerializedComponent.SelectComponentsThatExistInProject( serializedComponentGroup.Components ).ToArray(); 73 | targetGameObjectsToPasteTo = pasteTargets; 74 | componentSelectedStates = new bool[targetSerializedComponents.Length]; 75 | 76 | OnInitialize(); 77 | } 78 | 79 | private void OnInitialize() 80 | { 81 | for( int i = 0; i < componentSelectedStates.Length; i++ ) 82 | componentSelectedStates[i] = true; 83 | 84 | position = new Rect( new Vector2( -9999f, -9999f ), new Vector2( PreferredWidth, 9999f ) ); 85 | ShowPopup(); 86 | Focus(); 87 | } 88 | 89 | private void OnEnable() 90 | { 91 | wantsMouseMove = wantsMouseEnterLeaveWindow = true; 92 | wantsLessLayoutEvents = false; 93 | 94 | EditorApplication.update -= CheckWindowFocusRegularly; 95 | EditorApplication.update += CheckWindowFocusRegularly; 96 | } 97 | 98 | private void OnDisable() 99 | { 100 | EditorApplication.update -= CheckWindowFocusRegularly; 101 | PasteBinTooltip.Hide(); 102 | } 103 | 104 | private void CheckWindowFocusRegularly() 105 | { 106 | // Happens in rare cases 107 | if( !this ) 108 | EditorApplication.update -= CheckWindowFocusRegularly; 109 | else if( focusedWindow != this || EditorApplication.isCompiling ) 110 | Close(); 111 | } 112 | 113 | private void OnGUI() 114 | { 115 | if( backgroundStyle == null ) 116 | backgroundStyle = new GUIStyle( PasteBinTooltip.Style ) { margin = new RectOffset( 0, 0, 0, 0 ), padding = new RectOffset( 0, 0, 0, 0 ) }; 117 | 118 | Event ev = Event.current; 119 | 120 | Color backgroundColor = GUI.backgroundColor; 121 | GUI.backgroundColor = Color.Lerp( backgroundColor, new Color( 0.5f, 0.5f, 0.5f, 1f ), 0.325f ); 122 | 123 | GUILayout.BeginVertical( backgroundStyle ); 124 | 125 | GUI.backgroundColor = backgroundColor; 126 | 127 | scrollPosition = EditorGUILayout.BeginScrollView( scrollPosition ); 128 | 129 | GUILayout.BeginVertical(); 130 | 131 | if( targetComponents != null ) 132 | GUILayout.Label( "Select components to copy:", EditorStyles.boldLabel ); 133 | else 134 | { 135 | GUILayout.BeginHorizontal(); 136 | GUILayout.Label( "Select components to paste:", EditorStyles.boldLabel ); 137 | 138 | if( targetSerializedComponents != null && targetSerializedComponents.Length > 0 ) 139 | { 140 | EditorGUI.BeginChangeCheck(); 141 | InspectPlusSettings.Instance.SmartCopyPaste = GUILayout.Toggle( InspectPlusSettings.Instance.SmartCopyPaste, InspectPlusSettings.Instance.SmartCopyPaste ? smartPasteOnButtonLabel : smartPasteOffButtonLabel, GUI.skin.button ); 142 | if( EditorGUI.EndChangeCheck() ) 143 | InspectPlusSettings.Instance.Save(); 144 | } 145 | 146 | GUILayout.EndHorizontal(); 147 | } 148 | 149 | if( componentSelectedStates != null && componentSelectedStates.Length > 0 ) 150 | { 151 | bool allComponentsSelected = componentSelectedStates[0]; 152 | for( int i = 1; i < componentSelectedStates.Length; i++ ) 153 | { 154 | if( componentSelectedStates[i] != allComponentsSelected ) 155 | { 156 | allComponentsSelected = true; 157 | EditorGUI.showMixedValue = true; 158 | 159 | break; 160 | } 161 | } 162 | 163 | EditorGUI.BeginChangeCheck(); 164 | allComponentsSelected = EditorGUILayout.ToggleLeft( "All", allComponentsSelected, EditorStyles.boldLabel ); 165 | if( EditorGUI.EndChangeCheck() ) 166 | { 167 | for( int i = 0; i < componentSelectedStates.Length; i++ ) 168 | componentSelectedStates[i] = allComponentsSelected; 169 | } 170 | 171 | EditorGUI.showMixedValue = false; 172 | } 173 | 174 | if( targetComponents != null ) 175 | { 176 | for( int i = 0; i < targetComponents.Length; i++ ) 177 | { 178 | componentGUIContent.text = ObjectNames.GetInspectorTitle( targetComponents[i] ); 179 | componentGUIContent.image = AssetPreview.GetMiniThumbnail( targetComponents[i] ); 180 | if( !componentGUIContent.image ) 181 | componentGUIContent.image = EditorGUIUtility.IconContent( "cs Script Icon" ).image; 182 | 183 | componentSelectedStates[i] = EditorGUILayout.ToggleLeft( componentGUIContent, componentSelectedStates[i] ); 184 | } 185 | 186 | EditorGUILayout.Space(); 187 | 188 | GUI.enabled = System.Array.IndexOf( componentSelectedStates, true ) >= 0; 189 | 190 | if( GUILayout.Button( "Copy", GUILayout.Height( EditorGUIUtility.singleLineHeight * 1.5f ) ) ) 191 | CopySelectedComponents( false ); 192 | 193 | GUI.backgroundColor = Color.yellow; 194 | if( GUILayout.Button( cutGUIContent ) ) 195 | CopySelectedComponents( true ); 196 | GUI.backgroundColor = backgroundColor; 197 | 198 | GUI.enabled = true; 199 | } 200 | else 201 | { 202 | if( targetSerializedComponents == null || targetSerializedComponents.Length == 0 ) 203 | GUILayout.Label( "Nothing to paste here..." ); 204 | else 205 | { 206 | int hoveredComponentIndex = -1; 207 | 208 | for( int i = 0; i < targetSerializedComponents.Length; i++ ) 209 | { 210 | componentGUIContent.text = targetSerializedComponents[i].Component.RootUnityObjectType.Name; 211 | componentGUIContent.image = AssetPreview.GetMiniTypeThumbnail( targetSerializedComponents[i].Component.RootUnityObjectType.Type ); 212 | if( !componentGUIContent.image ) 213 | componentGUIContent.image = EditorGUIUtility.IconContent( "cs Script Icon" ).image; 214 | 215 | componentSelectedStates[i] = EditorGUILayout.ToggleLeft( componentGUIContent, componentSelectedStates[i] ); 216 | 217 | if( hoveredComponentIndex < 0 && ( ev.type == EventType.MouseDown || ev.type == EventType.MouseMove ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 218 | hoveredComponentIndex = i; 219 | } 220 | 221 | if( ev.type == EventType.MouseMove && this.hoveredComponentIndex != hoveredComponentIndex ) 222 | OnHoveredComponentChanged( hoveredComponentIndex ); 223 | } 224 | 225 | EditorGUILayout.Space(); 226 | 227 | GUI.enabled = System.Array.IndexOf( componentSelectedStates, true ) >= 0; 228 | 229 | if( GUILayout.Button( "Paste", GUILayout.Height( EditorGUIUtility.singleLineHeight * 1.5f ) ) ) 230 | PasteSelectedComponents(); 231 | 232 | GUI.enabled = true; 233 | } 234 | 235 | GUILayout.EndVertical(); 236 | 237 | if( shouldRepositionSelf ) 238 | { 239 | float preferredHeight = GUILayoutUtility.GetLastRect().height; 240 | if( preferredHeight > 10f ) 241 | { 242 | Vector2 size = new Vector2( position.width, preferredHeight + 15f ); 243 | position = Utilities.GetScreenFittedRect(new Rect(GUIUtility.GUIToScreenPoint(ev.mousePosition) - size * 0.5f, size), this); 244 | 245 | shouldRepositionSelf = false; 246 | GUIUtility.ExitGUI(); 247 | } 248 | } 249 | 250 | EditorGUILayout.EndScrollView(); 251 | GUILayout.EndVertical(); 252 | 253 | // Make the window draggable 254 | if( ev.type == EventType.MouseDown ) 255 | prevMousePos = GUIUtility.GUIToScreenPoint( ev.mousePosition ); 256 | else if( ev.type == EventType.MouseDrag && prevMousePos.HasValue ) 257 | { 258 | Vector2 mousePos = GUIUtility.GUIToScreenPoint( ev.mousePosition ); 259 | Rect _position = position; 260 | _position.position += mousePos - prevMousePos.Value; 261 | position = _position; 262 | 263 | prevMousePos = mousePos; 264 | ev.Use(); 265 | } 266 | else if( ev.type == EventType.MouseUp ) 267 | prevMousePos = null; 268 | else if( ev.type == EventType.MouseLeaveWindow ) 269 | PasteBinTooltip.Hide(); 270 | } 271 | 272 | private void OnHoveredComponentChanged( int hoveredComponentIndex ) 273 | { 274 | this.hoveredComponentIndex = hoveredComponentIndex; 275 | if( hoveredComponentIndex < 0 || !targetSerializedComponents[hoveredComponentIndex].Component.HasTooltip ) 276 | PasteBinTooltip.Hide(); 277 | else 278 | PasteBinTooltip.Show( position, targetSerializedComponents[hoveredComponentIndex].Component.LabelContent.tooltip ); 279 | 280 | Repaint(); 281 | } 282 | 283 | private void CopySelectedComponents( bool destroyComponentsAfterwards ) 284 | { 285 | List selectedComponents = new List( targetComponents.Length ); 286 | for( int i = 0; i < targetComponents.Length; i++ ) 287 | { 288 | if( componentSelectedStates[i] ) 289 | selectedComponents.Add( targetComponents[i] ); 290 | } 291 | 292 | string label = Utilities.GetDetailedObjectName( selectedComponents[0] ); 293 | if( selectedComponents.Count > 1 ) 294 | label += " (and " + ( selectedComponents.Count - 1 ) + " more)"; 295 | 296 | PasteBinWindow.AddToClipboard( new ComponentGroupClipboard( selectedComponents.ToArray() ), selectedComponents[0].name, label + " (Multiple Components)", null ); 297 | 298 | if( destroyComponentsAfterwards ) 299 | { 300 | bool someComponentsAreDeleted; 301 | do 302 | { 303 | Component[] allComponents = selectedComponents[0].GetComponents(); 304 | someComponentsAreDeleted = false; 305 | 306 | for( int i = selectedComponents.Count - 1; i >= 0; i-- ) 307 | { 308 | if( !IsComponentRequiredByOthers( selectedComponents[i], allComponents ) ) 309 | { 310 | Undo.DestroyObjectImmediate( selectedComponents[i] ); 311 | selectedComponents.RemoveAt( i ); 312 | 313 | someComponentsAreDeleted = true; 314 | } 315 | } 316 | } while( someComponentsAreDeleted && selectedComponents.Count > 0 ); 317 | } 318 | 319 | Close(); 320 | } 321 | 322 | private void PasteSelectedComponents() 323 | { 324 | List selectedComponents = new List( targetSerializedComponents.Length ); 325 | for( int i = 0; i < targetSerializedComponents.Length; i++ ) 326 | { 327 | if( componentSelectedStates[i] ) 328 | selectedComponents.Add( targetSerializedComponents[i] ); 329 | } 330 | 331 | for( int i = 0; i < targetGameObjectsToPasteTo.Length; i++ ) 332 | targetSerializedComponentGroup.PasteComponents( (GameObject) targetGameObjectsToPasteTo[i], selectedComponents.ToArray() ); 333 | 334 | Close(); 335 | } 336 | 337 | private bool IsComponentRequiredByOthers( Component component, Component[] allComponents ) 338 | { 339 | if( component is Transform ) 340 | return true; 341 | 342 | System.Type componentType = component.GetType(); 343 | foreach( Component otherComponent in allComponents ) 344 | { 345 | if( otherComponent && otherComponent != component ) 346 | { 347 | foreach( RequireComponent requireComponent in otherComponent.GetType().GetCustomAttributes( typeof( RequireComponent ), true ) ) 348 | { 349 | if( requireComponent.m_Type0 == componentType || requireComponent.m_Type1 == componentType || requireComponent.m_Type2 == componentType ) 350 | return true; 351 | } 352 | } 353 | } 354 | 355 | return false; 356 | } 357 | } 358 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/Utilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Reflection; 6 | using System.Text; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using UnityEngine.SceneManagement; 10 | using Object = UnityEngine.Object; 11 | 12 | namespace InspectPlusNamespace 13 | { 14 | internal static class Utilities 15 | { 16 | private const BindingFlags VARIABLE_BINDING_FLAGS = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; 17 | 18 | private static readonly HashSet primitiveUnityTypes = new HashSet() 19 | { 20 | typeof( string ), typeof( Vector4 ), typeof( Vector3 ), typeof( Vector2 ), typeof( Rect ), 21 | typeof( Quaternion ), typeof( Color ), typeof( Color32 ), typeof( Bounds ), typeof( Matrix4x4 ), 22 | typeof( Vector3Int ), typeof( Vector2Int ), typeof( RectInt ), typeof( BoundsInt ) 23 | }; 24 | 25 | private static readonly Dictionary typeNamesLookup = new Dictionary 26 | { 27 | { typeof(bool), "bool" }, 28 | { typeof(byte), "byte" }, 29 | { typeof(char), "char" }, 30 | { typeof(decimal), "decimal" }, 31 | { typeof(double), "double" }, 32 | { typeof(float), "float" }, 33 | { typeof(int), "int" }, 34 | { typeof(long), "long" }, 35 | { typeof(object), "object" }, 36 | { typeof(sbyte), "sbyte" }, 37 | { typeof(short), "short" }, 38 | { typeof(string), "string" }, 39 | { typeof(uint), "uint" }, 40 | { typeof(ulong), "ulong" }, 41 | { typeof(void), "void" } 42 | }; 43 | 44 | private static readonly HashSet obsoleteComponentAccessors = new HashSet() 45 | { 46 | "rigidbody", "rigidbody2D", "camera", "light", "animation", "constantForce", "renderer", "audio", "guiText", 47 | "networkView", "guiElement", "guiTexture", "collider", "collider2D", "hingeJoint", "particleEmitter", "particleSystem" 48 | }; 49 | 50 | private static readonly List validVariables = new List( 32 ); 51 | private static readonly Dictionary typeToVariables = new Dictionary( 1024 ); 52 | private static readonly CompareInfo caseInsensitiveComparer = new CultureInfo( "en-US" ).CompareInfo; 53 | public static readonly StringBuilder stringBuilder = new StringBuilder( 256 ); 54 | 55 | private static MethodInfo screenFittedRectGetter; 56 | private static FieldInfo editorWindowHostViewGetter; 57 | private static PropertyInfo hostViewContainerWindowGetter; 58 | 59 | public static string GetDetailedObjectName( Object obj ) 60 | { 61 | if( !obj ) 62 | return ""; 63 | 64 | if( obj is GameObject ) 65 | { 66 | Scene scene = ( (GameObject) obj ).scene; 67 | return scene.IsValid() ? string.Concat( scene.name, "/", obj.name, ".GameObject" ) : ( obj.name + " Asset.GameObject" ); 68 | } 69 | else if( obj is Component ) 70 | { 71 | Scene scene = ( (Component) obj ).gameObject.scene; 72 | return scene.IsValid() ? string.Concat( scene.name, "/", obj.name, ".", obj.GetType().Name ) : string.Concat( obj.name, " Asset.", obj.GetType().Name ); 73 | } 74 | else if( obj is AssetImporter ) 75 | return string.Concat( Path.GetFileNameWithoutExtension( ( (AssetImporter) obj ).assetPath ), " (", obj.GetType().Name, " Asset)" ); 76 | else if( AssetDatabase.Contains( obj ) ) 77 | return string.Concat( obj.name, " (", obj.GetType().Name, " Asset)" ); 78 | else 79 | { 80 | string scenePath = AssetDatabase.GetAssetOrScenePath( obj ); 81 | if( !string.IsNullOrEmpty( scenePath ) ) 82 | return string.Concat( scenePath, "/", obj.name, " (", obj.GetType().Name, ")" ); 83 | else 84 | return string.Concat( obj.name, " (", obj.GetType().Name, ")" ); 85 | } 86 | } 87 | 88 | public static bool ContainsIgnoreCase( this string source, string value ) 89 | { 90 | return caseInsensitiveComparer.IndexOf( source, value, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace ) >= 0; 91 | } 92 | 93 | #if UNITY_6000_3_OR_NEWER 94 | public static EntityId GetEntityId(this Object obj) 95 | { 96 | return obj.GetEntityId(); 97 | } 98 | 99 | public static Object EntityIdToObject(EntityId entityId) 100 | { 101 | return EditorUtility.EntityIdToObject(entityId); 102 | } 103 | #else 104 | public static int GetEntityId(this Object obj) 105 | { 106 | return obj.GetInstanceID(); 107 | } 108 | 109 | public static Object EntityIdToObject(int instanceID) 110 | { 111 | return EditorUtility.InstanceIDToObject(instanceID); 112 | } 113 | #endif 114 | 115 | // Get filtered variables for an object 116 | public static VariableGetterHolder[] GetFilteredVariablesForObject( object obj ) 117 | { 118 | if( obj is TypeWrapper ) 119 | return GetFilteredVariablesForType( ( (TypeWrapper) obj ).Type, VARIABLE_BINDING_FLAGS & ~BindingFlags.Instance ); 120 | else 121 | return GetFilteredVariablesForType( obj.GetType() ); 122 | } 123 | 124 | // Get filtered variables for a type 125 | public static VariableGetterHolder[] GetFilteredVariablesForType( Type type ) 126 | { 127 | return GetFilteredVariablesForType( type, VARIABLE_BINDING_FLAGS ); 128 | } 129 | 130 | private static VariableGetterHolder[] GetFilteredVariablesForType( Type type, BindingFlags bindingFlags ) 131 | { 132 | VariableGetterHolder[] result; 133 | if( bindingFlags == VARIABLE_BINDING_FLAGS && typeToVariables.TryGetValue( type, out result ) ) 134 | return result; 135 | 136 | validVariables.Clear(); 137 | 138 | // Filter the variables 139 | Type currType = type; 140 | while( currType != null && currType != typeof( object ) ) /// of an interface is null, so null check is necessary 141 | { 142 | FieldInfo[] fields = currType.GetFields( bindingFlags ); 143 | for( int i = 0; i < fields.Length; i++ ) 144 | { 145 | FieldInfo field = fields[i]; 146 | Type variableType = field.FieldType; 147 | 148 | // Pointers and ref variables can throw ArgumentException 149 | if( variableType.IsPointer || variableType.IsByRef ) 150 | continue; 151 | 152 | VariableGetVal getter; 153 | VariableSetVal setter; 154 | field.CreateGetterAndSetter( out getter, out setter ); 155 | if( getter != null ) 156 | validVariables.Add( new VariableGetterHolder( field, getter, setter ) ); 157 | } 158 | 159 | currType = currType.BaseType; 160 | } 161 | 162 | validVariables.Sort(); 163 | int validVariablesPrevCount = validVariables.Count; 164 | 165 | currType = type; 166 | while( currType != null && currType != typeof( object ) ) 167 | { 168 | PropertyInfo[] properties = currType.GetProperties( bindingFlags ); 169 | for( int i = 0; i < properties.Length; i++ ) 170 | { 171 | PropertyInfo property = properties[i]; 172 | Type variableType = property.PropertyType; 173 | 174 | // Pointers and ref variables can throw ArgumentException 175 | if( variableType.IsPointer || variableType.IsByRef ) 176 | continue; 177 | 178 | // Skip properties without a getter function 179 | MethodInfo propertyGetter = property.GetGetMethod( true ); 180 | if( propertyGetter == null ) 181 | continue; 182 | 183 | // Skip indexer properties 184 | if( property.GetIndexParameters().Length > 0 ) 185 | continue; 186 | 187 | // No need to check properties with 'override' keyword 188 | if( propertyGetter.GetBaseDefinition().DeclaringType != propertyGetter.DeclaringType ) 189 | continue; 190 | 191 | // Additional filtering for properties: 192 | // Prevent accessing properties of Unity that instantiate an existing resource (causing memory leak) 193 | // Hide obsolete useless Component properties like "rigidbody", "camera", "collider" and so on 194 | string propertyName = property.Name; 195 | if( typeof( MeshFilter ).IsAssignableFrom( currType ) && propertyName == "mesh" ) 196 | continue; 197 | else if( ( propertyName == "material" || propertyName == "materials" ) && 198 | ( typeof( Renderer ).IsAssignableFrom( currType ) || typeof( Collider ).IsAssignableFrom( currType ) || typeof( Collider2D ).IsAssignableFrom( currType ) ) ) 199 | continue; 200 | else if( ( propertyName == "transform" || propertyName == "gameObject" ) && 201 | ( property.DeclaringType == typeof( Component ) || property.DeclaringType == typeof( GameObject ) ) ) 202 | continue; 203 | else if( ( typeof( Component ).IsAssignableFrom( currType ) || typeof( GameObject ).IsAssignableFrom( currType ) ) && 204 | Attribute.IsDefined( property, typeof( ObsoleteAttribute ) ) && obsoleteComponentAccessors.Contains( propertyName ) ) 205 | continue; 206 | else 207 | { 208 | VariableGetVal getter; 209 | VariableSetVal setter; 210 | property.CreateGetterAndSetter( out getter, out setter ); 211 | if( getter != null ) 212 | validVariables.Add( new VariableGetterHolder( property, getter, setter ) ); 213 | } 214 | } 215 | 216 | currType = currType.BaseType; 217 | } 218 | 219 | validVariables.Sort( validVariablesPrevCount, validVariables.Count - validVariablesPrevCount, null ); 220 | validVariablesPrevCount = validVariables.Count; 221 | 222 | currType = type; 223 | while( currType != null && currType != typeof( object ) ) 224 | { 225 | MethodInfo[] methods = currType.GetMethods( bindingFlags ); 226 | for( int i = 0; i < methods.Length; i++ ) 227 | { 228 | MethodInfo method = methods[i]; 229 | 230 | // Skip operator overloads or property accessors 231 | if( method.IsSpecialName ) 232 | continue; 233 | 234 | // Skip functions that take parameters or generic arguments (i.e. "GetComponent()") 235 | if( method.GetParameters().Length > 0 || method.GetGenericArguments().Length > 0 ) 236 | continue; 237 | 238 | Type returnType = method.ReturnType; 239 | 240 | // Skip functions that don't return anything 241 | if( returnType == typeof( void ) ) 242 | continue; 243 | 244 | // Pointers and ref variables can throw ArgumentException 245 | if( returnType.IsPointer || returnType.IsByRef ) 246 | continue; 247 | 248 | // No need to check methods with 'override' keyword 249 | if( method.GetBaseDefinition().DeclaringType != method.DeclaringType ) 250 | continue; 251 | 252 | VariableGetVal getter = method.CreateGetter(); 253 | if( getter != null ) 254 | validVariables.Add( new VariableGetterHolder( method, getter ) ); 255 | } 256 | 257 | currType = currType.BaseType; 258 | } 259 | 260 | validVariables.Sort( validVariablesPrevCount, validVariables.Count - validVariablesPrevCount, null ); 261 | 262 | result = validVariables.ToArray(); 263 | 264 | // Cache the filtered variables 265 | if( bindingFlags == VARIABLE_BINDING_FLAGS ) 266 | typeToVariables.Add( type, result ); 267 | 268 | return result; 269 | } 270 | 271 | // Check if the type is a common Unity type (let's call them primitives) 272 | public static bool IsPrimitiveUnityType( this Type type ) 273 | { 274 | return type.IsPrimitive || primitiveUnityTypes.Contains( type ) || type.IsEnum; 275 | } 276 | 277 | /// 278 | /// Converts to . 279 | /// 280 | /// Case insensitive. Can be , or . 281 | public static Type GetType( string typeName ) 282 | { 283 | try 284 | { 285 | /// Try first 286 | Type type = Type.GetType( typeName, false, true ); 287 | if( type != null ) 288 | return type; 289 | 290 | bool isFullNameProvided = typeName.IndexOf( '.' ) >= 0; 291 | if( !isFullNameProvided ) 292 | { 293 | // Try loading the type from UnityEngine namespace 294 | type = typeof( Transform ).Assembly.GetType( "UnityEngine." + typeName, false, true ); 295 | if( type != null ) 296 | return type; 297 | } 298 | 299 | // Search all assemblies for the type 300 | foreach( Assembly assembly in AppDomain.CurrentDomain.GetAssemblies() ) 301 | { 302 | try 303 | { 304 | foreach( Type t in assembly.GetTypes() ) 305 | { 306 | if( ( isFullNameProvided ? t.FullName : t.Name ).Equals( typeName, StringComparison.OrdinalIgnoreCase ) ) 307 | return t; 308 | } 309 | } 310 | catch { } 311 | } 312 | } 313 | catch { } 314 | 315 | // The type just couldn't be found... 316 | return null; 317 | } 318 | 319 | public static StringBuilder AppendType( this StringBuilder sb, Type type ) 320 | { 321 | bool isCompilerGeneratedType = false; 322 | if( !typeNamesLookup.TryGetValue( type, out string name ) ) 323 | { 324 | name = type.Name; 325 | 326 | if( name.StartsWith( '<' ) && type.DeclaringType is Type declaringType ) 327 | { 328 | isCompilerGeneratedType = true; 329 | type = declaringType; 330 | name = declaringType.Name; 331 | } 332 | } 333 | 334 | if( !type.IsGenericType ) 335 | sb.Append( name ); 336 | else 337 | { 338 | int excludeIndex = name.IndexOf( '`' ); 339 | if( excludeIndex > 0 ) 340 | sb.Append( name, 0, excludeIndex ); 341 | else 342 | sb.Append( name ); 343 | 344 | sb.Append( "<" ); 345 | 346 | Type[] arguments = type.GetGenericArguments(); 347 | for( int i = 0; i < arguments.Length; i++ ) 348 | { 349 | sb.AppendType( arguments[i] ); 350 | 351 | if( i < arguments.Length - 1 ) 352 | sb.Append( "," ); 353 | } 354 | 355 | sb.Append( ">" ); 356 | } 357 | 358 | if( isCompilerGeneratedType ) 359 | sb.Append( "<(CompilerGenerated)>" ); 360 | 361 | return sb; 362 | } 363 | 364 | // Get and functions for a field 365 | private static void CreateGetterAndSetter( this FieldInfo fieldInfo, out VariableGetVal getter, out VariableSetVal setter ) 366 | { 367 | getter = fieldInfo.GetValue; 368 | setter = ( !fieldInfo.IsInitOnly && !fieldInfo.IsLiteral ) ? fieldInfo.SetValue : (VariableSetVal) null; 369 | } 370 | 371 | // Get and functions for a property 372 | private static void CreateGetterAndSetter( this PropertyInfo propertyInfo, out VariableGetVal getter, out VariableSetVal setter ) 373 | { 374 | // Can't use PropertyWrapper (which uses CreateDelegate) for property getters of structs 375 | if( propertyInfo.DeclaringType.IsValueType ) 376 | { 377 | getter = propertyInfo.CanRead ? ( ( obj ) => propertyInfo.GetValue( obj, null ) ) : (VariableGetVal) null; 378 | setter = propertyInfo.CanWrite ? ( ( obj, value ) => propertyInfo.SetValue( obj, value, null ) ) : (VariableSetVal) null; 379 | } 380 | else 381 | { 382 | MethodInfo getMethod = propertyInfo.GetGetMethod( true ); 383 | MethodInfo setMethod = propertyInfo.GetSetMethod( true ); 384 | 385 | Type propertyWrapperType; 386 | if( !getMethod.IsStatic ) 387 | propertyWrapperType = typeof( PropertyWrapper<,> ).MakeGenericType( propertyInfo.DeclaringType, propertyInfo.PropertyType ); 388 | else 389 | propertyWrapperType = typeof( PropertyWrapper<> ).MakeGenericType( propertyInfo.PropertyType ); 390 | 391 | IPropertyWrapper propertyWrapper = (IPropertyWrapper) Activator.CreateInstance( propertyWrapperType, getMethod, setMethod ); 392 | getter = propertyWrapper.GetValue; 393 | setter = propertyWrapper.SetValue; 394 | } 395 | } 396 | 397 | // Get function for a method 398 | private static VariableGetVal CreateGetter( this MethodInfo methodInfo ) 399 | { 400 | // Can't use PropertyWrapper (which uses CreateDelegate) for methods of structs 401 | if( methodInfo.DeclaringType.IsValueType ) 402 | return ( obj ) => methodInfo.Invoke( obj, null ); 403 | 404 | if( !methodInfo.IsStatic ) 405 | { 406 | Type GenType = typeof( PropertyWrapper<,> ).MakeGenericType( methodInfo.DeclaringType, methodInfo.ReturnType ); 407 | return ( (IPropertyWrapper) Activator.CreateInstance( GenType, methodInfo, null ) ).GetValue; 408 | } 409 | else 410 | { 411 | Type GenType = typeof( PropertyWrapper<> ).MakeGenericType( methodInfo.ReturnType ); 412 | return ( (IPropertyWrapper) Activator.CreateInstance( GenType, methodInfo, null ) ).GetValue; 413 | } 414 | } 415 | 416 | /// 417 | /// Restricts the given Rect within the screen's bounds. 418 | /// 419 | public static Rect GetScreenFittedRect(Rect originalRect, EditorWindow editorWindow) 420 | { 421 | screenFittedRectGetter ??= typeof(EditorWindow).Assembly.GetType("UnityEditor.ContainerWindow").GetMethod("FitRectToScreen", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); 422 | 423 | if (screenFittedRectGetter.GetParameters().Length == 3) 424 | return (Rect)screenFittedRectGetter.Invoke(null, new object[] { originalRect, true, true }); 425 | else 426 | { 427 | // New version introduced in Unity 2022.3.62f1, Unity 6.0.49f1 and Unity 6.1.0f1. 428 | // Usage example: https://github.com/Unity-Technologies/UnityCsReference/blob/10f8718268a7e34844ba7d59792117c28d75a99b/Editor/Mono/EditorWindow.cs#L1264 429 | editorWindowHostViewGetter ??= typeof(EditorWindow).GetField("m_Parent", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 430 | hostViewContainerWindowGetter ??= typeof(EditorWindow).Assembly.GetType("UnityEditor.HostView").GetProperty("window", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 431 | 432 | return (Rect)screenFittedRectGetter.Invoke(null, new object[] { originalRect, originalRect.center, true, hostViewContainerWindowGetter.GetValue(editorWindowHostViewGetter.GetValue(editorWindow), null) }); 433 | } 434 | } 435 | 436 | // Converts full paths to relative paths so that they can be used with AssetDatabase 437 | public static void ConvertAbsolutePathsToRelativePaths( string[] absolutePaths ) 438 | { 439 | string projectPath = Path.GetFullPath( Directory.GetCurrentDirectory() ); 440 | string projectPath2 = projectPath.Replace( '\\', '/' ); 441 | 442 | int projectPathLength = projectPath2.Length; 443 | if( projectPath2[projectPath.Length - 1] != '/' ) 444 | projectPathLength++; 445 | 446 | for( int i = 0; i < absolutePaths.Length; i++ ) 447 | { 448 | if( absolutePaths[i].StartsWith( projectPath ) || absolutePaths[i].StartsWith( projectPath2 ) ) 449 | absolutePaths[i] = absolutePaths[i].Substring( projectPathLength ); 450 | } 451 | } 452 | } 453 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/DebugModeEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Text; 6 | using UnityEditor; 7 | using UnityEditorInternal; 8 | using UnityEngine; 9 | using Object = UnityEngine.Object; 10 | 11 | namespace InspectPlusNamespace 12 | { 13 | public class DebugModeEntry 14 | { 15 | [Serializable] 16 | public class SerializedData 17 | { 18 | public bool IsExpanded; 19 | public bool ForceShowNestedVariables; 20 | public string SearchTerm; 21 | 22 | [SerializeReference] 23 | public List Variables; 24 | [SerializeReference] 25 | public SerializedData EnumerableRoot; 26 | } 27 | 28 | protected bool m_isExpanded; 29 | public bool IsExpanded 30 | { 31 | get { return m_isExpanded; } 32 | set 33 | { 34 | if( m_isExpanded == value ) 35 | return; 36 | 37 | m_isExpanded = value; 38 | if( m_isExpanded ) 39 | Refresh(); 40 | else 41 | { 42 | Obj = null; 43 | forceShowNestedVariables = false; 44 | PoolLists(); 45 | RefreshLabel(); 46 | } 47 | } 48 | } 49 | 50 | public VariableGetterHolder Variable; 51 | public object Obj; 52 | protected DebugModeEntry parent; 53 | protected readonly GUIContent guiContent = new(); 54 | protected string primitiveValue; 55 | 56 | protected DebugModeEnumerableEntry enumerableRoot; 57 | protected List variables; 58 | private bool forceShowNestedVariables; 59 | 60 | private string searchTerm; 61 | 62 | private static readonly Stack> pool = new Stack>( 32 ); 63 | 64 | public DebugModeEntry( DebugModeEntry parent ) 65 | { 66 | this.parent = parent; 67 | } 68 | 69 | public void SetSearchTerm( string searchTerm ) 70 | { 71 | if( searchTerm == string.Empty ) 72 | searchTerm = null; 73 | 74 | if( this.searchTerm != searchTerm ) 75 | { 76 | this.searchTerm = searchTerm; 77 | RefreshLabel(); 78 | } 79 | } 80 | 81 | public void Refresh() 82 | { 83 | RefreshInternal(); 84 | RefreshLabel(); 85 | } 86 | 87 | protected virtual void RefreshInternal() 88 | { 89 | if( m_isExpanded ) 90 | { 91 | Type prevType = Obj != null ? Obj.GetType() : null; 92 | Obj = Variable.Get( parent != null ? parent.Obj : null ); 93 | 94 | if( Obj == null || Obj.Equals( null ) ) 95 | PoolLists(); 96 | else 97 | { 98 | if( Obj.GetType() != prevType ) 99 | PoolLists(); 100 | 101 | // Cache ToString() values of primitives since they won't change until next Refresh 102 | primitiveValue = null; 103 | if( Obj.GetType().IsPrimitiveUnityType() ) 104 | primitiveValue = Obj.ToString(); 105 | else if( Obj is Type runtimeType ) 106 | primitiveValue = Utilities.stringBuilder.Clear().AppendType( runtimeType ).ToString(); 107 | 108 | if( Obj is IEnumerable && !( Obj is Transform ) ) 109 | { 110 | if( enumerableRoot == null ) 111 | enumerableRoot = new DebugModeEnumerableEntry( this ) { Variable = new VariableGetterHolder( "(IEnumerable) Elements", Obj.GetType(), null, null ) }; 112 | 113 | enumerableRoot.Refresh(); 114 | } 115 | else if( enumerableRoot != null ) 116 | { 117 | enumerableRoot.PoolLists(); 118 | enumerableRoot = null; 119 | } 120 | 121 | if( !( Obj is ICollection ) ) // Display only the enumerable elements of ICollections 122 | { 123 | if( variables == null ) 124 | { 125 | VariableGetterHolder[] childGetters = Utilities.GetFilteredVariablesForObject( Obj ); 126 | variables = PopList( childGetters.Length ); 127 | for( int i = 0; i < childGetters.Length; i++ ) 128 | variables.Add( new DebugModeEntry( this ) { Variable = childGetters[i] } ); 129 | } 130 | 131 | for( int i = 0; i < variables.Count; i++ ) 132 | variables[i].Refresh(); 133 | } 134 | } 135 | } 136 | } 137 | 138 | private void RefreshLabel() 139 | { 140 | guiContent.text = Variable.description; 141 | 142 | /// If 's Type is different than , it's useful to show it in Inspector so that we can see exactly what we're inspecting. 143 | bool showObjType = Obj != null && Obj is not Object && Obj.GetType() != Variable.type; 144 | if( showObjType || !string.IsNullOrEmpty( searchTerm ) ) 145 | { 146 | StringBuilder sb = Utilities.stringBuilder.Clear().Append( Variable.description ); 147 | if( showObjType ) 148 | sb.Append( " -> " ).AppendType( Obj.GetType() ); 149 | 150 | if( !string.IsNullOrEmpty( searchTerm ) ) 151 | sb.Append( " [Search: " ).Append( searchTerm ).Append( ']' ); 152 | 153 | guiContent.text = sb.ToString(); 154 | } 155 | } 156 | 157 | public void DrawOnGUI( bool flattenChildren = false ) 158 | { 159 | if( flattenChildren ) 160 | { 161 | IsExpanded = true; 162 | searchTerm = parent?.searchTerm; 163 | } 164 | else 165 | { 166 | if( !string.IsNullOrEmpty( parent?.searchTerm ) && !guiContent.text.ContainsIgnoreCase( parent.searchTerm ) ) 167 | return; 168 | } 169 | 170 | if( m_isExpanded ) 171 | { 172 | if( !flattenChildren && !DrawFoldout() ) 173 | { 174 | IsExpanded = false; 175 | GUIUtility.ExitGUI(); 176 | } 177 | else 178 | { 179 | if( !flattenChildren ) 180 | EditorGUI.indentLevel++; 181 | 182 | if( parent == null || !DrawValueOnGUI() || forceShowNestedVariables ) 183 | { 184 | if( forceShowNestedVariables ) 185 | EditorGUI.indentLevel++; 186 | 187 | if( enumerableRoot != null ) 188 | enumerableRoot.DrawOnGUI( variables == null ); // If only the enumerable elements exist, flatten them 189 | 190 | if( variables != null ) 191 | { 192 | for( int i = 0; i < variables.Count; i++ ) 193 | variables[i].DrawOnGUI(); 194 | } 195 | 196 | if( forceShowNestedVariables ) 197 | EditorGUI.indentLevel--; 198 | } 199 | 200 | if( !flattenChildren ) 201 | EditorGUI.indentLevel--; 202 | } 203 | } 204 | else 205 | { 206 | if( DrawFoldout() ) 207 | { 208 | IsExpanded = true; 209 | GUIUtility.ExitGUI(); 210 | } 211 | } 212 | } 213 | 214 | private bool DrawFoldout() 215 | { 216 | Rect rect = GUILayoutUtility.GetRect( EditorGUIUtility.fieldWidth, EditorGUIUtility.fieldWidth, EditorGUIUtility.singleLineHeight, EditorGUIUtility.singleLineHeight, EditorStyles.foldout ); 217 | bool isExpanded = EditorGUI.Foldout( rect, m_isExpanded, guiContent, true, EditorStyles.foldout ); 218 | if( Event.current.type == EventType.MouseDown && Event.current.button == 1 && rect.Contains( Event.current.mousePosition ) ) 219 | { 220 | GenericMenu menu = new(); 221 | menu.AddItem( new GUIContent( "Search" ), false, () => StringInputDialog.Show( "Enter search term:", searchTerm, SetSearchTerm ) ); 222 | if( !string.IsNullOrEmpty( searchTerm ) ) 223 | menu.AddItem( new GUIContent( "Reset Search" ), false, () => SetSearchTerm( null ) ); 224 | 225 | menu.ShowAsContext(); 226 | } 227 | 228 | return isExpanded; 229 | } 230 | 231 | private bool DrawValueOnGUI() 232 | { 233 | EditorGUI.BeginChangeCheck(); 234 | 235 | object newValue = Obj; 236 | if( Obj == null || Obj.Equals( null ) ) 237 | { 238 | if( typeof( Object ).IsAssignableFrom( Variable.type ) ) 239 | newValue = EditorGUILayout.ObjectField( GUIContent.none, null, Variable.type, true ); 240 | else 241 | EditorGUILayout.LabelField( "Null" ); 242 | } 243 | else if( Obj is Object ) 244 | { 245 | Type objType = Obj.GetType(); 246 | if( typeof( Object ).IsAssignableFrom( Variable.type ) && Variable.type.IsAssignableFrom( objType ) ) 247 | objType = Variable.type; 248 | 249 | EditorGUI.EndChangeCheck(); /// Toggling shouldn't count as a change 250 | Rect pickerRect = EditorGUILayout.GetControlRect( false, EditorGUIUtility.singleLineHeight ); 251 | float indentation = EditorGUI.IndentedRect( pickerRect ).x - pickerRect.x; 252 | forceShowNestedVariables = EditorGUI.Foldout( new Rect( pickerRect.x, pickerRect.y, pickerRect.height + indentation, pickerRect.height ), forceShowNestedVariables, GUIContent.none, true ); 253 | 254 | EditorGUI.BeginChangeCheck(); 255 | pickerRect.xMin += pickerRect.height + 2f; 256 | newValue = EditorGUI.ObjectField( pickerRect, (Object) Obj, objType, true ); 257 | 258 | Event ev = Event.current; 259 | if( ev.type == EventType.MouseDown && ev.button == 1 && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 260 | { 261 | GenericMenu menu = new GenericMenu(); 262 | MenuItems.OnObjectRightClicked( menu, (Object) Obj ); 263 | menu.ShowAsContext(); 264 | 265 | GUIUtility.ExitGUI(); 266 | } 267 | } 268 | else if( Obj is bool ) 269 | newValue = EditorGUILayout.ToggleLeft( GUIContent.none, (bool) Obj ); 270 | else if( Obj is int ) 271 | newValue = EditorGUILayout.DelayedIntField( GUIContent.none, (int) Obj ); 272 | else if( Obj is float ) 273 | newValue = EditorGUILayout.DelayedFloatField( GUIContent.none, (float) Obj ); 274 | else if (Obj is string stringValue) 275 | { 276 | // If String contains multiple lines, use flexible TextArea to render it. Otherwise, use a single-line TextField to render it. This way, the input can be submitted with Enter for single-line strings. 277 | if (stringValue?.IndexOf('\n') >= 0) 278 | newValue = EditorGUILayout.TextArea(stringValue); 279 | else 280 | newValue = EditorGUILayout.DelayedTextField(GUIContent.none, stringValue); 281 | } 282 | else if( Obj is double ) 283 | newValue = EditorGUILayout.DelayedDoubleField( GUIContent.none, (double) Obj ); 284 | else if( Obj is long ) 285 | newValue = EditorGUILayout.LongField( GUIContent.none, (long) Obj ); 286 | else if (Obj is decimal _decimal) 287 | newValue = decimal.TryParse(EditorGUILayout.DelayedTextField(GUIContent.none, _decimal.ToString(CultureInfo.InvariantCulture)), NumberStyles.Float, CultureInfo.InvariantCulture, out decimal parsedDecimal) ? parsedDecimal : _decimal; 288 | else if( Obj is Vector3 ) 289 | newValue = EditorGUILayout.Vector3Field( GUIContent.none, (Vector3) Obj ); 290 | else if( Obj is Vector2 ) 291 | newValue = EditorGUILayout.Vector2Field( GUIContent.none, (Vector2) Obj ); 292 | else if( Obj is Vector3Int ) 293 | newValue = EditorGUILayout.Vector3IntField( GUIContent.none, (Vector3Int) Obj ); 294 | else if( Obj is Vector2Int ) 295 | newValue = EditorGUILayout.Vector2IntField( GUIContent.none, (Vector2Int) Obj ); 296 | else if( Obj is Vector4 ) 297 | newValue = EditorGUILayout.Vector4Field( GUIContent.none, (Vector4) Obj ); 298 | else if( Obj is Quaternion ) 299 | newValue = Quaternion.Euler( EditorGUILayout.Vector3Field( GUIContent.none, ( (Quaternion) Obj ).eulerAngles ) ); // Edit Quaternions as Euler angles 300 | else if( Obj is Enum ) 301 | newValue = EditorGUILayout.EnumPopup( GUIContent.none, (Enum) Obj ); 302 | else if( Obj is Color ) 303 | newValue = EditorGUILayout.ColorField( GUIContent.none, (Color) Obj ); 304 | else if( Obj is Color32 ) 305 | newValue = (Color32) EditorGUILayout.ColorField( GUIContent.none, (Color32) Obj ); 306 | else if( Obj is LayerMask ) // Credit: http://answers.unity.com/answers/1387522/view.html 307 | newValue = InternalEditorUtility.ConcatenatedLayersMaskToLayerMask( EditorGUILayout.MaskField( InternalEditorUtility.LayerMaskToConcatenatedLayersMask( (LayerMask) Obj ), InternalEditorUtility.layers ) ); 308 | else if( Obj is Rect ) 309 | newValue = EditorGUILayout.RectField( GUIContent.none, (Rect) Obj ); 310 | else if( Obj is Bounds ) 311 | newValue = EditorGUILayout.BoundsField( GUIContent.none, (Bounds) Obj ); 312 | else if( Obj is RectInt ) 313 | newValue = EditorGUILayout.RectIntField( GUIContent.none, (RectInt) Obj ); 314 | else if( Obj is BoundsInt ) 315 | newValue = EditorGUILayout.BoundsIntField( GUIContent.none, (BoundsInt) Obj ); 316 | else if( Obj is AnimationCurve ) 317 | newValue = EditorGUILayout.CurveField( GUIContent.none, (AnimationCurve) Obj ); 318 | else if( Obj is Gradient ) 319 | newValue = PasteBinWindow.gradientField.Invoke( null, new object[] { GUIContent.none, (Gradient) Obj, null } ); 320 | else if( primitiveValue != null ) // Variable is primitive 321 | { 322 | EditorGUILayout.TextArea(primitiveValue); 323 | 324 | EditorGUI.EndChangeCheck(); 325 | return true; 326 | } 327 | else 328 | { 329 | EditorGUI.EndChangeCheck(); 330 | return false; 331 | } 332 | 333 | if( EditorGUI.EndChangeCheck() ) 334 | { 335 | Object modifiedPrefabInstance = null; 336 | DebugModeEntry _parent = parent; 337 | while( _parent != null ) 338 | { 339 | if( _parent.Obj as Object ) 340 | { 341 | Undo.RecordObject( (Object) _parent.Obj, "Change Value" ); 342 | if( _parent.Obj is Component ) 343 | Undo.RecordObject( ( (Component) _parent.Obj ).gameObject, "Change Value" ); // Required for at least name and tag properties 344 | 345 | if( PrefabUtility.IsPartOfPrefabInstance( (Object) _parent.Obj ) ) 346 | modifiedPrefabInstance = _parent.Obj as Object; 347 | 348 | break; 349 | } 350 | 351 | _parent = _parent.parent; 352 | } 353 | 354 | if( !ReferenceEquals( Obj, newValue ) && newValue != null && Obj.GetType() != newValue.GetType() ) 355 | PoolLists(); 356 | 357 | Obj = newValue; 358 | Variable.Set( parent != null ? parent.Obj : null, newValue ); 359 | Refresh(); 360 | 361 | if( modifiedPrefabInstance != null ) 362 | PrefabUtility.RecordPrefabInstancePropertyModifications( (Object) _parent.Obj ); // Required for prefab modifications (without this, Redo doesn't work for prefab variables) 363 | 364 | GUIUtility.ExitGUI(); 365 | } 366 | 367 | return true; 368 | } 369 | 370 | protected List PopList( int preferredSize = 8 ) 371 | { 372 | if( pool.Count > 0 ) 373 | return pool.Pop(); 374 | 375 | return new List( preferredSize ); 376 | } 377 | 378 | public void PoolLists() 379 | { 380 | if( enumerableRoot != null ) 381 | { 382 | enumerableRoot.PoolLists(); 383 | enumerableRoot = null; 384 | } 385 | 386 | if( variables != null ) 387 | { 388 | for( int i = 0; i < variables.Count; i++ ) 389 | variables[i].PoolLists(); 390 | 391 | pool.Push( variables ); 392 | 393 | variables.Clear(); 394 | variables = null; 395 | } 396 | } 397 | 398 | public SerializedData Serialize() 399 | { 400 | return new SerializedData() 401 | { 402 | IsExpanded = IsExpanded, 403 | ForceShowNestedVariables = forceShowNestedVariables, 404 | SearchTerm = searchTerm, 405 | Variables = variables?.ConvertAll((e) => e.Serialize()), 406 | EnumerableRoot = enumerableRoot?.Serialize(), 407 | }; 408 | } 409 | 410 | public void Deserialize(SerializedData data) 411 | { 412 | forceShowNestedVariables = data.ForceShowNestedVariables; 413 | searchTerm = data.SearchTerm; 414 | IsExpanded = data.IsExpanded; 415 | 416 | Refresh(); 417 | 418 | if (variables != null && data.Variables != null) 419 | { 420 | for (int i = 0; i < variables.Count && i < data.Variables.Count; i++) 421 | variables[i].Deserialize(data.Variables[i]); 422 | } 423 | 424 | if (enumerableRoot != null && data.EnumerableRoot != null) 425 | enumerableRoot.Deserialize(data.EnumerableRoot); 426 | } 427 | } 428 | 429 | public class DebugModeEnumerableEntry : DebugModeEntry 430 | { 431 | private readonly struct EnumerableValueWrapper 432 | { 433 | public readonly DebugModeEnumerableEntry entry; 434 | public readonly int index; 435 | 436 | public EnumerableValueWrapper( DebugModeEnumerableEntry entry, int index ) 437 | { 438 | this.entry = entry; 439 | this.index = index; 440 | } 441 | 442 | public readonly object GetValue( object obj ) 443 | { 444 | return entry.GetEnumerableValue( index ); 445 | } 446 | 447 | public readonly void SetValue( object obj, object value ) 448 | { 449 | entry.SetEnumerableValue( index, value ); 450 | } 451 | } 452 | 453 | public DebugModeEnumerableEntry( DebugModeEntry parent ) : base( parent ) 454 | { 455 | } 456 | 457 | protected override void RefreshInternal() 458 | { 459 | if( m_isExpanded ) 460 | { 461 | Obj = parent.Obj; 462 | 463 | if( variables == null ) 464 | variables = PopList( Obj is ICollection ? ( (ICollection) Obj ).Count : 8 ); 465 | 466 | if( Obj is IList ) 467 | { 468 | int count = ( (IList) Obj ).Count; 469 | 470 | // Add new entries to variables if there aren't enough entries 471 | for( int i = variables.Count; i < count; i++ ) 472 | { 473 | Type listType = Obj.GetType(); 474 | Type elementType; 475 | if( listType.IsArray && listType.GetArrayRank() == 1 ) 476 | elementType = listType.GetElementType(); 477 | else if( listType.IsGenericType ) 478 | elementType = listType.GetGenericArguments()[0]; 479 | else 480 | elementType = typeof( object ); 481 | 482 | EnumerableValueWrapper valueWrapper = new EnumerableValueWrapper( this, i ); 483 | variables.Add( new DebugModeEntry( this ) { Variable = new VariableGetterHolder( i + ":", elementType, valueWrapper.GetValue, valueWrapper.SetValue ) } ); 484 | } 485 | 486 | // Remove excessive entries from variables 487 | for( int i = variables.Count - 1; i >= count; i-- ) 488 | { 489 | variables[i].PoolLists(); 490 | variables.RemoveAt( i ); 491 | } 492 | } 493 | else 494 | { 495 | int index = 0; 496 | foreach( object element in (IEnumerable) Obj ) 497 | { 498 | DebugModeEntry entry; 499 | if( index < variables.Count ) 500 | entry = variables[index]; 501 | else 502 | { 503 | EnumerableValueWrapper valueWrapper = new EnumerableValueWrapper( this, index ); 504 | entry = new DebugModeEntry( this ) { Variable = new VariableGetterHolder( index + ":", typeof( object ), valueWrapper.GetValue, valueWrapper.SetValue ) }; 505 | variables.Add( entry ); 506 | } 507 | 508 | entry.Obj = element; 509 | index++; 510 | } 511 | 512 | for( int i = variables.Count - 1; i >= index; i-- ) 513 | { 514 | variables[i].PoolLists(); 515 | variables.RemoveAt( i ); 516 | } 517 | } 518 | 519 | for( int i = 0; i < variables.Count; i++ ) 520 | variables[i].Refresh(); 521 | } 522 | } 523 | 524 | private object GetEnumerableValue( int index ) 525 | { 526 | if( Obj is IList ) 527 | return ( (IList) Obj )[index]; 528 | 529 | return variables[index].Obj; 530 | } 531 | 532 | private void SetEnumerableValue( int index, object value ) 533 | { 534 | if( Obj is IList ) 535 | ( (IList) Obj )[index] = value; 536 | } 537 | } 538 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/PasteBinWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | using UnityEditor; 6 | using UnityEditor.SceneManagement; 7 | using UnityEngine; 8 | using UnityEngine.SceneManagement; 9 | using Object = UnityEngine.Object; 10 | using SceneObjectReference = InspectPlusNamespace.SerializedClipboard.IPSceneObjectReference; 11 | using AssetReference = InspectPlusNamespace.SerializedClipboard.IPAssetReference; 12 | using VectorClipboard = InspectPlusNamespace.SerializablePropertyExtensions.VectorClipboard; 13 | using ArrayClipboard = InspectPlusNamespace.SerializablePropertyExtensions.ArrayClipboard; 14 | using GenericObjectClipboard = InspectPlusNamespace.SerializablePropertyExtensions.GenericObjectClipboard; 15 | using ManagedObjectClipboard = InspectPlusNamespace.SerializablePropertyExtensions.ManagedObjectClipboard; 16 | using GameObjectHierarchyClipboard = InspectPlusNamespace.SerializablePropertyExtensions.GameObjectHierarchyClipboard; 17 | using ComponentGroupClipboard = InspectPlusNamespace.SerializablePropertyExtensions.ComponentGroupClipboard; 18 | using AssetFilesClipboard = InspectPlusNamespace.SerializablePropertyExtensions.AssetFilesClipboard; 19 | 20 | namespace InspectPlusNamespace 21 | { 22 | // Paste Bin: a collection of save data that is shared between all Unity instances on the computer 23 | // 24 | // We aren't using EditorPrefs for synchronizing data between Unity editor instances because 25 | // changes made to EditorPrefs aren't reflected to other live Unity instances until domain reload. 26 | // We want the changes to be immediately available on the other Unity instances 27 | public class PasteBinWindow : EditorWindow, IHasCustomMenu 28 | { 29 | private const int CLIPBOARD_CAPACITY = 16; 30 | private const double CLIPBOARD_REFRESH_INTERVAL = 1.5; 31 | private const double CLIPBOARD_REFRESH_MIN_COOLDOWN = 0.1; 32 | private static readonly Color ACTIVE_CLIPBOARD_COLOR = new Color32( 245, 170, 35, 255 ); 33 | 34 | private double clipboardRefreshTime; 35 | private static readonly List clipboard = new List( 4 ); 36 | private readonly List clipboardValues = new List( 4 ); 37 | 38 | private static bool loadedActiveClipboardOnly; 39 | 40 | private static PasteBinWindow mainWindow; 41 | 42 | private static double clipboardIndexLastCheckTime; 43 | private static double clipboardLastCheckTime; 44 | private static DateTime clipboardLastDateTime; 45 | 46 | private static int m_activeClipboardIndex; 47 | private static int ActiveClipboardIndex 48 | { 49 | get 50 | { 51 | // Don't refresh too frequently (too many file operations) 52 | if( EditorApplication.timeSinceStartup - clipboardIndexLastCheckTime >= CLIPBOARD_REFRESH_MIN_COOLDOWN ) 53 | { 54 | clipboardIndexLastCheckTime = EditorApplication.timeSinceStartup; 55 | 56 | int index; 57 | if( File.Exists( ClipboardIndexSavePath ) && int.TryParse( File.ReadAllText( ClipboardIndexSavePath ), out index ) ) 58 | m_activeClipboardIndex = index; 59 | } 60 | 61 | return m_activeClipboardIndex; 62 | } 63 | set 64 | { 65 | if( value >= 0 && value < clipboard.Count ) 66 | { 67 | m_activeClipboardIndex = value; 68 | 69 | Directory.CreateDirectory( Path.GetDirectoryName( ClipboardIndexSavePath ) ); 70 | File.WriteAllText( ClipboardIndexSavePath, m_activeClipboardIndex.ToString() ); 71 | } 72 | } 73 | } 74 | 75 | public static SerializedClipboard ActiveClipboard 76 | { 77 | get 78 | { 79 | LoadClipboard( true ); 80 | 81 | int activeClipboardIndex = ActiveClipboardIndex; 82 | if( activeClipboardIndex >= clipboard.Count ) 83 | return null; 84 | 85 | // This is an edge case: after loading only the active clipboard entry with LoadClipboard(true), 86 | // if ActiveClipboardIndex is changed via another Unity project's Paste Bin window, then the 87 | // clipboard entry at that new index will be null because LoadClipboard(true) loads only the latest 88 | // clipboard entry and not the other entries. We must reload the whole clipboard data in this case 89 | if( clipboard[activeClipboardIndex] == null ) 90 | LoadClipboard( false ); 91 | 92 | return clipboard[activeClipboardIndex]; 93 | } 94 | } 95 | 96 | private static string ClipboardIndexSavePath { get { return Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData ), "Unity" + Path.DirectorySeparatorChar + "UnityInspectPlus.index" ); } } 97 | private static string ClipboardSavePath { get { return Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData ), "Unity" + Path.DirectorySeparatorChar + "UnityInspectPlus.dat" ); } } 98 | 99 | private static GUIStyle clipboardLabelGUIStyle; 100 | internal static readonly MethodInfo gradientField = typeof( EditorGUILayout ).GetMethod( "GradientField", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof( GUIContent ), typeof( Gradient ), typeof( GUILayoutOption[] ) }, null ); 101 | 102 | private int clickedSerializedClipboardIndex = -1; 103 | private Vector2 scrollPosition; 104 | 105 | public static new void Show() 106 | { 107 | PasteBinWindow window = GetWindow(); 108 | window.titleContent = new GUIContent( "Paste Bin" ); 109 | window.minSize = new Vector2( 250f, 150f ); 110 | ( (EditorWindow) window ).Show(); 111 | } 112 | 113 | private void OnEnable() 114 | { 115 | mainWindow = this; 116 | 117 | EditorApplication.update -= RefreshClipboardRegularly; 118 | EditorApplication.update += RefreshClipboardRegularly; 119 | EditorSceneManager.sceneOpened -= OnSceneOpened; 120 | EditorSceneManager.sceneOpened += OnSceneOpened; 121 | SceneManager.sceneLoaded -= OnSceneLoaded; 122 | SceneManager.sceneLoaded += OnSceneLoaded; 123 | 124 | if( !LoadClipboard() ) 125 | { 126 | // When LoadClipboard returns true, clipboardValues are filled by LoadClipboard automatically 127 | clipboardValues.Clear(); 128 | for( int i = 0; i < clipboard.Count; i++ ) 129 | clipboardValues.Add( clipboard[i].RootValue.GetClipboardObject( null ) ); 130 | } 131 | 132 | Repaint(); 133 | } 134 | 135 | private void OnDisable() 136 | { 137 | EditorApplication.update -= RefreshClipboardRegularly; 138 | EditorSceneManager.sceneOpened -= OnSceneOpened; 139 | SceneManager.sceneLoaded -= OnSceneLoaded; 140 | } 141 | 142 | private void OnDestroy() 143 | { 144 | mainWindow = null; 145 | } 146 | 147 | void IHasCustomMenu.AddItemsToMenu( GenericMenu menu ) 148 | { 149 | menu.AddItem( new GUIContent( "Clear" ), false, ClearClipboard ); 150 | menu.AddItem( new GUIContent( "About" ), false, ShowAboutDialog ); 151 | } 152 | 153 | private void RefreshClipboardRegularly() 154 | { 155 | if( EditorApplication.timeSinceStartup >= clipboardRefreshTime ) 156 | { 157 | clipboardRefreshTime = EditorApplication.timeSinceStartup + CLIPBOARD_REFRESH_INTERVAL; 158 | LoadClipboard(); 159 | } 160 | } 161 | 162 | private void OnSceneOpened( Scene scene, OpenSceneMode openSceneMode ) 163 | { 164 | RefreshSceneObjectClipboards(); 165 | } 166 | 167 | private void OnSceneLoaded( Scene scene, LoadSceneMode loadSceneMode ) 168 | { 169 | RefreshSceneObjectClipboards(); 170 | } 171 | 172 | private void RefreshSceneObjectClipboards() 173 | { 174 | for( int i = 0; i < clipboard.Count; i++ ) 175 | { 176 | if( clipboard[i].RootType == SerializedClipboard.IPObjectType.SceneObjectReference ) 177 | clipboardValues[i] = clipboard[i].RootValue.GetClipboardObject( null ); 178 | } 179 | } 180 | 181 | private void OnGUI() 182 | { 183 | Event ev = Event.current; 184 | Color backgroundColor = GUI.backgroundColor; 185 | 186 | bool originalWideMode = EditorGUIUtility.wideMode; 187 | float originalLabelWidth = EditorGUIUtility.labelWidth; 188 | 189 | float windowWidth = position.width; 190 | EditorGUIUtility.wideMode = windowWidth > 330f; 191 | EditorGUIUtility.labelWidth = windowWidth < 350f ? 130f : windowWidth * 0.4f; 192 | 193 | EditorGUILayout.HelpBox( "The highlighted value will be used in Paste operations. You can right click a value to set it active or remove it.", MessageType.None ); 194 | 195 | scrollPosition = EditorGUILayout.BeginScrollView( scrollPosition ); 196 | 197 | // Traverse the list in reverse order so that the newest SerializedClipboards will be at the top of the list 198 | for( int i = clipboard.Count - 1; i >= 0; i-- ) 199 | { 200 | if( clipboard[i] == null || clipboard[i].Equals( null ) ) 201 | { 202 | RemoveClipboard( i-- ); 203 | continue; 204 | } 205 | 206 | DrawClipboardOnGUI( clipboard[i], clipboardValues[i], ActiveClipboardIndex == i, true ); 207 | 208 | if( ev.type == EventType.MouseDown && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 209 | { 210 | clickedSerializedClipboardIndex = i; 211 | ev.Use(); 212 | } 213 | else if( ev.type == EventType.ContextClick && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 214 | { 215 | int j = i; 216 | 217 | GenericMenu menu = new GenericMenu(); 218 | menu.AddItem( new GUIContent( "Select" ), false, SetActiveClipboard, j ); 219 | menu.AddItem( new GUIContent( "Remove" ), false, RemoveClipboard, j ); 220 | 221 | menu.AddSeparator( "" ); 222 | menu.AddItem( new GUIContent( "Copy To System Clipboard" ), false, CopyClipboardToSystemBuffer, j ); 223 | 224 | string systemBuffer = GUIUtility.systemCopyBuffer; 225 | if( !string.IsNullOrEmpty( systemBuffer ) && systemBuffer.StartsWith( "Inspect+", StringComparison.Ordinal ) ) 226 | menu.AddItem( new GUIContent( "Paste From System Clipboard" ), false, PasteClipboardFromSystemBuffer, j ); 227 | else 228 | menu.AddDisabledItem( new GUIContent( "Paste From System Clipboard" ) ); 229 | 230 | menu.ShowAsContext(); 231 | 232 | ev.Use(); 233 | } 234 | else if( clickedSerializedClipboardIndex == i && ev.type == EventType.MouseUp && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 235 | { 236 | clickedSerializedClipboardIndex = -1; 237 | 238 | if( ev.button == 0 ) 239 | { 240 | ActiveClipboardIndex = i; 241 | Repaint(); 242 | ev.Use(); 243 | } 244 | else if( ev.button == 2 ) 245 | { 246 | RemoveClipboard( i ); 247 | Repaint(); 248 | ev.Use(); 249 | } 250 | } 251 | } 252 | 253 | EditorGUILayout.EndScrollView(); 254 | 255 | if( ev.type == EventType.MouseUp ) 256 | clickedSerializedClipboardIndex = -1; 257 | else if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 258 | { 259 | // Accept drag&drop 260 | DragAndDrop.visualMode = DragAndDropVisualMode.Copy; 261 | if( ev.type == EventType.DragPerform ) 262 | { 263 | DragAndDrop.AcceptDrag(); 264 | 265 | Object[] draggedObjects = DragAndDrop.objectReferences; 266 | for( int i = 0; i < draggedObjects.Length; i++ ) 267 | AddToClipboard( draggedObjects[i], Utilities.GetDetailedObjectName( draggedObjects[i] ), draggedObjects[i] ); 268 | } 269 | 270 | ev.Use(); 271 | } 272 | else if( ev.type == EventType.KeyDown ) 273 | { 274 | if( ev.keyCode == KeyCode.Delete ) 275 | { 276 | RemoveClipboard( ActiveClipboardIndex ); 277 | Repaint(); 278 | ev.Use(); 279 | } 280 | else if( ev.keyCode == KeyCode.UpArrow ) 281 | { 282 | ActiveClipboardIndex = Mathf.Max( 0, ActiveClipboardIndex - 1 ); 283 | Repaint(); 284 | ev.Use(); 285 | } 286 | else if( ev.keyCode == KeyCode.DownArrow ) 287 | { 288 | ActiveClipboardIndex = Mathf.Min( clipboard.Count - 1, ActiveClipboardIndex + 1 ); 289 | Repaint(); 290 | ev.Use(); 291 | } 292 | } 293 | 294 | EditorGUIUtility.wideMode = originalWideMode; 295 | EditorGUIUtility.labelWidth = originalLabelWidth; 296 | } 297 | 298 | public static void DrawClipboardOnGUI( SerializedClipboard clipboard, object clipboardValue, bool isActiveClipboard, bool showTooltip ) 299 | { 300 | if( clipboardLabelGUIStyle == null ) 301 | clipboardLabelGUIStyle = new GUIStyle( EditorStyles.boldLabel ) { wordWrap = true }; 302 | 303 | if( !isActiveClipboard ) 304 | GUILayout.BeginVertical( PasteBinTooltip.Style ); 305 | else 306 | { 307 | Color backgroundColor = GUI.backgroundColor; 308 | GUI.backgroundColor = Color.Lerp( backgroundColor, ACTIVE_CLIPBOARD_COLOR, 0.5f ); 309 | GUILayout.BeginVertical( PasteBinTooltip.Style ); 310 | GUI.backgroundColor = backgroundColor; 311 | } 312 | 313 | if( showTooltip ) 314 | EditorGUILayout.LabelField( clipboard.LabelContent, clipboardLabelGUIStyle ); 315 | else 316 | EditorGUILayout.LabelField( clipboard.Label, clipboardLabelGUIStyle ); 317 | 318 | EditorGUI.indentLevel++; 319 | 320 | if( clipboardValue as Object ) 321 | EditorGUILayout.ObjectField( GUIContent.none, clipboardValue as Object, typeof( Object ), true ); 322 | else if( clipboardValue is long ) 323 | EditorGUILayout.TextField( GUIContent.none, ( (long) clipboardValue ).ToString() ); 324 | else if( clipboardValue is double ) 325 | EditorGUILayout.TextField( GUIContent.none, ( (double) clipboardValue ).ToString() ); 326 | else if( clipboardValue is Color ) 327 | EditorGUILayout.ColorField( GUIContent.none, (Color) clipboardValue ); 328 | else if( clipboardValue is string ) 329 | EditorGUILayout.TextField( GUIContent.none, (string) clipboardValue ); 330 | else if( clipboardValue is bool ) 331 | EditorGUILayout.Toggle( GUIContent.none, (bool) clipboardValue ); 332 | else if( clipboardValue is AnimationCurve ) 333 | EditorGUILayout.CurveField( GUIContent.none, (AnimationCurve) clipboardValue ); 334 | else if( clipboardValue is Gradient ) 335 | gradientField.Invoke( null, new object[] { GUIContent.none, clipboardValue, null } ); 336 | else if( clipboardValue is VectorClipboard ) 337 | EditorGUILayout.Vector4Field( GUIContent.none, (VectorClipboard) clipboardValue ); 338 | else if( clipboardValue is ArrayClipboard ) 339 | { 340 | ArrayClipboard obj = (ArrayClipboard) clipboardValue; 341 | EditorGUILayout.TextField( GUIContent.none, string.Concat( obj.elementType, "[", obj.elements.Length, "] array" ) ); 342 | } 343 | else if( clipboardValue is GenericObjectClipboard ) 344 | EditorGUILayout.TextField( GUIContent.none, ( (GenericObjectClipboard) clipboardValue ).type + " object" ); 345 | else if( clipboardValue is ManagedObjectClipboard ) 346 | EditorGUILayout.TextField( GUIContent.none, ( (ManagedObjectClipboard) clipboardValue ).type + " object (SerializeField)" ); 347 | else if( clipboardValue is GameObjectHierarchyClipboard ) 348 | EditorGUILayout.TextField( GUIContent.none, ( (GameObjectHierarchyClipboard) clipboardValue ).name + " (Complete GameObject)" ); 349 | else if( clipboardValue is ComponentGroupClipboard ) 350 | EditorGUILayout.TextField( GUIContent.none, ( (ComponentGroupClipboard) clipboardValue ).name + " (Multiple Components)" ); 351 | else if( clipboardValue is AssetFilesClipboard ) 352 | EditorGUILayout.TextField( GUIContent.none, ( (AssetFilesClipboard) clipboardValue ).paths[0] + " (Asset File)" ); 353 | else if( clipboard.RootValue is SceneObjectReference ) 354 | EditorGUILayout.TextField( GUIContent.none, clipboard.RootUnityObjectType.Name + " object (Scene Object)" ); 355 | else if( clipboard.RootValue is AssetReference ) 356 | EditorGUILayout.TextField( GUIContent.none, clipboard.RootUnityObjectType.Name + " object (Asset)" ); 357 | else 358 | EditorGUILayout.TextField( GUIContent.none, clipboard.RootValue.GetType().Name + " object" ); 359 | 360 | EditorGUI.indentLevel--; 361 | GUILayout.EndVertical(); 362 | } 363 | 364 | private void SetActiveClipboard( object obj ) 365 | { 366 | int index = (int) obj; 367 | if( index < clipboard.Count ) 368 | ActiveClipboardIndex = index; 369 | } 370 | 371 | public static void AddToClipboard( SerializedProperty prop ) 372 | { 373 | object clipboard = prop.CopyValue(); 374 | if( clipboard != null ) 375 | AddToClipboard( clipboard, prop.name, string.Concat( Utilities.GetDetailedObjectName( prop.serializedObject.targetObject ), ".", prop.propertyPath.Replace( ".Array.data[", "[" ) ), prop.serializedObject.targetObject ); 376 | } 377 | 378 | public static void AddToClipboard( object obj, string label, Object context ) 379 | { 380 | AddToClipboard( obj, null, label, context ); 381 | } 382 | 383 | public static void AddToClipboard( object obj, string propertyName, string label, Object context ) 384 | { 385 | if( obj == null || obj.Equals( null ) ) 386 | return; 387 | 388 | LoadClipboard(); 389 | 390 | if( clipboard.Count >= CLIPBOARD_CAPACITY ) 391 | { 392 | clipboard.RemoveAt( 0 ); 393 | 394 | if( mainWindow ) 395 | mainWindow.clipboardValues.RemoveAt( 0 ); 396 | } 397 | 398 | clipboard.Add( new SerializedClipboard( obj, context, propertyName, label ) ); 399 | ActiveClipboardIndex = clipboard.Count - 1; 400 | 401 | if( mainWindow ) 402 | { 403 | mainWindow.clipboardValues.Add( clipboard[clipboard.Count - 1].RootValue.GetClipboardObject( null ) ); 404 | mainWindow.Repaint(); 405 | } 406 | 407 | // Call SaveClipboard in the next frame because sometimes AddToClipboard can be called in a batch (e.g. drag & drop, 408 | // context menu) and we don't want to execute multiple file save operations in the same frame for no reason 409 | EditorApplication.update -= SaveClipboardDelayed; 410 | EditorApplication.update += SaveClipboardDelayed; 411 | } 412 | 413 | public static void RemoveClipboard( SerializedClipboard clipboard ) 414 | { 415 | int index = PasteBinWindow.clipboard.IndexOf( clipboard ); 416 | if( index >= 0 ) 417 | { 418 | RemoveClipboard( index ); 419 | 420 | if( mainWindow ) 421 | mainWindow.Repaint(); 422 | } 423 | } 424 | 425 | private static void RemoveClipboard( object obj ) 426 | { 427 | int index = (int) obj; 428 | if( index < clipboard.Count ) 429 | { 430 | clipboard.RemoveAt( index ); 431 | if( mainWindow ) 432 | mainWindow.clipboardValues.RemoveAt( index ); 433 | 434 | if( ActiveClipboardIndex > 0 && ActiveClipboardIndex >= clipboard.Count ) 435 | ActiveClipboardIndex = clipboard.Count - 1; 436 | } 437 | 438 | SaveClipboard(); 439 | } 440 | 441 | private void ClearClipboard() 442 | { 443 | clipboard.Clear(); 444 | clipboardValues.Clear(); 445 | 446 | ActiveClipboardIndex = 0; 447 | SaveClipboard(); 448 | } 449 | 450 | private void CopyClipboardToSystemBuffer( object obj ) 451 | { 452 | int index = (int) obj; 453 | if( index < clipboard.Count ) 454 | { 455 | using( MemoryStream stream = new MemoryStream() ) 456 | using( BinaryWriter writer = new BinaryWriter( stream ) ) 457 | { 458 | clipboard[index].Serialize( writer ); 459 | GUIUtility.systemCopyBuffer = "Inspect+" + Convert.ToBase64String( stream.ToArray() ); 460 | } 461 | } 462 | } 463 | 464 | private void PasteClipboardFromSystemBuffer( object obj ) 465 | { 466 | int index = (int) obj; 467 | string systemBuffer = GUIUtility.systemCopyBuffer; 468 | if( string.IsNullOrEmpty( systemBuffer ) || !systemBuffer.StartsWith( "Inspect+", StringComparison.Ordinal ) ) 469 | return; 470 | 471 | using( MemoryStream stream = new MemoryStream( Convert.FromBase64String( systemBuffer.Substring( 8 ) ) ) ) 472 | using( BinaryReader reader = new BinaryReader( stream ) ) 473 | { 474 | SerializedClipboard _clipboard = new SerializedClipboard( reader ); 475 | 476 | if( clipboard.Count >= CLIPBOARD_CAPACITY ) 477 | { 478 | clipboard.RemoveAt( 0 ); 479 | clipboardValues.RemoveAt( 0 ); 480 | } 481 | 482 | clipboard.Insert( index, _clipboard ); 483 | clipboardValues.Insert( index, _clipboard.RootValue.GetClipboardObject( null ) ); 484 | 485 | ActiveClipboardIndex = index; 486 | 487 | Repaint(); 488 | SaveClipboard(); 489 | } 490 | } 491 | 492 | private void ShowAboutDialog() 493 | { 494 | EditorUtility.DisplayDialog( "Paste Bin", "Paste Bin save file is located at: " + ClipboardSavePath + ".\n\nThe same file is used in all Unity projects on this computer.", "OK" ); 495 | } 496 | 497 | public static List GetSerializedClipboards() 498 | { 499 | LoadClipboard(); 500 | return clipboard; 501 | } 502 | 503 | private static void SaveClipboardDelayed() 504 | { 505 | EditorApplication.update -= SaveClipboardDelayed; 506 | SaveClipboard(); 507 | } 508 | 509 | private static void SaveClipboard() 510 | { 511 | try 512 | { 513 | Directory.CreateDirectory( Path.GetDirectoryName( ClipboardSavePath ) ); 514 | 515 | using( FileStream stream = new FileStream( ClipboardSavePath, FileMode.Create, FileAccess.Write, FileShare.None ) ) 516 | using( BinaryWriter writer = new BinaryWriter( stream ) ) 517 | { 518 | writer.Write( clipboard.Count ); 519 | 520 | // Writing the clipboard data in reverse order allows us to access the latest clipboard entry 521 | // immediately while reading the clipboard data. Then, we can skip the rest of the clipboard 522 | // data when possible (i.e. when loadActiveClipboardOnly=true in LoadClipboard) 523 | for( int i = clipboard.Count - 1; i >= 0; i-- ) 524 | clipboard[i].Serialize( writer ); 525 | } 526 | 527 | clipboardLastDateTime = File.GetLastWriteTimeUtc( ClipboardSavePath ); 528 | } 529 | catch( Exception e ) 530 | { 531 | Debug.LogException( e ); 532 | } 533 | } 534 | 535 | private static bool LoadClipboard( bool loadActiveClipboardOnly = false ) 536 | { 537 | if( EditorApplication.timeSinceStartup - clipboardLastCheckTime < CLIPBOARD_REFRESH_MIN_COOLDOWN ) 538 | return false; 539 | 540 | clipboardLastCheckTime = EditorApplication.timeSinceStartup; 541 | 542 | FileInfo saveFile = new FileInfo( ClipboardSavePath ); 543 | if( !saveFile.Exists ) 544 | return false; 545 | 546 | // Don't reload clipboard if it is up-to-date 547 | bool shouldForceReload = !loadActiveClipboardOnly && loadedActiveClipboardOnly; 548 | if( !shouldForceReload && saveFile.LastWriteTimeUtc <= clipboardLastDateTime ) 549 | return false; 550 | 551 | clipboardLastDateTime = saveFile.LastWriteTimeUtc; 552 | clipboard.Clear(); 553 | 554 | try 555 | { 556 | using( FileStream stream = new FileStream( ClipboardSavePath, FileMode.Open, FileAccess.Read, FileShare.Read ) ) 557 | using( BinaryReader reader = new BinaryReader( stream ) ) 558 | { 559 | int clipboardSize = reader.ReadInt32(); 560 | if( loadActiveClipboardOnly && ActiveClipboardIndex == clipboardSize - 1 ) 561 | { 562 | // This is the case most of the time 563 | loadedActiveClipboardOnly = true; 564 | 565 | clipboard.Add( new SerializedClipboard( reader ) ); 566 | 567 | // No need to deserialize the rest of the clipboard data 568 | for( int i = 1; i < clipboardSize; i++ ) 569 | clipboard.Add( null ); 570 | } 571 | else 572 | { 573 | loadedActiveClipboardOnly = false; 574 | 575 | for( int i = 0; i < clipboardSize; i++ ) 576 | clipboard.Add( new SerializedClipboard( reader ) ); 577 | } 578 | 579 | // We are writing the clipboard data in reverse order in SaveClipboard 580 | clipboard.Reverse(); 581 | } 582 | } 583 | catch( IOException e ) 584 | { 585 | Debug.LogException( e ); 586 | } 587 | catch( Exception e ) 588 | { 589 | Debug.LogWarning( "Couldn't load saved clipboard data (probably save format has changed in an update).\n" + e.ToString() ); 590 | 591 | clipboard.Clear(); 592 | saveFile.Delete(); 593 | } 594 | 595 | if( mainWindow ) 596 | { 597 | mainWindow.clipboardValues.Clear(); 598 | for( int i = 0; i < clipboard.Count; i++ ) 599 | mainWindow.clipboardValues.Add( clipboard[i].RootValue.GetClipboardObject( null ) ); 600 | } 601 | 602 | return true; 603 | } 604 | } 605 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/BasketWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using UnityEditor; 5 | using UnityEditor.IMGUI.Controls; 6 | using UnityEditor.SceneManagement; 7 | using UnityEngine; 8 | using UnityEngine.SceneManagement; 9 | using Object = UnityEngine.Object; 10 | #if UNITY_6000_3_OR_NEWER 11 | using EntityId = UnityEngine.EntityId; 12 | #else 13 | using EntityId = System.Int32; 14 | #endif 15 | #if UNITY_6000_3_OR_NEWER 16 | using TreeView = UnityEditor.IMGUI.Controls.TreeView; 17 | using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem; 18 | using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState; 19 | #elif UNITY_6000_2_OR_NEWER 20 | using TreeView = UnityEditor.IMGUI.Controls.TreeView; 21 | using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem; 22 | using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState; 23 | #endif 24 | 25 | namespace InspectPlusNamespace 26 | { 27 | public class BasketWindow : EditorWindow, IHasCustomMenu 28 | { 29 | private class NameComparer : IComparer where T : BasketWindowEntry 30 | { 31 | public int Compare( T x, T y ) 32 | { 33 | return EditorUtility.NaturalCompare( x.Name, y.Name ); 34 | } 35 | } 36 | 37 | private class TypeComparer : IComparer where T : BasketWindowEntry 38 | { 39 | private readonly ObjectBrowserWindow.TypeComparer objectTypeComparer = new ObjectBrowserWindow.TypeComparer(); 40 | 41 | public int Compare( T x, T y ) 42 | { 43 | if( x.Target == null ) 44 | { 45 | if( y.Target != null ) 46 | return 1; 47 | else 48 | return EditorUtility.NaturalCompare( x.Name, y.Name ); 49 | } 50 | else if( y.Target == null ) 51 | return -1; 52 | 53 | return objectTypeComparer.Compare( x.Target, y.Target ); 54 | } 55 | } 56 | 57 | private const string SAVE_FILE_EXTENSION = "basket"; 58 | private const string SAVE_DIRECTORY = "UserSettings/BasketWindows"; 59 | private const string ACTIVE_WINDOW_SAVE_FILE = SAVE_DIRECTORY + "/_ActiveWindow." + SAVE_FILE_EXTENSION; 60 | 61 | private BasketWindowDrawer treeView; 62 | [SerializeField] 63 | private BasketWindowState treeViewState = new BasketWindowState(); 64 | private SearchField searchField; 65 | 66 | private bool shouldRepositionSelf; 67 | private bool isDataDirty; 68 | private bool isHierarchyWindowDirty, isProjectWindowDirty, isNewSceneOpened; 69 | private int titleObjectCount = 0; 70 | 71 | public static new BasketWindow Show( bool newInstance ) 72 | { 73 | BasketWindow window = newInstance ? CreateInstance() : GetWindow(); 74 | window.titleObjectCount = 0; 75 | window.titleContent = new GUIContent( "Basket (0)" ); 76 | window.minSize = new Vector2( 200f, 100f ); 77 | 78 | if( newInstance ) 79 | window.shouldRepositionSelf = true; 80 | else if( window.treeViewState.Entries.Count == 0 && File.Exists( ACTIVE_WINDOW_SAVE_FILE ) ) 81 | window.LoadData( ACTIVE_WINDOW_SAVE_FILE ); 82 | 83 | window.Show(); 84 | return window; 85 | } 86 | 87 | void IHasCustomMenu.AddItemsToMenu( GenericMenu menu ) 88 | { 89 | if( treeView == null ) 90 | return; 91 | 92 | if( treeViewState.Entries.Count > 0 ) 93 | { 94 | menu.AddItem( new GUIContent( "Save..." ), false, () => 95 | { 96 | Directory.CreateDirectory( SAVE_DIRECTORY ); 97 | 98 | string savePath = EditorUtility.SaveFilePanel( "Save As", SAVE_DIRECTORY, "", SAVE_FILE_EXTENSION ); 99 | if( !string.IsNullOrEmpty( savePath ) ) 100 | SaveData( savePath ); 101 | } ); 102 | } 103 | else 104 | menu.AddDisabledItem( new GUIContent( "Save..." ) ); 105 | 106 | menu.AddItem( new GUIContent( "Load..." ), false, () => 107 | { 108 | Directory.CreateDirectory( SAVE_DIRECTORY ); 109 | 110 | string loadPath = EditorUtility.OpenFilePanel( "Load", SAVE_DIRECTORY, SAVE_FILE_EXTENSION ); 111 | if( !string.IsNullOrEmpty( loadPath ) ) 112 | LoadData( loadPath ); 113 | } ); 114 | 115 | menu.AddSeparator( "" ); 116 | 117 | menu.AddItem( new GUIContent( "Synchronize Selection With Unity" ), treeViewState.SyncSelection, () => treeViewState.SyncSelection = !treeViewState.SyncSelection ); 118 | 119 | if( treeViewState.Entries.Count > 1 ) 120 | { 121 | menu.AddSeparator( "" ); 122 | 123 | menu.AddItem( new GUIContent( "Sort By Name" ), false, () => 124 | { 125 | treeViewState.Entries.Sort( new NameComparer() ); 126 | foreach( BasketWindowRootEntry entry in treeViewState.Entries ) 127 | { 128 | if( entry.Children.Count > 0 ) 129 | entry.Children.Sort( new NameComparer() ); 130 | } 131 | 132 | treeView.Reload(); 133 | } ); 134 | 135 | menu.AddItem( new GUIContent( "Sort By Type" ), false, () => 136 | { 137 | treeViewState.Entries.Sort( new TypeComparer() ); 138 | foreach( BasketWindowRootEntry entry in treeViewState.Entries ) 139 | { 140 | if( entry.Children.Count > 0 ) 141 | entry.Children.Sort( new TypeComparer() ); 142 | } 143 | 144 | treeView.Reload(); 145 | } ); 146 | } 147 | } 148 | 149 | private void Awake() 150 | { 151 | treeViewState.SyncSelection = InspectPlusSettings.Instance.SyncBasketSelection; 152 | 153 | if( treeViewState.Entries.Count > 0 ) 154 | { 155 | // This BasketWindow has persisted between Editor sessions, reload its data 156 | LoadData(); 157 | } 158 | } 159 | 160 | private void OnEnable() 161 | { 162 | EditorSceneManager.sceneOpened += OnSceneOpened; 163 | EditorApplication.wantsToQuit += OnEditorQuitting; 164 | } 165 | 166 | private void OnDisable() 167 | { 168 | EditorSceneManager.sceneOpened -= OnSceneOpened; 169 | EditorApplication.wantsToQuit -= OnEditorQuitting; 170 | } 171 | 172 | private bool OnEditorQuitting() 173 | { 174 | // Calling SaveData inside OnDestroy doesn't seem to save the changes to savedData between Unity sessions 175 | // on at least Unity 2019.4.26f1 (EditorWindow is possibly serialized before OnDestroy is invoked). Thus, 176 | // we're saving the data in EditorApplication.wantsToQuit instead 177 | SaveData(); 178 | return true; 179 | } 180 | 181 | private void OnDestroy() 182 | { 183 | SaveData(); 184 | } 185 | 186 | private void OnSceneOpened( Scene scene, OpenSceneMode mode ) 187 | { 188 | isNewSceneOpened = isHierarchyWindowDirty = true; 189 | } 190 | 191 | private void OnHierarchyChange() 192 | { 193 | isHierarchyWindowDirty = true; 194 | } 195 | 196 | private void OnProjectChange() 197 | { 198 | isProjectWindowDirty = true; 199 | } 200 | 201 | private void InitializeTreeViewIfNecessary() 202 | { 203 | if( treeView == null ) 204 | treeView = new BasketWindowDrawer( treeViewState ); 205 | } 206 | 207 | public void AddToBasket( Object[] objects ) 208 | { 209 | InitializeTreeViewIfNecessary(); 210 | treeView.AddObjects( objects, treeViewState.Entries.Count ); 211 | } 212 | 213 | private void SaveData() 214 | { 215 | if( isDataDirty ) 216 | { 217 | Directory.CreateDirectory( SAVE_DIRECTORY ); 218 | SaveData( ACTIVE_WINDOW_SAVE_FILE ); 219 | 220 | isDataDirty = false; 221 | } 222 | } 223 | 224 | private void SaveData( string path ) 225 | { 226 | File.WriteAllText( path, EditorJsonUtility.ToJson( treeViewState, false ) ); 227 | } 228 | 229 | private void LoadData( string path ) 230 | { 231 | EditorJsonUtility.FromJsonOverwrite( File.ReadAllText( path ), treeViewState ); 232 | LoadData(); 233 | } 234 | 235 | private void LoadData() 236 | { 237 | isHierarchyWindowDirty = isProjectWindowDirty = isNewSceneOpened = true; 238 | 239 | if( treeView != null ) 240 | treeView.Reload(); 241 | } 242 | 243 | private void OnGUI() 244 | { 245 | InitializeTreeViewIfNecessary(); 246 | 247 | if( searchField == null ) 248 | { 249 | searchField = new SearchField(); 250 | searchField.downOrUpArrowKeyPressed += treeView.SetFocusAndEnsureSelectedItem; 251 | } 252 | 253 | bool isDirty = false; 254 | if( isNewSceneOpened ) 255 | { 256 | isNewSceneOpened = false; 257 | foreach( BasketWindowRootEntry entry in treeViewState.Entries ) 258 | isDirty |= entry.RefreshTargetsOfChildren(); 259 | } 260 | 261 | if( isHierarchyWindowDirty ) 262 | { 263 | isHierarchyWindowDirty = false; 264 | foreach( BasketWindowRootEntry entry in treeViewState.Entries ) 265 | { 266 | if( entry.Target as SceneAsset ) 267 | isDirty |= entry.RefreshNamesOfChildren(); 268 | } 269 | } 270 | 271 | if( isProjectWindowDirty ) 272 | { 273 | isProjectWindowDirty = false; 274 | foreach( BasketWindowRootEntry entry in treeViewState.Entries ) 275 | isDirty |= entry.RefreshName(); 276 | } 277 | 278 | isDataDirty |= isDirty; 279 | 280 | string searchTerm = treeViewState.SearchTerm; 281 | treeViewState.SearchTerm = searchField.OnToolbarGUI( searchTerm ); 282 | isDirty |= treeViewState.SearchTerm != searchTerm; 283 | 284 | if( isDirty ) 285 | treeView.Reload(); 286 | 287 | treeView.OnGUI( GUILayoutUtility.GetRect( 0f, 100000f, 0f, 100000f ) ); 288 | 289 | // This happens only when the mouse click is not captured by the TreeView. In this case, clear its selection 290 | if( Event.current.type == EventType.MouseDown && Event.current.button == 0 ) 291 | { 292 | treeView.SetSelection(new EntityId[0]); 293 | 294 | Event.current.Use(); 295 | Repaint(); 296 | } 297 | 298 | int entryCount = treeViewState.TotalEntryCount; 299 | if( titleObjectCount != entryCount ) 300 | { 301 | titleObjectCount = entryCount; 302 | titleContent = new GUIContent( "Basket (" + titleObjectCount + ")" ); 303 | isDataDirty = true; 304 | } 305 | 306 | if( shouldRepositionSelf ) 307 | { 308 | shouldRepositionSelf = false; 309 | Vector2 _position = position.position + new Vector2( 50f, 50f ); 310 | position = new Rect( _position, position.size ); 311 | } 312 | } 313 | } 314 | 315 | public abstract class BasketWindowEntry 316 | { 317 | public Object Target; 318 | public string Name = "Null"; 319 | public EntityId InstanceID => (Target != null) ? Target.GetEntityId() : GetHashCode(); 320 | 321 | public BasketWindowEntry( Object target ) 322 | { 323 | RefreshTarget( target ); 324 | } 325 | 326 | public bool RefreshTarget( Object target ) 327 | { 328 | if( target == null || target == Target ) 329 | return RefreshName(); 330 | 331 | Target = target; 332 | RefreshName(); 333 | return true; 334 | } 335 | 336 | public bool RefreshName() 337 | { 338 | if( Target == null ) 339 | return false; 340 | 341 | string prevName = Name; 342 | Name = Target.name; 343 | return Name != prevName; 344 | } 345 | } 346 | 347 | [Serializable] 348 | public class BasketWindowRootEntry : BasketWindowEntry 349 | { 350 | public List Children = new List(); 351 | 352 | public BasketWindowRootEntry( Object target ) : base( target ) 353 | { 354 | } 355 | 356 | public bool RefreshNamesOfChildren() 357 | { 358 | bool isDirty = false; 359 | foreach( BasketWindowChildEntry child in Children ) 360 | isDirty |= child.RefreshName(); 361 | 362 | return isDirty; 363 | } 364 | 365 | public bool RefreshTargetsOfChildren() 366 | { 367 | if( Children == null ) 368 | return false; 369 | 370 | List nullEntries = Children.FindAll( ( e ) => e.Target == null ); 371 | if( nullEntries.Count == 0 ) 372 | return false; 373 | 374 | bool isDirty = false; 375 | Object[] objects = new Object[nullEntries.Count]; 376 | GlobalObjectId[] globalObjectIds = new GlobalObjectId[nullEntries.Count]; 377 | for( int i = 0; i < nullEntries.Count; i++ ) 378 | GlobalObjectId.TryParse( nullEntries[i].ID, out globalObjectIds[i] ); 379 | 380 | GlobalObjectId.GlobalObjectIdentifiersToObjectsSlow( globalObjectIds, objects ); 381 | for( int i = 0; i < objects.Length; i++ ) 382 | isDirty |= nullEntries[i].RefreshTarget( objects[i] ); 383 | 384 | return isDirty; 385 | } 386 | } 387 | 388 | [Serializable] 389 | public class BasketWindowChildEntry : BasketWindowEntry 390 | { 391 | public string ID; 392 | 393 | public BasketWindowChildEntry( Object target ) : base( target ) 394 | { 395 | ID = GlobalObjectId.GetGlobalObjectIdSlow( target ).ToString(); 396 | } 397 | } 398 | 399 | [Serializable] 400 | public class BasketWindowState : TreeViewState 401 | { 402 | public List Entries = new List(); 403 | public bool SyncSelection = true; 404 | public string SearchTerm; // Built-in search doesn't preserve row order, so we perform search manually 405 | 406 | public int TotalEntryCount 407 | { 408 | get 409 | { 410 | int result = Entries.Count; 411 | foreach( BasketWindowRootEntry entry in Entries ) 412 | result += entry.Children.Count; 413 | 414 | return result; 415 | } 416 | } 417 | } 418 | 419 | public class BasketWindowTreeViewItem : TreeViewItem 420 | { 421 | public readonly BasketWindowEntry Entry; 422 | public BasketWindowRootEntry ParentEntry { get { return ( parent is BasketWindowTreeViewItem ) ? ( parent as BasketWindowTreeViewItem ).Entry as BasketWindowRootEntry : null; } } 423 | public int Index { get { return parent.children.IndexOf( this ); } } 424 | 425 | public BasketWindowTreeViewItem( BasketWindowEntry entry ) : base() 426 | { 427 | Entry = entry; 428 | } 429 | } 430 | 431 | public class BasketWindowDrawer : TreeView 432 | { 433 | private readonly new BasketWindowState state; 434 | 435 | public BasketWindowDrawer( BasketWindowState state ) : base( state ) 436 | { 437 | this.state = state; 438 | Reload(); 439 | } 440 | 441 | protected override TreeViewItem BuildRoot() 442 | { 443 | TreeViewItem root = new TreeViewItem() { id = -1, depth = -1, displayName = "Root" }; 444 | foreach( BasketWindowRootEntry entry in state.Entries ) 445 | CreateItemForEntryRecursive( entry, root ); 446 | 447 | if( !root.hasChildren ) // If we don't create a dummy child, Unity throws an exception 448 | root.AddChild( new TreeViewItem() { id = -2, depth = 0, displayName = string.IsNullOrEmpty( state.SearchTerm ) ? "Basket is empty..." : "No matching results..." } ); 449 | 450 | return root; 451 | } 452 | 453 | private void CreateItemForEntryRecursive( BasketWindowEntry entry, TreeViewItem parent ) 454 | { 455 | if( string.IsNullOrEmpty( state.SearchTerm ) || entry.Name.ContainsIgnoreCase( state.SearchTerm ) ) 456 | { 457 | BasketWindowTreeViewItem item = new BasketWindowTreeViewItem( entry ) 458 | { 459 | id = entry.InstanceID, 460 | depth = parent.depth + 1, 461 | displayName = entry.Name, 462 | icon = ( entry.Target != null ) ? AssetPreview.GetMiniThumbnail( entry.Target ) : null, 463 | }; 464 | 465 | parent.AddChild( item ); 466 | if( string.IsNullOrEmpty( state.SearchTerm ) ) 467 | parent = item; 468 | } 469 | 470 | if( entry is BasketWindowRootEntry ) 471 | { 472 | foreach( BasketWindowChildEntry childEntry in ( entry as BasketWindowRootEntry ).Children ) 473 | CreateItemForEntryRecursive( childEntry, parent ); 474 | } 475 | } 476 | 477 | protected override void SelectionChanged(IList selectedIds) 478 | { 479 | if( !state.SyncSelection || selectedIds == null ) 480 | return; 481 | 482 | EntityId[] selectionArray = new EntityId[selectedIds.Count]; 483 | selectedIds.CopyTo( selectionArray, 0 ); 484 | 485 | #if UNITY_6000_3_OR_NEWER 486 | Selection.entityIds = selectionArray; 487 | #else 488 | Selection.instanceIDs = selectionArray; 489 | #endif 490 | } 491 | 492 | protected override void DoubleClickedItem(EntityId id) 493 | { 494 | AssetDatabase.OpenAsset( id ); 495 | } 496 | 497 | protected override void ContextClickedItem(EntityId id) 498 | { 499 | ContextClicked(); 500 | } 501 | 502 | protected override void ContextClicked() 503 | { 504 | if( state.Entries.Count > 0 && HasSelection() && HasFocus() ) 505 | { 506 | GenericMenu contextMenu = new GenericMenu(); 507 | contextMenu.AddItem( new GUIContent( "Remove" ), false, () => RemoveObjects( GetSelection() ) ); 508 | contextMenu.AddSeparator( "" ); 509 | foreach( string builtInMenuItem in Unsupported.GetSubmenus( "Assets" ) ) 510 | { 511 | if( !builtInMenuItem.StartsWith( "Assets/Create" ) && builtInMenuItem != "Assets/Rename" && builtInMenuItem != "Assets/Delete" ) 512 | contextMenu.AddItem( new GUIContent( builtInMenuItem ), false, () => EditorApplication.ExecuteMenuItem( builtInMenuItem ) ); 513 | } 514 | contextMenu.ShowAsContext(); 515 | 516 | if( Event.current != null && Event.current.type == EventType.ContextClick ) 517 | Event.current.Use(); // It's safer to eat the event and if we don't, the context menu is sometimes displayed with a delay 518 | } 519 | } 520 | 521 | protected override bool CanStartDrag( CanStartDragArgs args ) 522 | { 523 | return state.Entries.Count > 0; 524 | } 525 | 526 | protected override void SetupDragAndDrop( SetupDragAndDropArgs args ) 527 | { 528 | IList draggedItemIds = SortItemIDsInRowOrder(args.draggedItemIDs); 529 | if( draggedItemIds.Count == 0 ) 530 | return; 531 | 532 | List draggedObjects = new List( draggedItemIds.Count ); 533 | for( int i = 0; i < draggedItemIds.Count; i++ ) 534 | { 535 | Object obj = Utilities.EntityIdToObject(draggedItemIds[i]); 536 | if( obj ) 537 | draggedObjects.Add( obj ); 538 | } 539 | 540 | DragAndDrop.objectReferences = draggedObjects.ToArray(); 541 | DragAndDrop.SetGenericData( "BasketIDs", draggedItemIds ); 542 | DragAndDrop.StartDrag((draggedItemIds.Count > 1) ? "" : FindEntryWithInstanceID(draggedItemIds[0], out _).Name); 543 | } 544 | 545 | protected override DragAndDropVisualMode HandleDragAndDrop( DragAndDropArgs args ) 546 | { 547 | if( args.dragAndDropPosition == DragAndDropPosition.UponItem ) 548 | return DragAndDropVisualMode.None; 549 | if( hasSearch && args.dragAndDropPosition == DragAndDropPosition.BetweenItems ) 550 | return DragAndDropVisualMode.None; 551 | 552 | if( args.performDrop ) 553 | { 554 | AddObjects(DragAndDrop.objectReferences, DragAndDrop.GetGenericData("BasketIDs") as IList, 555 | ( args.parentItem is BasketWindowTreeViewItem ) ? ( args.parentItem as BasketWindowTreeViewItem ).Entry as BasketWindowRootEntry : null, 556 | ( args.dragAndDropPosition == DragAndDropPosition.OutsideItems ) ? state.Entries.Count : args.insertAtIndex ); 557 | } 558 | 559 | return DragAndDropVisualMode.Copy; 560 | } 561 | 562 | protected override void CommandEventHandling() 563 | { 564 | if( state.Entries.Count > 0 && HasFocus() ) // There may be multiple SearchResultTreeViews. Execute the event only for the currently focused one 565 | { 566 | Event ev = Event.current; 567 | if( ev.type == EventType.ValidateCommand || ev.type == EventType.ExecuteCommand ) 568 | { 569 | if( ev.commandName == "Delete" || ev.commandName == "SoftDelete" ) 570 | { 571 | if( ev.type == EventType.ExecuteCommand ) 572 | RemoveObjects( GetSelection() ); 573 | 574 | ev.Use(); 575 | return; 576 | } 577 | } 578 | } 579 | 580 | base.CommandEventHandling(); 581 | } 582 | 583 | public void AddObjects( Object[] objects, int insertIndex ) 584 | { 585 | AddObjects( objects, null, null, insertIndex ); 586 | } 587 | 588 | private void AddObjects(Object[] objects, IList instanceIDs, BasketWindowRootEntry targetParentEntry, int insertIndex) 589 | { 590 | // If we're in search mode, exit search mode to make things easier 591 | if( !string.IsNullOrEmpty( state.SearchTerm ) ) 592 | { 593 | state.SearchTerm = null; 594 | Reload(); 595 | } 596 | 597 | instanceIDs ??= Array.ConvertAll(objects, (e) => (e != null) ? e.GetEntityId() : default); 598 | 599 | List addedInstanceIDs = new(); 600 | for( int i = instanceIDs.Count - 1; i >= 0; i-- ) 601 | { 602 | if( !addedInstanceIDs.Contains( instanceIDs[i] ) && AddObject( instanceIDs[i], targetParentEntry, ref insertIndex ) != null ) 603 | addedInstanceIDs.Add( instanceIDs[i] ); 604 | } 605 | 606 | if( addedInstanceIDs.Count > 0 ) 607 | { 608 | /// Filtering addedInstanceIDs with is necessary in the following scenario to avoid an error in : 609 | /// 1) Object X from scene Y is added to the basket 610 | /// 2) Scene Y is closed 611 | /// 3) Object X is drag & dropped to change its sibling index 612 | Reload(); 613 | SetSelection( addedInstanceIDs.FindAll( ( e ) => FindItem( e, rootItem ) != null ), TreeViewSelectionOptions.FireSelectionChanged | TreeViewSelectionOptions.RevealAndFrame ); 614 | } 615 | } 616 | 617 | private BasketWindowEntry AddObject(EntityId instanceID, BasketWindowRootEntry targetParentEntry, ref int insertIndex) 618 | { 619 | BasketWindowRootEntry parentEntry; 620 | BasketWindowEntry entry = FindEntryWithInstanceID( instanceID, out parentEntry ); 621 | if( entry != null ) // If the object already exists in the BasketWindow 622 | { 623 | if( parentEntry != targetParentEntry ) // Don't allow changing the entry's parent 624 | return entry; 625 | else if( parentEntry != null ) 626 | ReorderEntry( entry as BasketWindowChildEntry, parentEntry.Children, ref insertIndex ); 627 | else 628 | ReorderEntry( entry as BasketWindowRootEntry, state.Entries, ref insertIndex ); 629 | 630 | return entry; 631 | } 632 | 633 | Object obj = Utilities.EntityIdToObject(instanceID); 634 | if( obj == null ) 635 | return null; 636 | 637 | if( AssetDatabase.Contains( obj ) ) 638 | { 639 | entry = new BasketWindowRootEntry( obj ); 640 | state.Entries.Insert( ( targetParentEntry == null ) ? insertIndex : state.Entries.IndexOf( targetParentEntry ), entry as BasketWindowRootEntry ); 641 | } 642 | else 643 | { 644 | string scenePath = AssetDatabase.GetAssetOrScenePath( obj ); 645 | if( string.IsNullOrEmpty( scenePath ) ) 646 | { 647 | Debug.LogWarning( "Object is neither asset nor scene object: " + obj, obj ); 648 | return null; 649 | } 650 | 651 | // Make sure that scene objects' SceneAsset exists in the list 652 | SceneAsset sceneAsset = AssetDatabase.LoadAssetAtPath( scenePath ); 653 | parentEntry = (FindEntryWithInstanceID(sceneAsset.GetEntityId(), out _) as BasketWindowRootEntry) ?? AddObject(sceneAsset.GetEntityId(), targetParentEntry, ref insertIndex) as BasketWindowRootEntry; 654 | if( parentEntry == null ) 655 | return null; 656 | 657 | entry = new BasketWindowChildEntry( obj ); 658 | parentEntry.Children.Insert( ( parentEntry == targetParentEntry ) ? insertIndex : parentEntry.Children.Count, entry as BasketWindowChildEntry ); 659 | } 660 | 661 | return entry; 662 | } 663 | 664 | private void RemoveObjects(IList instanceIDs) 665 | { 666 | bool removedObjects = false; 667 | foreach (EntityId instanceID in instanceIDs) 668 | { 669 | BasketWindowRootEntry parentEntry; 670 | BasketWindowEntry entry = FindEntryWithInstanceID( instanceID, out parentEntry ); 671 | if( entry is BasketWindowRootEntry ) 672 | removedObjects |= state.Entries.Remove( entry as BasketWindowRootEntry ); 673 | else if( entry is BasketWindowChildEntry ) 674 | removedObjects |= parentEntry.Children.Remove( entry as BasketWindowChildEntry ); 675 | } 676 | 677 | if( removedObjects ) 678 | Reload(); 679 | } 680 | 681 | private void ReorderEntry( T entry, List siblings, ref int newIndex ) where T : BasketWindowEntry 682 | { 683 | int index = siblings.IndexOf( entry ); 684 | if( index < newIndex ) 685 | newIndex--; 686 | 687 | siblings.RemoveAt( index ); 688 | siblings.Insert( newIndex, entry ); 689 | } 690 | 691 | private BasketWindowEntry FindEntryWithInstanceID(EntityId instanceID, out BasketWindowRootEntry parentEntry) 692 | { 693 | foreach( BasketWindowRootEntry entry in state.Entries ) 694 | { 695 | if( entry.InstanceID == instanceID ) 696 | { 697 | parentEntry = null; 698 | return entry; 699 | } 700 | 701 | foreach( BasketWindowChildEntry childEntry in entry.Children ) 702 | { 703 | if( childEntry.InstanceID == instanceID ) 704 | { 705 | parentEntry = entry; 706 | return childEntry; 707 | } 708 | } 709 | } 710 | 711 | /// Entry couldn't be found. Perhaps its has changed because the object was destroyed or restored after the tree was last reloaded. 712 | BasketWindowTreeViewItem item = FindItem( instanceID, rootItem ) as BasketWindowTreeViewItem; 713 | if( item != null ) 714 | { 715 | parentEntry = item.ParentEntry; 716 | return item.Entry; 717 | } 718 | 719 | parentEntry = null; 720 | return null; 721 | } 722 | } 723 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/CustomHierarchyWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEditor; 5 | using UnityEditor.IMGUI.Controls; 6 | using UnityEngine; 7 | using Object = UnityEngine.Object; 8 | #if UNITY_6000_3_OR_NEWER 9 | using EntityId = UnityEngine.EntityId; 10 | #else 11 | using EntityId = System.Int32; 12 | #endif 13 | #if UNITY_6000_3_OR_NEWER 14 | using TreeView = UnityEditor.IMGUI.Controls.TreeView; 15 | using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem; 16 | using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState; 17 | #elif UNITY_6000_2_OR_NEWER 18 | using TreeView = UnityEditor.IMGUI.Controls.TreeView; 19 | using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem; 20 | using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState; 21 | #endif 22 | 23 | namespace InspectPlusNamespace 24 | { 25 | public delegate void HierarchyWindowSelectionChangedDelegate(IList newSelection); 26 | 27 | public class IsolatedHierarchy : ScriptableObject 28 | { 29 | public Transform rootTransform; 30 | 31 | public override bool Equals( object other ) { return this == ( other as Object ) || rootTransform == ( other as Object ); } 32 | public override int GetHashCode() { return rootTransform ? rootTransform.GetHashCode() : base.GetHashCode(); } 33 | } 34 | 35 | [Serializable] 36 | public class CustomHierarchyWindow 37 | { 38 | [SerializeField] 39 | private TreeViewState treeViewState; 40 | [SerializeField] 41 | private Transform rootTransform; 42 | 43 | private CustomHierarchyWindowDrawer treeView; 44 | private SearchField searchField; 45 | private GUIContent createButtonContent; 46 | 47 | public HierarchyWindowSelectionChangedDelegate OnSelectionChanged; 48 | 49 | public void Show( Transform transform ) 50 | { 51 | if( treeView != null && rootTransform == transform ) 52 | { 53 | Refresh(); 54 | return; 55 | } 56 | 57 | if( treeViewState == null || rootTransform != transform ) 58 | treeViewState = new TreeViewState(); 59 | 60 | treeView = new CustomHierarchyWindowDrawer( treeViewState, transform ) 61 | { 62 | OnSelectionChanged = ( newSelection ) => 63 | { 64 | if( OnSelectionChanged != null ) 65 | OnSelectionChanged( newSelection ); 66 | } 67 | }; 68 | 69 | searchField = new SearchField(); 70 | searchField.downOrUpArrowKeyPressed += treeView.SetFocusAndEnsureSelectedItem; 71 | 72 | createButtonContent = new GUIContent( "Create" ); 73 | rootTransform = transform; 74 | } 75 | 76 | public CustomHierarchyWindowDrawer GetTreeView() 77 | { 78 | return treeView; 79 | } 80 | 81 | public void Refresh() 82 | { 83 | if( treeView != null ) 84 | treeView.Reload(); 85 | } 86 | 87 | public void OnGUI() 88 | { 89 | GUILayout.BeginHorizontal( EditorStyles.toolbar ); 90 | Rect rect = GUILayoutUtility.GetRect( createButtonContent, EditorStyles.toolbarDropDown, GUILayout.ExpandWidth( false ) ); 91 | if( EditorGUI.DropdownButton( rect, createButtonContent, FocusType.Passive, EditorStyles.toolbarDropDown ) ) 92 | { 93 | GUIUtility.hotControl = 0; 94 | treeView.ShowContextMenu( null, true ); 95 | } 96 | 97 | GUILayout.Space( 8f ); 98 | treeView.searchString = searchField.OnToolbarGUI( treeView.searchString ); 99 | GUILayout.EndHorizontal(); 100 | 101 | rect = GUILayoutUtility.GetRect( 0, 100000, 0, 100000 ); 102 | treeView.OnGUI( rect ); 103 | } 104 | } 105 | 106 | // Credit: https://docs.unity3d.com/Manual/TreeViewAPI.html (TreeViewExamples.zip) 107 | // Credit: https://github.com/Unity-Technologies/UnityCsReference/blob/2020.2/Editor/Mono/SceneHierarchy.cs 108 | public class CustomHierarchyWindowDrawer : TreeView 109 | { 110 | private class SelectionChangeApplier : IDisposable 111 | { 112 | private readonly CustomHierarchyWindowDrawer hierarchy; 113 | private readonly GameObject[] oldSelection; 114 | 115 | public SelectionChangeApplier( CustomHierarchyWindowDrawer hierarchy ) 116 | { 117 | this.hierarchy = hierarchy; 118 | oldSelection = Selection.gameObjects; 119 | } 120 | 121 | public void Dispose() 122 | { 123 | // Check if Unity's selection has changed 124 | GameObject[] newSelection = Selection.gameObjects; 125 | if( newSelection.Length == oldSelection.Length ) 126 | { 127 | int index; 128 | for( index = 0; index < newSelection.Length; index++ ) 129 | { 130 | if( oldSelection[index] != newSelection[index] ) 131 | break; 132 | } 133 | 134 | if( index == newSelection.Length ) 135 | return; 136 | } 137 | 138 | hierarchy.Reload(); 139 | #if UNITY_6000_3_OR_NEWER 140 | hierarchy.SetSelection(Selection.entityIds, TreeViewSelectionOptions.RevealAndFrame); 141 | #else 142 | hierarchy.SetSelection(Selection.instanceIDs, TreeViewSelectionOptions.RevealAndFrame); 143 | #endif 144 | } 145 | } 146 | 147 | // Credit: https://github.com/Unity-Technologies/UnityCsReference/blob/33cbfe062d795667c39e16777230e790fcd4b28b/Editor/Mono/GUI/TreeView/GameObjectTreeViewGUI.cs#L26-L30 148 | private static class GameObjectStyles 149 | { 150 | public static readonly GUIStyle disabledLabel = new GUIStyle( "PR DisabledLabel" ); 151 | public static readonly GUIStyle prefabLabel = new GUIStyle( "PR PrefabLabel" ); 152 | public static readonly GUIStyle disabledPrefabLabel = new GUIStyle( "PR DisabledPrefabLabel" ); 153 | public static readonly GUIStyle brokenPrefabLabel = new GUIStyle( "PR BrokenPrefabLabel" ); 154 | public static readonly GUIStyle disabledBrokenPrefabLabel = new GUIStyle( "PR DisabledBrokenPrefabLabel" ); 155 | 156 | static GameObjectStyles() 157 | { 158 | disabledLabel.padding.left = 0; 159 | disabledLabel.alignment = TextAnchor.MiddleLeft; 160 | prefabLabel.padding.left = 0; 161 | prefabLabel.alignment = TextAnchor.MiddleLeft; 162 | disabledPrefabLabel.padding.left = 0; 163 | disabledPrefabLabel.alignment = TextAnchor.MiddleLeft; 164 | brokenPrefabLabel.padding.left = 0; 165 | brokenPrefabLabel.alignment = TextAnchor.MiddleLeft; 166 | disabledBrokenPrefabLabel.padding.left = 0; 167 | disabledBrokenPrefabLabel.alignment = TextAnchor.MiddleLeft; 168 | } 169 | } 170 | 171 | private readonly EntityId rootGameObjectID; 172 | private GameObject RootGameObject { get { return GetGameObjectFromInstanceID( rootGameObjectID ); } } 173 | private Transform RootTransform { get { return GetTransformFromInstanceID( rootGameObjectID ); } } 174 | 175 | private readonly List rows = new List( 100 ); 176 | 177 | private bool isSearching; 178 | 179 | public HierarchyWindowSelectionChangedDelegate OnSelectionChanged; 180 | public bool SyncSelection; 181 | 182 | private readonly MethodInfo selectedIconGetter; 183 | 184 | public CustomHierarchyWindowDrawer( TreeViewState state, Transform rootTransform ) : base( state ) 185 | { 186 | rootGameObjectID = rootTransform.gameObject.GetEntityId(); 187 | selectedIconGetter = typeof( EditorUtility ).GetMethod( "GetIconInActiveState", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ); 188 | 189 | Reload(); 190 | } 191 | 192 | protected override TreeViewItem BuildRoot() 193 | { 194 | if( RootGameObject ) 195 | return new TreeViewItem { id = rootGameObjectID, depth = -1 }; 196 | 197 | return new TreeViewItem { id = -1, depth = -1 }; 198 | } 199 | 200 | protected override IList BuildRows( TreeViewItem root ) 201 | { 202 | rows.Clear(); 203 | isSearching = !string.IsNullOrEmpty( searchString ); 204 | 205 | Transform rootTransform = RootTransform; 206 | if( rootTransform && rootTransform.childCount > 0 ) 207 | { 208 | AddChildrenRecursive( rootTransform, 0 ); 209 | 210 | if( isSearching ) 211 | rows.Sort( ( x, y ) => EditorUtility.NaturalCompare( x.displayName, y.displayName ) ); 212 | } 213 | 214 | SetupParentsAndChildrenFromDepths( root, rows ); 215 | return rows; 216 | } 217 | 218 | private void AddChildrenRecursive( Transform parent, int depth ) 219 | { 220 | for( int i = 0, childCount = parent.childCount; i < childCount; i++ ) 221 | { 222 | Transform child = parent.GetChild( i ); 223 | if( !child ) 224 | continue; 225 | 226 | EntityId instanceID = child.gameObject.GetEntityId(); 227 | string displayName = child.name; 228 | TreeViewItem item = null; 229 | if( !isSearching || displayName.ContainsIgnoreCase( searchString ) ) 230 | { 231 | item = new TreeViewItem( instanceID, !isSearching ? depth : 0, displayName ); 232 | item.icon = PrefabUtility.GetIconForGameObject( child.gameObject ); 233 | rows.Add( item ); 234 | } 235 | 236 | if( child.childCount > 0 ) 237 | { 238 | if( isSearching || IsExpanded( instanceID ) ) 239 | AddChildrenRecursive( child, depth + 1 ); 240 | else 241 | item.children = CreateChildListForCollapsedParent(); 242 | } 243 | } 244 | } 245 | 246 | protected override IList GetAncestors(EntityId id) 247 | { 248 | List ancestors = new(); 249 | Transform transform = GetTransformFromInstanceID( id ); 250 | if( !transform ) 251 | return ancestors; 252 | 253 | while( transform.parent ) 254 | { 255 | transform = transform.parent; 256 | ancestors.Add(transform.gameObject.GetEntityId()); 257 | } 258 | 259 | return ancestors; 260 | } 261 | 262 | protected override IList GetDescendantsThatHaveChildren(EntityId id) 263 | { 264 | Transform transform = GetTransformFromInstanceID( id ); 265 | if( !transform ) 266 | return new List(0); 267 | 268 | Stack stack = new Stack(); 269 | stack.Push( transform ); 270 | 271 | List parents = new(); 272 | while( stack.Count > 0 ) 273 | { 274 | Transform current = stack.Pop(); 275 | parents.Add(current.gameObject.GetEntityId()); 276 | for( int i = 0, childCount = current.childCount; i < childCount; i++ ) 277 | { 278 | Transform child = current.GetChild( i ); 279 | if( child ) 280 | stack.Push( child ); 281 | } 282 | } 283 | 284 | return parents; 285 | } 286 | 287 | protected override void RowGUI( RowGUIArgs args ) 288 | { 289 | GameObject go = GetGameObjectFromInstanceID( args.item.id ); 290 | if( !go ) 291 | { 292 | base.RowGUI( args ); 293 | return; 294 | } 295 | 296 | bool goActive = go.activeInHierarchy; 297 | GUIStyle style; 298 | if( goActive ) 299 | { 300 | switch( PrefabUtility.GetPrefabInstanceStatus( go ) ) 301 | { 302 | case PrefabInstanceStatus.MissingAsset: style = GameObjectStyles.brokenPrefabLabel; break; 303 | case PrefabInstanceStatus.Connected: style = GameObjectStyles.prefabLabel; break; 304 | default: style = DefaultStyles.foldoutLabel; break; 305 | } 306 | } 307 | else 308 | { 309 | switch( PrefabUtility.GetPrefabInstanceStatus( go ) ) 310 | { 311 | case PrefabInstanceStatus.MissingAsset: style = GameObjectStyles.disabledBrokenPrefabLabel; break; 312 | case PrefabInstanceStatus.Connected: style = GameObjectStyles.disabledPrefabLabel; break; 313 | default: style = GameObjectStyles.disabledLabel; break; 314 | } 315 | } 316 | 317 | Rect rect = args.rowRect; 318 | rect.x += GetContentIndent( args.item ); 319 | 320 | Texture2D icon = args.item.icon; 321 | if( icon ) 322 | { 323 | Color iconTint = goActive ? Color.white : new Color( 1f, 1f, 1f, 0.5f ); 324 | Rect iconRect = rect; 325 | iconRect.width = 16f; 326 | 327 | if( args.selected && args.focused && selectedIconGetter != null ) 328 | { 329 | icon = selectedIconGetter.Invoke( null, new object[] { icon } ) as Texture2D; 330 | if( !icon ) 331 | icon = args.item.icon; 332 | } 333 | 334 | GUI.DrawTexture( iconRect, icon, ScaleMode.ScaleToFit, true, 0f, iconTint, 0f, 0f ); 335 | 336 | if( PrefabUtility.IsAddedGameObjectOverride( go ) ) 337 | GUI.DrawTexture( iconRect, EditorGUIUtility.IconContent( "PrefabOverlayAdded Icon" ).image, ScaleMode.ScaleToFit, true, 0f, iconTint, 0f, 0f ); 338 | 339 | rect.x += iconRect.width + 2f; 340 | } 341 | 342 | if( Event.current.type == EventType.Repaint ) 343 | style.Draw( rect, args.label, false, false, args.selected, args.focused ); 344 | } 345 | 346 | protected override void SelectionChanged(IList selectedIds) 347 | { 348 | try 349 | { 350 | if( OnSelectionChanged != null ) 351 | OnSelectionChanged( selectedIds ); 352 | } 353 | catch( Exception e ) 354 | { 355 | Debug.LogException( e ); 356 | } 357 | 358 | if( SyncSelection && selectedIds != null ) 359 | Selection.objects = GetSelectedGameObjects(); 360 | } 361 | 362 | private GameObject[] GetSelectedGameObjects() 363 | { 364 | IList selectedIds = GetSelection(); 365 | if( selectedIds == null || selectedIds.Count == 0 ) 366 | return new GameObject[0]; 367 | 368 | selectedIds = SortItemIDsInRowOrder( selectedIds ); 369 | 370 | Transform rootTransform = RootTransform; 371 | if( !rootTransform ) 372 | return new GameObject[0]; 373 | 374 | List gameObjects = new List( selectedIds.Count ); 375 | for( int i = 0; i < selectedIds.Count; i++ ) 376 | { 377 | Transform transform = GetTransformFromInstanceID( selectedIds[i] ); 378 | if( transform && transform != rootTransform && transform.IsChildOf( rootTransform ) ) 379 | gameObjects.Add( transform.gameObject ); 380 | } 381 | 382 | return gameObjects.ToArray(); 383 | } 384 | 385 | protected override bool CanRename( TreeViewItem item ) 386 | { 387 | return true; 388 | } 389 | 390 | protected override void RenameEnded( RenameEndedArgs args ) 391 | { 392 | if( args.acceptedRename && args.newName != args.originalName && args.newName.Trim().Length > 0 ) 393 | { 394 | GameObject selection = (GameObject)Utilities.EntityIdToObject(args.itemID); 395 | Undo.RegisterCompleteObjectUndo( selection, "Rename Transform" ); 396 | selection.name = args.newName; 397 | } 398 | } 399 | 400 | protected override void DoubleClickedItem(EntityId id) 401 | { 402 | Transform transform = GetTransformFromInstanceID( id ); 403 | if( transform && SceneView.lastActiveSceneView ) 404 | { 405 | Selection.activeTransform = transform; 406 | SceneView.lastActiveSceneView.FrameSelected(); 407 | } 408 | } 409 | 410 | protected override void ContextClicked() 411 | { 412 | ShowContextMenu( GetSelectedGameObjects(), false ); 413 | } 414 | 415 | protected override void ContextClickedItem(EntityId id) 416 | { 417 | ShowContextMenu( GetSelectedGameObjects(), false ); 418 | } 419 | 420 | public void ShowContextMenu( GameObject[] selection, bool openedViaCreateButton ) 421 | { 422 | bool hasSelection = !openedViaCreateButton && selection != null && selection.Length > 0; 423 | 424 | if( selection == null || selection.Length == 0 ) 425 | selection = new GameObject[1] { RootGameObject }; 426 | 427 | GenericMenu menu = new GenericMenu(); 428 | 429 | if( !openedViaCreateButton ) 430 | { 431 | if( hasSelection ) 432 | menu.AddItem( new GUIContent( "Copy" ), false, () => CopySelection( selection ) ); 433 | else 434 | menu.AddDisabledItem( new GUIContent( "Copy" ) ); 435 | 436 | menu.AddItem( new GUIContent( "Paste" ), false, () => PasteToSelection( selection ) ); 437 | 438 | menu.AddSeparator( "" ); 439 | 440 | if( hasSelection ) 441 | { 442 | TreeViewItem selectedItem = FindItem(selection[0].GetEntityId(), rootItem); 443 | if( selectedItem != null ) 444 | { 445 | menu.AddItem( new GUIContent( "Rename" ), false, () => BeginRename( selectedItem ) ); 446 | menu.AddItem( new GUIContent( "Duplicate" ), false, () => DuplicateSelection( selection ) ); 447 | menu.AddItem( new GUIContent( "Delete" ), false, () => DeleteSelection( selection ) ); 448 | 449 | menu.AddSeparator( "" ); 450 | 451 | Object prefab = PrefabUtility.GetCorrespondingObjectFromSource(Utilities.EntityIdToObject(selectedItem.id)); 452 | if( prefab ) 453 | { 454 | menu.AddItem( new GUIContent( "Select Prefab" ), false, () => 455 | { 456 | Selection.activeObject = prefab; 457 | EditorGUIUtility.PingObject(prefab); 458 | } ); 459 | 460 | for( int i = 0; i < selection.Length; i++ ) 461 | { 462 | if( selection[i] && PrefabUtility.IsPartOfNonAssetPrefabInstance( selection[i] ) && PrefabUtility.IsOutermostPrefabInstanceRoot( selection[i] ) ) 463 | { 464 | int _i = i; 465 | 466 | menu.AddItem( new GUIContent( "Prefab/Unpack" ), false, () => 467 | { 468 | for( int j = _i; j < selection.Length; j++ ) 469 | { 470 | GameObject go = selection[j]; 471 | if( go && PrefabUtility.IsPartOfNonAssetPrefabInstance( go ) && PrefabUtility.IsOutermostPrefabInstanceRoot( go ) ) 472 | PrefabUtility.UnpackPrefabInstance( go, PrefabUnpackMode.OutermostRoot, InteractionMode.UserAction ); 473 | } 474 | } ); 475 | 476 | menu.AddItem( new GUIContent( "Prefab/Unpack Completely" ), false, () => 477 | { 478 | for( int j = _i; j < selection.Length; j++ ) 479 | { 480 | GameObject go = selection[j]; 481 | if( go && PrefabUtility.IsPartOfNonAssetPrefabInstance( go ) && PrefabUtility.IsOutermostPrefabInstanceRoot( go ) ) 482 | PrefabUtility.UnpackPrefabInstance( go, PrefabUnpackMode.Completely, InteractionMode.UserAction ); 483 | } 484 | } ); 485 | 486 | break; 487 | } 488 | } 489 | 490 | menu.AddSeparator( "" ); 491 | } 492 | } 493 | } 494 | } 495 | 496 | string menusLastItem = (string) typeof( GameObjectUtility ).GetMethod( "GetFirstItemPathAfterGameObjectCreationMenuItems", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ).Invoke( null, null ); 497 | foreach( string path in Unsupported.GetSubmenus( "GameObject" ) ) 498 | { 499 | if( path.Equals( menusLastItem, StringComparison.OrdinalIgnoreCase ) ) 500 | break; 501 | else if( path.Equals( "GameObject/Create Empty Child", StringComparison.OrdinalIgnoreCase ) ) // "Create Empty" does the same thing 502 | continue; 503 | else if( path.Equals( "GameObject/Create Empty Parent", StringComparison.OrdinalIgnoreCase ) ) // Doesn't take context into account, it uses Unity's Selection 504 | continue; 505 | else if( path.IndexOf( "Collapse All", StringComparison.OrdinalIgnoreCase ) >= 0 ) // Collapse functions don't work in this window 506 | continue; 507 | 508 | // Don't include context for Wizards (...) to avoid opening multiple wizards at once 509 | Object[] tempContext = selection; 510 | if( path.EndsWith( "..." ) ) 511 | tempContext = null; 512 | 513 | menu.AddItem( new GUIContent( path.Substring( 11 ) ), false, () => // Substring: remove "GameObject/" prefix 514 | { 515 | using( new SelectionChangeApplier( this ) ) 516 | { 517 | if( tempContext != null ) 518 | typeof( EditorApplication ).GetMethod( "ExecuteMenuItemWithTemporaryContext", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ).Invoke( null, new object[] { path, tempContext } ); 519 | else 520 | EditorApplication.ExecuteMenuItem( path ); 521 | } 522 | } ); 523 | } 524 | 525 | menu.ShowAsContext(); 526 | Event.current.Use(); 527 | } 528 | 529 | protected override void CommandEventHandling() 530 | { 531 | Event e = Event.current; 532 | if( e.type == EventType.ValidateCommand || e.type == EventType.ExecuteCommand ) 533 | { 534 | GameObject[] selection = GetSelectedGameObjects(); 535 | if( selection == null || selection.Length == 0 ) 536 | return; 537 | 538 | if( e.commandName == "Delete" || e.commandName == "SoftDelete" ) 539 | { 540 | if( e.type == EventType.ExecuteCommand ) 541 | DeleteSelection( selection ); 542 | 543 | e.Use(); 544 | return; 545 | } 546 | else if( e.commandName == "Duplicate" ) 547 | { 548 | if( e.type == EventType.ExecuteCommand ) 549 | DuplicateSelection( selection ); 550 | 551 | e.Use(); 552 | return; 553 | } 554 | else if( e.commandName == "Copy" ) 555 | { 556 | if( e.type == EventType.ExecuteCommand ) 557 | CopySelection( selection ); 558 | 559 | e.Use(); 560 | return; 561 | } 562 | else if( e.commandName == "Paste" ) 563 | { 564 | if( e.type == EventType.ExecuteCommand ) 565 | PasteToSelection( selection ); 566 | 567 | e.Use(); 568 | return; 569 | } 570 | } 571 | 572 | base.CommandEventHandling(); 573 | } 574 | 575 | protected override bool CanStartDrag( CanStartDragArgs args ) 576 | { 577 | return true; 578 | } 579 | 580 | protected override void SetupDragAndDrop( SetupDragAndDropArgs args ) 581 | { 582 | DragAndDrop.PrepareStartDrag(); 583 | IList sortedDraggedIDs = SortItemIDsInRowOrder(args.draggedItemIDs); 584 | 585 | List objList = new List( sortedDraggedIDs.Count ); 586 | for( int i = 0; i < sortedDraggedIDs.Count; i++ ) 587 | { 588 | EntityId instanceID = sortedDraggedIDs[i]; 589 | 590 | Object obj = Utilities.EntityIdToObject(instanceID); 591 | if( obj != null ) 592 | objList.Add( obj ); 593 | } 594 | 595 | DragAndDrop.objectReferences = objList.ToArray(); 596 | DragAndDrop.StartDrag( objList.Count > 1 ? "" : objList[0].name ); 597 | } 598 | 599 | protected override DragAndDropVisualMode HandleDragAndDrop( DragAndDropArgs args ) 600 | { 601 | Transform parent = null; 602 | int siblingIndex = 0; 603 | switch( args.dragAndDropPosition ) 604 | { 605 | case DragAndDropPosition.UponItem: 606 | if( args.parentItem != null ) 607 | { 608 | parent = GetTransformFromInstanceID( args.parentItem.id ); 609 | if( parent ) 610 | siblingIndex = parent.childCount; 611 | } 612 | 613 | break; 614 | case DragAndDropPosition.BetweenItems: 615 | if( args.parentItem != null && !hasSearch ) 616 | { 617 | parent = GetTransformFromInstanceID( args.parentItem.id ); 618 | if( parent ) 619 | siblingIndex = Mathf.Min( args.insertAtIndex, parent.childCount ); 620 | } 621 | 622 | break; 623 | case DragAndDropPosition.OutsideItems: 624 | parent = RootTransform; 625 | if( parent ) 626 | siblingIndex = parent.childCount; 627 | 628 | break; 629 | } 630 | 631 | if( hasSearch && ( args.parentItem == rootItem || parent == RootTransform ) ) 632 | return DragAndDropVisualMode.None; 633 | 634 | if( !parent ) 635 | return DragAndDropVisualMode.None; 636 | 637 | Object[] draggedObjects = DragAndDrop.objectReferences; 638 | List draggedTransforms = new List( draggedObjects.Length ); 639 | for( int i = 0; i < draggedObjects.Length; i++ ) 640 | { 641 | GameObject draggedGameObject = draggedObjects[i] as GameObject; 642 | if( draggedGameObject ) 643 | { 644 | // Don't let parent's parents become children of it 645 | if( parent.IsChildOf( draggedGameObject.transform ) ) 646 | return DragAndDropVisualMode.None; 647 | 648 | draggedTransforms.Add( draggedGameObject.transform ); 649 | } 650 | } 651 | 652 | if( draggedTransforms.Count == 0 ) 653 | return DragAndDropVisualMode.None; 654 | 655 | // Remove all Transforms that are children of other Transforms in the list 656 | draggedTransforms.RemoveAll( ( transform ) => 657 | { 658 | while( transform.parent ) 659 | { 660 | transform = transform.parent; 661 | if( draggedTransforms.Contains( transform ) ) 662 | return true; 663 | } 664 | 665 | return false; 666 | } ); 667 | 668 | if( args.performDrop ) 669 | { 670 | List newSelection = new(draggedTransforms.Count); 671 | for( int i = 0; i < draggedTransforms.Count; i++, siblingIndex++ ) 672 | { 673 | Undo.SetTransformParent( draggedTransforms[i], parent, "Object Parenting" ); 674 | draggedTransforms[i].SetSiblingIndex( draggedTransforms[i].GetSiblingIndex() >= siblingIndex ? siblingIndex : ( siblingIndex - 1 ) ); 675 | 676 | newSelection.Add(draggedTransforms[i].gameObject.GetEntityId()); 677 | } 678 | 679 | Reload(); 680 | SetSelection( newSelection, TreeViewSelectionOptions.FireSelectionChanged | TreeViewSelectionOptions.RevealAndFrame ); 681 | } 682 | 683 | return DragAndDropVisualMode.Move; 684 | } 685 | 686 | private void CopySelection( GameObject[] selection ) 687 | { 688 | if( selection != null && selection.Length > 0 ) 689 | { 690 | Selection.objects = selection; 691 | Unsupported.CopyGameObjectsToPasteboard(); 692 | } 693 | } 694 | 695 | private void PasteToSelection( GameObject[] selection ) 696 | { 697 | GameObject tempObject = null; 698 | if( selection == null || selection.Length == 0 || ( selection.Length == 1 && selection[0] == RootGameObject ) ) 699 | { 700 | // We want to paste inside the root object, so we have to select one of its children 701 | Transform rootTransform = RootTransform; 702 | if( rootTransform.childCount > 0 ) 703 | selection = new GameObject[1] { rootTransform.GetChild( 0 ).gameObject }; 704 | else 705 | { 706 | // Create a temporary child object 707 | tempObject = new GameObject( "TEMP" ); 708 | selection = new GameObject[1] { tempObject }; 709 | } 710 | } 711 | 712 | try 713 | { 714 | if( tempObject ) 715 | tempObject.transform.SetParent( RootTransform, false ); 716 | 717 | Selection.objects = selection; 718 | 719 | using( new SelectionChangeApplier( this ) ) 720 | Unsupported.PasteGameObjectsFromPasteboard(); 721 | } 722 | finally 723 | { 724 | if( tempObject ) 725 | Object.DestroyImmediate( tempObject ); 726 | } 727 | } 728 | 729 | private void DuplicateSelection( GameObject[] selection ) 730 | { 731 | if( selection != null && selection.Length > 0 ) 732 | { 733 | Selection.objects = selection; 734 | 735 | using( new SelectionChangeApplier( this ) ) 736 | Unsupported.DuplicateGameObjectsUsingPasteboard(); 737 | } 738 | } 739 | 740 | private void DeleteSelection( GameObject[] selection ) 741 | { 742 | if( selection != null && selection.Length > 0 ) 743 | { 744 | for( int i = 0; i < selection.Length; i++ ) 745 | { 746 | if( selection[i] ) 747 | Undo.DestroyObjectImmediate( selection[i] ); 748 | } 749 | } 750 | } 751 | 752 | private GameObject GetGameObjectFromInstanceID(EntityId instanceID) 753 | { 754 | return Utilities.EntityIdToObject(instanceID) as GameObject; 755 | } 756 | 757 | private Transform GetTransformFromInstanceID(EntityId instanceID) 758 | { 759 | GameObject gameObject = GetGameObjectFromInstanceID(instanceID); 760 | return gameObject ? gameObject.transform : null; 761 | } 762 | } 763 | } -------------------------------------------------------------------------------- /Plugins/InspectPlus/Editor/CustomProjectWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Text; 5 | using UnityEditor; 6 | using UnityEditor.IMGUI.Controls; 7 | using UnityEngine; 8 | #if UNITY_6000_3_OR_NEWER 9 | using EntityId = UnityEngine.EntityId; 10 | #else 11 | using EntityId = System.Int32; 12 | #endif 13 | #if UNITY_6000_3_OR_NEWER 14 | using TreeView = UnityEditor.IMGUI.Controls.TreeView; 15 | using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem; 16 | using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState; 17 | #elif UNITY_6000_2_OR_NEWER 18 | using TreeView = UnityEditor.IMGUI.Controls.TreeView; 19 | using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem; 20 | using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState; 21 | #endif 22 | 23 | namespace InspectPlusNamespace 24 | { 25 | public delegate void ProjectWindowSelectionChangedDelegate(IList newSelection); 26 | 27 | [System.Serializable] 28 | public class CustomProjectWindow 29 | { 30 | [SerializeField] 31 | private TreeViewState treeViewState; 32 | [SerializeField] 33 | private string rootDirectory; 34 | 35 | private CustomProjectWindowDrawer treeView; 36 | private SearchField searchField; 37 | private GUIContent createButtonContent; 38 | 39 | public ProjectWindowSelectionChangedDelegate OnSelectionChanged; 40 | 41 | public void Show( string directory ) 42 | { 43 | if( treeView != null && rootDirectory == directory ) 44 | { 45 | Refresh(); 46 | return; 47 | } 48 | 49 | if( treeViewState == null || rootDirectory != directory ) 50 | treeViewState = new TreeViewState(); 51 | 52 | treeView = new CustomProjectWindowDrawer( treeViewState, directory ) 53 | { 54 | OnSelectionChanged = ( newSelection ) => 55 | { 56 | if( OnSelectionChanged != null ) 57 | OnSelectionChanged( newSelection ); 58 | } 59 | }; 60 | 61 | searchField = new SearchField(); 62 | searchField.downOrUpArrowKeyPressed += treeView.SetFocusAndEnsureSelectedItem; 63 | 64 | createButtonContent = new GUIContent( "Create" ); 65 | rootDirectory = directory; 66 | } 67 | 68 | public CustomProjectWindowDrawer GetTreeView() 69 | { 70 | return treeView; 71 | } 72 | 73 | public void Refresh() 74 | { 75 | if( treeView != null ) 76 | treeView.Reload(); 77 | } 78 | 79 | public void OnGUI() 80 | { 81 | GUILayout.BeginHorizontal( EditorStyles.toolbar ); 82 | Rect rect = GUILayoutUtility.GetRect( createButtonContent, EditorStyles.toolbarDropDown, GUILayout.ExpandWidth( false ) ); 83 | if( EditorGUI.DropdownButton( rect, createButtonContent, FocusType.Passive, EditorStyles.toolbarDropDown ) ) 84 | { 85 | GUIUtility.hotControl = 0; 86 | 87 | treeView.ChangeUnitySelection(); 88 | EditorUtility.DisplayPopupMenu( rect, "Assets/Create", null ); 89 | } 90 | 91 | GUILayout.Space( 8f ); 92 | treeView.searchString = searchField.OnToolbarGUI( treeView.searchString ); 93 | GUILayout.EndHorizontal(); 94 | 95 | rect = GUILayoutUtility.GetRect( 0, 100000, 0, 100000 ); 96 | treeView.OnGUI( rect ); 97 | } 98 | } 99 | 100 | public class CustomProjectWindowDrawer : TreeView 101 | { 102 | private class CacheEntry 103 | { 104 | private Hash128 hash; 105 | 106 | public EntityId[] ChildIDs; 107 | public string[] ChildNames; 108 | public Texture2D[] ChildThumbnails; 109 | 110 | public CacheEntry( string path ) 111 | { 112 | Refresh( path ); 113 | } 114 | 115 | public void Refresh( string path ) 116 | { 117 | Hash128 hash = AssetDatabase.GetAssetDependencyHash( path ); 118 | if( this.hash != hash ) 119 | { 120 | this.hash = hash; 121 | 122 | Object[] childAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath( path ); 123 | ChildIDs = new EntityId[childAssets.Length]; 124 | ChildNames = new string[childAssets.Length]; 125 | ChildThumbnails = new Texture2D[childAssets.Length]; 126 | 127 | for( int i = 0; i < childAssets.Length; i++ ) 128 | { 129 | Object childAsset = childAssets[i]; 130 | 131 | ChildIDs[i] = childAsset.GetEntityId(); 132 | ChildNames[i] = childAsset.name; 133 | ChildThumbnails[i] = AssetPreview.GetMiniThumbnail( childAsset ); 134 | } 135 | } 136 | } 137 | } 138 | 139 | private readonly string rootDirectory; 140 | private readonly List rows = new List( 100 ); 141 | private readonly Dictionary childAssetsCache = new(256); 142 | 143 | private bool isSearching; 144 | 145 | public ProjectWindowSelectionChangedDelegate OnSelectionChanged; 146 | public bool SyncSelection; 147 | 148 | public CustomProjectWindowDrawer( TreeViewState state, string rootDirectory ) : base( state ) 149 | { 150 | this.rootDirectory = rootDirectory; 151 | Reload(); 152 | } 153 | 154 | protected override TreeViewItem BuildRoot() 155 | { 156 | if( AssetDatabase.IsValidFolder( rootDirectory ) ) 157 | return new TreeViewItem { id = GetInstanceIDFromPath( rootDirectory ), depth = -1 }; 158 | 159 | return new TreeViewItem { id = -1, depth = -1 }; 160 | } 161 | 162 | protected override IList BuildRows( TreeViewItem root ) 163 | { 164 | rows.Clear(); 165 | isSearching = !string.IsNullOrEmpty( searchString ); 166 | 167 | string[] entries; 168 | if( FolderHasEntries( rootDirectory, out entries ) ) 169 | { 170 | AddChildrenRecursive( rootDirectory, 0, entries ); 171 | 172 | if( isSearching ) 173 | rows.Sort( ( x, y ) => EditorUtility.NaturalCompare( x.displayName, y.displayName ) ); 174 | } 175 | 176 | SetupParentsAndChildrenFromDepths( root, rows ); 177 | return rows; 178 | } 179 | 180 | private void AddChildrenRecursive( string directory, int depth, string[] entries ) 181 | { 182 | for( int i = 0; i < entries.Length; i++ ) 183 | { 184 | string entry = entries[i]; 185 | if( string.IsNullOrEmpty( entry ) ) 186 | continue; 187 | 188 | EntityId instanceID = GetInstanceIDFromPath(entry); 189 | string displayName = Path.GetFileNameWithoutExtension( entry ); 190 | TreeViewItem item = null; 191 | if( !isSearching || displayName.ContainsIgnoreCase( searchString ) ) 192 | { 193 | item = new TreeViewItem( instanceID, !isSearching ? depth : 0, displayName ) { icon = AssetDatabase.GetCachedIcon( entry ) as Texture2D }; 194 | rows.Add( item ); 195 | } 196 | 197 | if( Directory.Exists( entry ) ) 198 | { 199 | if( isSearching || IsExpanded( instanceID ) ) 200 | { 201 | string[] entries2; 202 | if( FolderHasEntries( entry, out entries2 ) ) 203 | AddChildrenRecursive( entry, depth + 1, entries2 ); 204 | } 205 | else if( FolderHasEntries( entry ) ) 206 | item.children = CreateChildListForCollapsedParent(); 207 | } 208 | else 209 | { 210 | CacheEntry cacheEntry = GetCacheEntry( instanceID, entry ); 211 | EntityId[] childAssets = cacheEntry.ChildIDs; 212 | if( childAssets.Length > 0 ) 213 | { 214 | if( isSearching || IsExpanded( instanceID ) ) 215 | { 216 | string[] childNames = cacheEntry.ChildNames; 217 | Texture2D[] childThumbnails = cacheEntry.ChildThumbnails; 218 | 219 | if( !isSearching ) 220 | { 221 | for( int j = 0; j < childAssets.Length; j++ ) 222 | rows.Add( new TreeViewItem( childAssets[j], depth + 1, childNames[j] ) { icon = childThumbnails[j] } ); 223 | } 224 | else 225 | { 226 | for( int j = 0; j < childAssets.Length; j++ ) 227 | { 228 | if( childNames[j].ContainsIgnoreCase( searchString ) ) 229 | rows.Add( new TreeViewItem( childAssets[j], 0, childNames[j] ) { icon = childThumbnails[j] } ); 230 | } 231 | } 232 | } 233 | else 234 | item.children = CreateChildListForCollapsedParent(); 235 | } 236 | } 237 | } 238 | } 239 | 240 | protected override IList GetAncestors(EntityId id) 241 | { 242 | List ancestors = new(); 243 | string path = AssetDatabase.GetAssetPath( id ); 244 | if( string.IsNullOrEmpty( path ) ) 245 | return ancestors; 246 | 247 | if( !AssetDatabase.IsMainAsset( id ) ) 248 | ancestors.Add( GetInstanceIDFromPath( path ) ); 249 | 250 | while( !string.IsNullOrEmpty( path ) ) 251 | { 252 | path = Path.GetDirectoryName( path ); 253 | if( !StringStartsWithFast( path, rootDirectory ) || !AssetDatabase.IsValidFolder( path ) ) 254 | break; 255 | 256 | ancestors.Add( GetInstanceIDFromPath( path ) ); 257 | } 258 | 259 | return ancestors; 260 | } 261 | 262 | protected override IList GetDescendantsThatHaveChildren(EntityId id) 263 | { 264 | string path = AssetDatabase.GetAssetPath( id ); 265 | if( string.IsNullOrEmpty( path ) ) 266 | return new List(0); 267 | 268 | if( !StringStartsWithFast( path, rootDirectory ) ) 269 | { 270 | if( StringStartsWithFast( rootDirectory, path ) ) 271 | { 272 | path = rootDirectory; 273 | id = rootItem.id; 274 | } 275 | else 276 | return new List(0); 277 | } 278 | 279 | string[] entries; 280 | if( !FolderHasEntries( path, out entries ) ) 281 | { 282 | if( File.Exists( path ) && AssetDatabase.IsMainAsset( id ) ) 283 | { 284 | if( GetCacheEntry( id, path ).ChildIDs.Length > 0 ) 285 | return new List(1) { id }; 286 | } 287 | 288 | return new List(0); 289 | } 290 | 291 | Stack pathsStack = new Stack(); 292 | Stack entriesStack = new Stack(); 293 | 294 | pathsStack.Push( path ); 295 | entriesStack.Push( entries ); 296 | 297 | List parents = new(); 298 | while( pathsStack.Count > 0 ) 299 | { 300 | string current = pathsStack.Pop(); 301 | string[] currentEntries = entriesStack.Pop(); 302 | parents.Add( GetInstanceIDFromPath( current ) ); 303 | 304 | for( int i = 0; i < currentEntries.Length; i++ ) 305 | { 306 | string currentEntry = currentEntries[i]; 307 | 308 | if( string.IsNullOrEmpty( currentEntry ) ) 309 | continue; 310 | 311 | if( FolderHasEntries( currentEntry, out entries ) ) 312 | { 313 | pathsStack.Push( currentEntry ); 314 | entriesStack.Push( entries ); 315 | } 316 | else if( File.Exists( currentEntry ) ) 317 | { 318 | EntityId instanceID = GetInstanceIDFromPath(currentEntry); 319 | if( GetCacheEntry( instanceID, currentEntry ).ChildIDs.Length > 0 ) 320 | parents.Add( instanceID ); 321 | } 322 | } 323 | } 324 | 325 | return parents; 326 | } 327 | 328 | protected override bool CanBeParent( TreeViewItem item ) 329 | { 330 | return AssetDatabase.IsValidFolder( AssetDatabase.GetAssetPath( item.id ) ); 331 | } 332 | 333 | protected override void SelectionChanged(IList selectedIds) 334 | { 335 | try 336 | { 337 | if( OnSelectionChanged != null ) 338 | OnSelectionChanged( selectedIds ); 339 | } 340 | catch( System.Exception e ) 341 | { 342 | Debug.LogException( e ); 343 | } 344 | 345 | if( !SyncSelection || selectedIds == null ) 346 | return; 347 | 348 | EntityId[] selectionArray = new EntityId[selectedIds.Count]; 349 | selectedIds.CopyTo( selectionArray, 0 ); 350 | 351 | #if UNITY_6000_3_OR_NEWER 352 | Selection.entityIds = selectionArray; 353 | #else 354 | Selection.instanceIDs = selectionArray; 355 | #endif 356 | } 357 | 358 | protected override bool CanRename( TreeViewItem item ) 359 | { 360 | return true; 361 | } 362 | 363 | protected override void RenameEnded( RenameEndedArgs args ) 364 | { 365 | if( args.acceptedRename ) 366 | AssetDatabase.RenameAsset( AssetDatabase.GetAssetPath( args.itemID ), args.newName ); 367 | } 368 | 369 | protected override void DoubleClickedItem(EntityId id) 370 | { 371 | Object obj = Utilities.EntityIdToObject(id); 372 | if( obj != null ) 373 | { 374 | if( obj is DefaultAsset && AssetDatabase.IsValidFolder( AssetDatabase.GetAssetPath( obj ) ) ) 375 | SetExpanded( id, true ); 376 | else 377 | AssetDatabase.OpenAsset( obj ); 378 | } 379 | } 380 | 381 | protected override void ContextClicked() 382 | { 383 | Selection.activeObject = AssetDatabase.LoadAssetAtPath( rootDirectory ); 384 | EditorUtility.DisplayPopupMenu( new Rect( Event.current.mousePosition, new Vector2( 0f, 0f ) ), "Assets/", null ); 385 | Event.current.Use(); 386 | } 387 | 388 | protected override void ContextClickedItem(EntityId id) 389 | { 390 | ChangeUnitySelection(); 391 | EditorUtility.DisplayPopupMenu( new Rect( Event.current.mousePosition, new Vector2( 0f, 0f ) ), "Assets/", null ); 392 | Event.current.Use(); 393 | } 394 | 395 | protected override void CommandEventHandling() 396 | { 397 | Event e = Event.current; 398 | if( ( e.type == EventType.ValidateCommand || e.type == EventType.ExecuteCommand ) && HasSelection() ) 399 | { 400 | if( e.commandName == "Delete" || e.commandName == "SoftDelete" ) 401 | { 402 | if( e.type == EventType.ExecuteCommand ) 403 | DeleteAssets( GetSelection(), e.commandName == "SoftDelete" ); 404 | 405 | e.Use(); 406 | return; 407 | } 408 | else if( e.commandName == "Duplicate" ) 409 | { 410 | if( e.type == EventType.ExecuteCommand ) 411 | DuplicateAssets( GetSelection() ); 412 | 413 | e.Use(); 414 | return; 415 | } 416 | } 417 | 418 | base.CommandEventHandling(); 419 | } 420 | 421 | protected override bool CanStartDrag( CanStartDragArgs args ) 422 | { 423 | return true; 424 | } 425 | 426 | protected override void SetupDragAndDrop( SetupDragAndDropArgs args ) 427 | { 428 | DragAndDrop.PrepareStartDrag(); 429 | IList sortedDraggedIDs = SortItemIDsInRowOrder(args.draggedItemIDs); 430 | 431 | List objList = new List( sortedDraggedIDs.Count ); 432 | List paths = new List( sortedDraggedIDs.Count ); 433 | for( int i = 0; i < sortedDraggedIDs.Count; i++ ) 434 | { 435 | EntityId instanceID = sortedDraggedIDs[i]; 436 | 437 | Object obj = Utilities.EntityIdToObject(instanceID); 438 | if( obj != null ) 439 | { 440 | objList.Add( obj ); 441 | 442 | string path = AssetDatabase.GetAssetPath( obj ); 443 | if( !string.IsNullOrEmpty( path ) && paths.IndexOf( path ) < 0 ) 444 | paths.Add( path ); 445 | } 446 | } 447 | 448 | DragAndDrop.objectReferences = objList.ToArray(); 449 | DragAndDrop.paths = paths.ToArray(); 450 | DragAndDrop.StartDrag( objList.Count > 1 ? "" : objList[0].name ); 451 | } 452 | 453 | protected override DragAndDropVisualMode HandleDragAndDrop( DragAndDropArgs args ) 454 | { 455 | string parentFolder = null; 456 | switch( args.dragAndDropPosition ) 457 | { 458 | case DragAndDropPosition.UponItem: 459 | case DragAndDropPosition.BetweenItems: 460 | if( args.parentItem != null && ( !hasSearch || args.dragAndDropPosition == DragAndDropPosition.UponItem ) ) 461 | parentFolder = AssetDatabase.GetAssetPath( args.parentItem.id ); 462 | 463 | break; 464 | case DragAndDropPosition.OutsideItems: 465 | parentFolder = rootDirectory; 466 | break; 467 | } 468 | 469 | if( hasSearch && ( args.parentItem == rootItem || parentFolder == rootDirectory ) ) 470 | return DragAndDropVisualMode.None; 471 | 472 | if( string.IsNullOrEmpty( parentFolder ) || !AssetDatabase.IsValidFolder( parentFolder ) ) 473 | return DragAndDropVisualMode.None; 474 | 475 | if( args.performDrop ) 476 | MoveAssets( DragAndDrop.objectReferences, parentFolder ); 477 | 478 | return DragAndDropVisualMode.Move; 479 | } 480 | 481 | private bool MoveAssets( IList assets, string parentFolder ) 482 | { 483 | bool containsAsset = false; 484 | bool containsSceneObject = false; 485 | List paths = new List( assets.Count ); 486 | List directoryStates = new List( assets.Count ); 487 | for( int i = 0; i < assets.Count; i++ ) 488 | { 489 | string path = AssetDatabase.GetAssetPath( assets[i] ); 490 | 491 | // Can't make a folder a subdirectory of itself 492 | if( path == parentFolder ) 493 | return false; 494 | 495 | if( string.IsNullOrEmpty( path ) ) 496 | containsSceneObject = true; 497 | else if( paths.IndexOf( path ) < 0 ) 498 | { 499 | paths.Add( path ); 500 | directoryStates.Add( Directory.Exists( path ) ); 501 | containsAsset = true; 502 | } 503 | } 504 | 505 | if( containsAsset && containsSceneObject ) 506 | return false; 507 | 508 | if( containsSceneObject ) 509 | { 510 | // Convert all scene objects to Transforms 511 | int invalidObjectCount = 0; 512 | for( int i = 0; i < assets.Count; i++ ) 513 | { 514 | if( assets[i] is GameObject ) 515 | assets[i] = ( (GameObject) assets[i] ).transform; 516 | else if( assets[i] is Component ) 517 | assets[i] = ( (Component) assets[i] ).transform; 518 | else 519 | { 520 | assets[i] = null; 521 | invalidObjectCount++; 522 | } 523 | } 524 | 525 | if( invalidObjectCount == assets.Count ) 526 | return false; 527 | 528 | // Remove child Transforms whose parents are also included in drag&drop 529 | for( int i = assets.Count - 1; i >= 0; i-- ) 530 | { 531 | if( assets[i] == null ) 532 | continue; 533 | 534 | Transform transform = (Transform) assets[i]; 535 | for( int j = 0; j < assets.Count; j++ ) 536 | { 537 | if( i == j || assets[j] == null ) 538 | continue; 539 | 540 | if( transform.IsChildOf( (Transform) assets[j] ) ) 541 | { 542 | assets[i] = null; 543 | break; 544 | } 545 | } 546 | } 547 | 548 | List instanceIDs = new(assets.Count); 549 | AssetDatabase.StartAssetEditing(); 550 | try 551 | { 552 | for( int i = assets.Count - 1; i >= 0; i-- ) 553 | { 554 | if( assets[i] == null ) 555 | continue; 556 | 557 | Transform transform = (Transform) assets[i]; 558 | string path = AssetDatabase.GenerateUniqueAssetPath( Path.Combine( parentFolder, transform.name + ".prefab" ) ); 559 | GameObject prefab = PrefabUtility.SaveAsPrefabAssetAndConnect( transform.gameObject, path, InteractionMode.UserAction ); 560 | if( prefab ) 561 | instanceIDs.Add(prefab.GetEntityId()); 562 | } 563 | } 564 | finally 565 | { 566 | AssetDatabase.StopAssetEditing(); 567 | AssetDatabase.Refresh(); 568 | } 569 | 570 | SetSelection( instanceIDs, TreeViewSelectionOptions.RevealAndFrame ); 571 | return true; 572 | } 573 | 574 | // Remove descendant paths 575 | for( int i = paths.Count - 1; i >= 0; i-- ) 576 | { 577 | string path = paths[i]; 578 | for( int j = 0; j < paths.Count; j++ ) 579 | { 580 | if( i == j || !directoryStates[j] ) 581 | continue; 582 | 583 | if( StringStartsWithFast( path, paths[j] ) ) 584 | { 585 | paths.RemoveAt( i ); 586 | break; 587 | } 588 | } 589 | } 590 | 591 | if( paths.Count == 0 ) 592 | return false; 593 | 594 | string[] entries = Directory.GetFileSystemEntries( parentFolder ); 595 | for( int i = 0; i < entries.Length; i++ ) 596 | { 597 | // Don't allow move if an asset is already located inside parentFolder 598 | if( paths.IndexOf( entries[i].Replace( '\\', '/' ) ) >= 0 ) 599 | return false; 600 | 601 | entries[i] = Path.GetFileName( entries[i] ); 602 | } 603 | 604 | // Check if there are files in parentFolder with conflicting names 605 | string[] newPaths = new string[paths.Count]; 606 | for( int i = 0; i < paths.Count; i++ ) 607 | { 608 | string filename = Path.GetFileName( paths[i] ); 609 | for( int j = 0; j < entries.Length; j++ ) 610 | { 611 | if( filename == entries[j] ) 612 | return false; 613 | } 614 | 615 | newPaths[i] = Path.Combine( parentFolder, filename ); 616 | } 617 | 618 | string error = null; 619 | AssetDatabase.StartAssetEditing(); 620 | try 621 | { 622 | for( int i = 0; i < paths.Count; i++ ) 623 | { 624 | error = AssetDatabase.MoveAsset( paths[i], newPaths[i] ); 625 | if( !string.IsNullOrEmpty( error ) ) 626 | break; 627 | } 628 | } 629 | finally 630 | { 631 | AssetDatabase.StopAssetEditing(); 632 | AssetDatabase.Refresh(); 633 | } 634 | 635 | if( !string.IsNullOrEmpty( error ) ) 636 | { 637 | Debug.LogError( error ); 638 | return false; 639 | } 640 | else 641 | { 642 | EntityId[] instanceIDs = new EntityId[newPaths.Length]; 643 | for( int i = 0; i < newPaths.Length; i++ ) 644 | instanceIDs[i] = GetInstanceIDFromPath( newPaths[i] ); 645 | 646 | SetSelection( instanceIDs, TreeViewSelectionOptions.RevealAndFrame ); 647 | return true; 648 | } 649 | } 650 | 651 | // Credit: https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ProjectWindow/ProjectWindowUtil.cs 652 | private bool DeleteAssets(IList instanceIDs, bool askIfSure) 653 | { 654 | if( instanceIDs.Count == 0 ) 655 | return true; 656 | 657 | if( instanceIDs.IndexOf( GetInstanceIDFromPath( "Assets" ) ) >= 0 ) 658 | { 659 | EditorUtility.DisplayDialog( "Cannot Delete", "Deleting the 'Assets' folder is not allowed", "Ok" ); 660 | return false; 661 | } 662 | 663 | List paths = GetPathsOfMainAssets( instanceIDs ); 664 | if( paths.Count == 0 ) 665 | return false; 666 | 667 | if( askIfSure ) 668 | { 669 | int maxCount = 3; 670 | StringBuilder infotext = new StringBuilder(); 671 | for( int i = 0; i < paths.Count && i < maxCount; ++i ) 672 | infotext.AppendLine( " " + paths[i] ); 673 | 674 | if( paths.Count > maxCount ) 675 | infotext.AppendLine( " ..." ); 676 | 677 | infotext.AppendLine( "You cannot undo this action." ); 678 | 679 | if( !EditorUtility.DisplayDialog( paths.Count > 1 ? "Delete selected assets?" : "Delete selected asset?", infotext.ToString(), "Delete", "Cancel" ) ) 680 | return false; 681 | } 682 | 683 | bool success = true; 684 | AssetDatabase.StartAssetEditing(); 685 | try 686 | { 687 | for( int i = 0; i < paths.Count; i++ ) 688 | { 689 | if( ( File.Exists( paths[i] ) || Directory.Exists( paths[i] ) ) && !AssetDatabase.MoveAssetToTrash( paths[i] ) ) 690 | success = false; 691 | } 692 | } 693 | finally 694 | { 695 | AssetDatabase.StopAssetEditing(); 696 | AssetDatabase.Refresh(); 697 | } 698 | 699 | if( !success ) 700 | { 701 | string message = "Some assets could not be deleted.\n" + 702 | "If you are using Version Control server, make sure you are connected to your VCS or \"Work Offline\" is enabled.\n" + 703 | "Otherwise, make sure nothing is keeping a hook on the deleted assets, like a loaded DLL for example."; 704 | 705 | EditorUtility.DisplayDialog( "Cannot Delete", message, "Ok" ); 706 | } 707 | 708 | return success; 709 | } 710 | 711 | // Credit: https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ProjectWindow/ProjectWindowUtil.cs 712 | private void DuplicateAssets(IList instanceIDs) 713 | { 714 | AssetDatabase.Refresh(); 715 | 716 | List paths = GetPathsOfMainAssets( instanceIDs ); 717 | if( paths.Count == 0 ) 718 | return; 719 | 720 | List copiedPaths = new List( paths.Count ); 721 | AssetDatabase.StartAssetEditing(); 722 | try 723 | { 724 | for( int i = 0; i < paths.Count; i++ ) 725 | { 726 | string newPath = AssetDatabase.GenerateUniqueAssetPath( paths[i] ); 727 | if( !string.IsNullOrEmpty( newPath ) && AssetDatabase.CopyAsset( paths[i], newPath ) ) 728 | copiedPaths.Add( newPath ); 729 | } 730 | } 731 | finally 732 | { 733 | AssetDatabase.StopAssetEditing(); 734 | AssetDatabase.Refresh(); 735 | } 736 | 737 | EntityId[] newInstanceIDs = new EntityId[copiedPaths.Count]; 738 | for( int i = 0; i < copiedPaths.Count; i++ ) 739 | newInstanceIDs[i] = GetInstanceIDFromPath( copiedPaths[i] ); 740 | 741 | SetSelection( newInstanceIDs, TreeViewSelectionOptions.RevealAndFrame ); 742 | } 743 | 744 | public void ChangeUnitySelection() 745 | { 746 | IList selection = GetSelection(); 747 | if( selection.Count == 0 ) 748 | Selection.activeObject = AssetDatabase.LoadAssetAtPath( rootDirectory ); 749 | else 750 | { 751 | EntityId[] selectionArray = new EntityId[selection.Count]; 752 | selection.CopyTo( selectionArray, 0 ); 753 | 754 | #if UNITY_6000_3_OR_NEWER 755 | Selection.entityIds = selectionArray; 756 | #else 757 | Selection.instanceIDs = selectionArray; 758 | #endif 759 | } 760 | } 761 | 762 | private List GetPathsOfMainAssets(IList instanceIDs) 763 | { 764 | List result = new List( instanceIDs.Count ); 765 | for( int i = 0; i < instanceIDs.Count; i++ ) 766 | { 767 | if( AssetDatabase.IsMainAsset( instanceIDs[i] ) ) 768 | result.Add( AssetDatabase.GetAssetPath( instanceIDs[i] ) ); 769 | } 770 | 771 | return result; 772 | } 773 | 774 | private bool FolderHasEntries( string path ) 775 | { 776 | if( !AssetDatabase.IsValidFolder( path ) ) 777 | return false; 778 | 779 | string[] entries = Directory.GetFileSystemEntries( path ); 780 | for( int i = 0; i < entries.Length; i++ ) 781 | { 782 | string entry = entries[i]; 783 | 784 | if( !StringEndsWithFast( entry, ".meta" ) && !string.IsNullOrEmpty( AssetDatabase.AssetPathToGUID( entry ) ) ) 785 | return true; 786 | } 787 | 788 | return false; 789 | } 790 | 791 | private bool FolderHasEntries( string path, out string[] entries ) 792 | { 793 | if( !AssetDatabase.IsValidFolder( path ) ) 794 | { 795 | entries = null; 796 | return false; 797 | } 798 | 799 | bool hasValidEntries = false; 800 | entries = Directory.GetFileSystemEntries( path ); 801 | for( int i = 0, lastFileIndex = -1; i < entries.Length; i++ ) 802 | { 803 | string entry = entries[i]; 804 | 805 | if( !StringEndsWithFast( entry, ".meta" ) && !string.IsNullOrEmpty( AssetDatabase.AssetPathToGUID( entry ) ) ) 806 | hasValidEntries = true; 807 | else 808 | { 809 | entries[i] = null; 810 | continue; 811 | } 812 | 813 | // Sort the entries to ensure that directories come first 814 | if( Directory.Exists( entry ) ) 815 | { 816 | if( lastFileIndex >= 0 ) 817 | { 818 | for( int j = i; j > lastFileIndex; j-- ) 819 | entries[j] = entries[j - 1]; 820 | 821 | entries[lastFileIndex] = entry; 822 | lastFileIndex++; 823 | } 824 | } 825 | else if( lastFileIndex < 0 ) 826 | lastFileIndex = i; 827 | } 828 | 829 | return hasValidEntries; 830 | } 831 | 832 | private EntityId GetInstanceIDFromPath(string path) 833 | { 834 | return AssetDatabase.LoadMainAssetAtPath(path).GetEntityId(); 835 | } 836 | 837 | private CacheEntry GetCacheEntry(EntityId instanceID, string path) 838 | { 839 | CacheEntry cacheEntry; 840 | if( !childAssetsCache.TryGetValue( instanceID, out cacheEntry ) ) 841 | { 842 | cacheEntry = new CacheEntry( path ); 843 | childAssetsCache[instanceID] = cacheEntry; 844 | } 845 | else 846 | cacheEntry.Refresh( path ); 847 | 848 | return cacheEntry; 849 | } 850 | 851 | private bool StringStartsWithFast( string str, string prefix ) 852 | { 853 | int length1 = str.Length; 854 | int length2 = prefix.Length; 855 | int index1 = 0; int index2 = 0; 856 | while( index1 < length1 && index2 < length2 && str[index1] == prefix[index2] ) 857 | { 858 | index1++; 859 | index2++; 860 | } 861 | 862 | return index2 == length2; 863 | } 864 | 865 | private bool StringEndsWithFast( string str, string suffix ) 866 | { 867 | int index1 = str.Length - 1; 868 | int index2 = suffix.Length - 1; 869 | while( index1 >= 0 && index2 >= 0 && str[index1] == suffix[index2] ) 870 | { 871 | index1--; 872 | index2--; 873 | } 874 | 875 | return index2 < 0; 876 | } 877 | } 878 | } --------------------------------------------------------------------------------