Add "Dirt" plugin
This commit is contained in:
1 parent
c2d535c661
commit
8567b9d8ef
23 files changed
+1202
No files matched your search
@@ -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<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user