Files
chameleon/Assets/Scripts/Editor/Dirt/DirtWindow.cs
T
2026-07-01 13:07:16 +02:00

518 lines
16 KiB
C#

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<DirtWindow>("Dirt", typeof(SceneView));
string activePath;
int refreshCountdown;
TextField searchField;
ScrollView content;
readonly List<List<Modification>> cache = new();
readonly List<Modification> visibleModifications = new();
int cacheIndexGUI;
public void CreateGUI()
{
DirtWindowSettings.Load();
if (!visualTreeAsset)
{
return;
}
var templateContainer = visualTreeAsset.Instantiate();
rootVisualElement.Add(templateContainer);
templateContainer.StretchToParentSize();
searchField = rootVisualElement.Q<TextField>("Search");
searchField.RegisterValueChangedCallback(e => refreshCountdown = 3);
content = rootVisualElement.Q<ScrollView>("Content");
content.RegisterCallback<MouseDownEvent>(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<GameObject> FindInstanceOwners(PrefabStage prefabStage)
{
return (
prefabStage
? prefabStage
.FindComponentsOfType<Transform>()
.Where(x => x.hideFlags != HideFlags.HideAndDontSave)
.Select(x => x.gameObject)
: FindObjectsByType<GameObject>(FindObjectsInactive.Include, FindObjectsSortMode.InstanceID)
.AsEnumerable()
).ToList();
}
void RefreshCache(List<GameObject> 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<VisualElement>("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<MouseDownEvent>(e => OnMouseDown(e, row, modification));
row.Q<ObjectField>("InstanceTarget").value = modification.instance.GetTarget();
foreach (var (name, reference) in new[] {
("Prefab", modification.prefab),
("Instance", modification.instance)
})
{
var label = row.Q<Label>($"{name}Value");
var objectField = row.Q<ObjectField>($"{name}ObjectValue");
if (reference.hasTargetPropertyObjectValue)
{
label.RemoveFromHierarchy();
objectField.value = reference.targetPropertyObjectValue;
}
else
{
label.text = reference.targetPropertyValue;
objectField.RemoveFromHierarchy();
}
}
var propertyPathLabel = row.Q<Label>("PropertyPath");
propertyPathLabel.text = modification.propertyPath;
RefreshRowAppeareance(row, modification);
return row;
}
void RefreshRowAppeareance(VisualElement row, Modification modification)
{
if (modification.excluded)
{
row.AddToClassList("excluded");
}
else
{
row.RemoveFromClassList("excluded");
}
if (modification.IsUnchangedButDirty())
{
row.AddToClassList("unchanged-but-dirty");
}
else
{
row.RemoveFromClassList("unchanged-but-dirty");
}
}
void OnMouseDown(MouseDownEvent e, VisualElement row = null, Modification modification = null)
{
if (e.button != 1)
{
return;
}
var settings = DirtWindowSettings.instance;
if (row is not null)
{
e.StopPropagation();
}
var menu = new GenericMenu();
if (row is not null)
{
menu.AddItem(
content: new("Revert"),
on: false,
func: x =>
{
(x as Modification).Revert();
row.SetEnabled(false);
},
userData: modification
);
}
var modifications = visibleModifications
.Where(x => x.IsUnchangedButDirty())
.ToList();
if (modifications.Count > 0)
{
menu.AddItem(new($"Revert Unchanged Values ({modifications.Count})"), false, () =>
{
if (
EditorUtility.DisplayDialog(
"Warning",
$"Do you really want to revert {modifications.Count} values?",
"Yes",
"No"
)
)
{
foreach (var modification in modifications)
{
modification.Revert();
}
refreshCountdown = 1;
}
});
}
if (row is not null)
{
menu.AddItem(
content: new("Apply"),
on: false,
func: x =>
{
(x as Modification).Apply();
row.SetEnabled(false);
},
userData: modification
);
if (modification.excluded)
{
menu.AddDisabledItem(
content: new("Excluded"),
on: true
);
}
else
{
menu.AddItem(
content: new("Exclude"),
on: false,
func: x =>
{
var m = x as Modification;
settings.ToggleExclusion(m);
RefreshRowAppeareance(row, m);
},
userData: modification
);
}
}
if (row is not null || modifications.Count > 0)
{
menu.AddSeparator("");
}
menu.AddItem(
content: new("Refresh"),
on: false,
func: () => refreshCountdown = 1
);
menu.AddSeparator("");
foreach (var (x, y) in new[] {
(VisibilityMask.ChangedDirtyValues, "Changed Dirty Values"),
(VisibilityMask.UnchangedDirtyValues, "Unchanged Dirty Values"),
(VisibilityMask.Exclusions, "Exclusions")
})
{
var on = settings.visibility.HasFlag(x);
menu.AddItem(
content: new($"Show {y}"),
on,
func: () =>
{
if (on)
{
settings.visibility ^= x;
}
else
{
settings.visibility |= x;
}
refreshCountdown = 1;
}
);
}
menu.AddSeparator("");
menu.AddItem(
content: new("Open Settings"),
on: false,
func: () =>
{
var activeObject = Selection.activeObject;
Selection.activeObject = settings;
EditorApplication.ExecuteMenuItem("Assets/Properties...");
Selection.activeObject = activeObject;
}
);
menu.ShowAsContext();
}
}
}