diff --git a/Assets/DirtWindowSettings.asset b/Assets/DirtWindowSettings.asset new file mode 100644 index 0000000..3abef6f --- /dev/null +++ b/Assets/DirtWindowSettings.asset @@ -0,0 +1,17 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3b5401099ed7f1049a6f1339094e387e, type: 3} + m_Name: DirtWindowSettings + m_EditorClassIdentifier: Dirt::Dirt.DirtWindowSettings + modificationLimit: {x: 3, y: 10} + visibility: 3 + exclusions: [] diff --git a/Assets/DirtWindowSettings.asset.meta b/Assets/DirtWindowSettings.asset.meta new file mode 100644 index 0000000..8c38324 --- /dev/null +++ b/Assets/DirtWindowSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 949cf3c297246c54593be19eac457770 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/Dirt.meta b/Assets/Scripts/Editor/Dirt.meta new file mode 100644 index 0000000..8ed8e5e --- /dev/null +++ b/Assets/Scripts/Editor/Dirt.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8484dc58e4e6dec4d8bbec756fa758bb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/Dirt/Dirt.asmdef b/Assets/Scripts/Editor/Dirt/Dirt.asmdef new file mode 100644 index 0000000..bcc0c9e --- /dev/null +++ b/Assets/Scripts/Editor/Dirt/Dirt.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Dirt", + "rootNamespace": "", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/Editor/Dirt/Dirt.asmdef.meta b/Assets/Scripts/Editor/Dirt/Dirt.asmdef.meta new file mode 100644 index 0000000..6fcee6c --- /dev/null +++ b/Assets/Scripts/Editor/Dirt/Dirt.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4e2a065fa28e72148b0fa49a1061dba9 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/Dirt/DirtHeader.uxml b/Assets/Scripts/Editor/Dirt/DirtHeader.uxml new file mode 100644 index 0000000..8511b6b --- /dev/null +++ b/Assets/Scripts/Editor/Dirt/DirtHeader.uxml @@ -0,0 +1,5 @@ + + + + + diff --git a/Assets/Scripts/Editor/Dirt/DirtHeader.uxml.meta b/Assets/Scripts/Editor/Dirt/DirtHeader.uxml.meta new file mode 100644 index 0000000..1218dca --- /dev/null +++ b/Assets/Scripts/Editor/Dirt/DirtHeader.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 872d329bdd85298469aecb4acce3d6f2 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Assets/Scripts/Editor/Dirt/DirtRow.uxml b/Assets/Scripts/Editor/Dirt/DirtRow.uxml new file mode 100644 index 0000000..82d268a --- /dev/null +++ b/Assets/Scripts/Editor/Dirt/DirtRow.uxml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/Assets/Scripts/Editor/Dirt/DirtRow.uxml.meta b/Assets/Scripts/Editor/Dirt/DirtRow.uxml.meta new file mode 100644 index 0000000..96ad530 --- /dev/null +++ b/Assets/Scripts/Editor/Dirt/DirtRow.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: e96daeb60bacf3d48b0e9e4ccbbe167c +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Assets/Scripts/Editor/Dirt/DirtWindow.cs b/Assets/Scripts/Editor/Dirt/DirtWindow.cs new file mode 100644 index 0000000..b12d77d --- /dev/null +++ b/Assets/Scripts/Editor/Dirt/DirtWindow.cs @@ -0,0 +1,517 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEditor.UIElements; + +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.UIElements; + +namespace Dirt +{ + public class DirtWindow : EditorWindow + { + [SerializeField] VisualTreeAsset visualTreeAsset; + [SerializeField] VisualTreeAsset headerAsset; + [SerializeField] VisualTreeAsset rowAsset; + + [MenuItem("Tools/Dirt")] + public static void Open() => GetWindow("Dirt", typeof(SceneView)); + + string activePath; + int refreshCountdown; + + TextField searchField; + ScrollView content; + + readonly List> cache = new(); + readonly List visibleModifications = new(); + + int cacheIndexGUI; + + public void CreateGUI() + { + DirtWindowSettings.Load(); + + if (!visualTreeAsset) + { + return; + } + + var templateContainer = visualTreeAsset.Instantiate(); + rootVisualElement.Add(templateContainer); + templateContainer.StretchToParentSize(); + + searchField = rootVisualElement.Q("Search"); + searchField.RegisterValueChangedCallback(e => refreshCountdown = 3); + + content = rootVisualElement.Q("Content"); + content.RegisterCallback(e => OnMouseDown(e)); + + refreshCountdown = 2; + } + + void Update() + { + var text = "Dirt"; + + if (cache.Count > 0) + { + var progress = Mathf.RoundToInt( + (float) cacheIndexGUI / cache.Count * 100 + ); + + if (progress < 100) + { + text += $" - {progress}%"; + } + } + + if (text == "Dirt" && titleContent.text != text) + { + Repaint(); + } + + titleContent.text = text; + + if (refreshCountdown > 0) + { + return; + } + + UpdateGUI(); + } + + void OnInspectorUpdate() + { + var activePath = GetActivePath(out _); + + if (this.activePath != activePath) + { + this.activePath = activePath; + refreshCountdown += 2; + } + + if (refreshCountdown != 0) + { + if (refreshCountdown == 1) + { + this.activePath = GetActivePath(out var prefabStage); + var instanceOwners = FindInstanceOwners(prefabStage); + RefreshCache(instanceOwners); + ClearGUI(); + } + + refreshCountdown--; + } + } + + static string GetActivePath(out PrefabStage prefabStage) + { + prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); + + if (prefabStage) + { + return prefabStage.assetPath; + } + else + { + return string.Join(";", Enumerable + .Range(0, SceneManager.sceneCount) + .Select(i => SceneManager.GetSceneAt(i).path) + ); + } + } + + static List FindInstanceOwners(PrefabStage prefabStage) + { + return ( + prefabStage + ? prefabStage + .FindComponentsOfType() + .Where(x => x.hideFlags != HideFlags.HideAndDontSave) + .Select(x => x.gameObject) + : FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.InstanceID) + .AsEnumerable() + ).ToList(); + } + + void RefreshCache(List instanceOwners) + { + cache.Clear(); + + foreach (var instanceOwner in instanceOwners.Where(PrefabUtility.IsOutermostPrefabInstanceRoot)) + { + var prefabOwner = PrefabUtility.GetCorrespondingObjectFromSource(instanceOwner); + + cache.Add( + PrefabUtility.GetPropertyModifications(instanceOwner) + .Select(x => new Modification(prefabOwner, instanceOwner, x)) + .Where(x => x.instance is not null) + .ToList() + ); + } + } + + void ClearGUI() + { + content.Clear(); + visibleModifications.Clear(); + cacheIndexGUI = 0; + } + + void UpdateGUI() + { + if (cacheIndexGUI >= cache.Count) + { + return; + } + + var settings = DirtWindowSettings.instance; + + foreach (var modifications in cache.Skip(cacheIndexGUI)) + { + cacheIndexGUI++; + + var filteredModifications = modifications.Where(x => + { + var s = searchField.value; + if (s == string.Empty) + { + return true; + } + + var i = StringComparison.InvariantCultureIgnoreCase; + + foreach (var value in x.instance.GetOwnerHierarchy()) + { + if (value.name.Contains(s, i)) + { + return true; + } + } + + var t = x.instance.GetTarget(); + if (t && (t.name.Contains(s, i) || t.GetType().Name.Contains(s, i))) + { + return true; + } + + if (x.propertyPath.Contains(s, i)) + { + return true; + } + + if (x.prefab.targetPropertyValue.Contains(s, i)) + { + return true; + } + + if (x.instance.targetPropertyValue.Contains(s, i)) + { + return true; + } + + return false; + + }).Where(x => x.IsUnchangedButDirty() + ? settings.visibility.HasFlag(VisibilityMask.UnchangedDirtyValues) + : settings.visibility.HasFlag(VisibilityMask.ChangedDirtyValues) + ).ToList(); + + if (filteredModifications.Count == 0) + { + continue; + } + + visibleModifications.AddRange(filteredModifications); + + var header = headerAsset.Instantiate(); + content.Add(header); + + var instanceOwnerHierarchyContainer = header.Q("InstanceOwnerHierarchy"); + + var fields = filteredModifications + .First() + .instance + .GetOwnerHierarchy() + .Select(x => new ObjectField + { + value = x, + style = { + flexShrink = 1, + marginTop = 10, + marginRight = 0, + marginBottom = 0, + marginLeft = 0 + } + }); + + foreach (var field in fields) + { + if (instanceOwnerHierarchyContainer.childCount > 0) + { + instanceOwnerHierarchyContainer.Add(new Label + { + text = "/", + style = { + marginTop = 10, + marginRight = 2, + marginBottom = 0, + marginLeft = 2 + } + }); + } + instanceOwnerHierarchyContainer.Add(field); + } + + var rowContent = new VisualElement(); + content.Add(rowContent); + + if (settings.modificationLimit.y > 0 && filteredModifications.Count <= settings.modificationLimit.y) + { + foreach (var m in filteredModifications) + { + rowContent.Add(CreateRow(m)); + } + break; + } + + foreach (var m in filteredModifications.Take(settings.modificationLimit.x)) + { + rowContent.Add(CreateRow(m)); + } + + var moreModifications = filteredModifications + .Skip(settings.modificationLimit.x) + .ToList(); + + var button = new Button + { + text = $"Show More... ({moreModifications.Count})" + }; + + button.AddToClassList("link"); + button.clicked += () => + { + button.RemoveFromHierarchy(); + foreach (var m in moreModifications) + { + rowContent.Add(CreateRow(m)); + } + }; + + rowContent.Add(button); + + break; + } + } + + VisualElement CreateRow(Modification modification) + { + var row = rowAsset.Instantiate(); + row.AddToClassList("row"); + + row.RegisterCallback(e => OnMouseDown(e, row, modification)); + row.Q("InstanceTarget").value = modification.instance.GetTarget(); + + foreach (var (name, reference) in new[] { + ("Prefab", modification.prefab), + ("Instance", modification.instance) + }) + { + var label = row.Q