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