Skip to content

Commit

Permalink
✨ feat(editor): Prefab Refiner
Browse files Browse the repository at this point in the history
  • Loading branch information
esnya committed May 29, 2022
1 parent 22e6191 commit f9be3ff
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

namespace EsnyaFactory
{
public class PrefabRefinery : EditorWindow
{
[MenuItem("EsnyaTools/Prefab Refinery")]
private static void ShowWindow()
{
GetWindow<PrefabRefinery>().Show();
}

private void OnEnable()
{
titleContent = new GUIContent("Prefab Refinery");

Resources.Load<VisualTreeAsset>("PrefabRefinery").CloneTree(rootVisualElement);
rootVisualElement.Q<Button>("revert-all").clicked += () => {
Refresh(true);
AssetDatabase.Refresh();
Refresh();
};

Refresh();
}

private static UnityEngine.Object GetTargetObject(GameObject prefabInstanceRoot, PropertyModification mod)
{
if (mod.target == null) return null;

if (mod.target is GameObject)
{
return prefabInstanceRoot.GetComponentsInChildren<Transform>(true)
.Select(c => c.gameObject)
.FirstOrDefault(o => PrefabUtility.GetCorrespondingObjectFromSource(o)?.GetInstanceID() == mod.target.GetInstanceID());
}
return prefabInstanceRoot.GetComponentsInChildren(mod.target.GetType(), true)
.FirstOrDefault(c => PrefabUtility.GetCorrespondingObjectFromSource(c)?.GetInstanceID() == mod.target.GetInstanceID());
}

private static FieldInfo GetModificationField(UnityEngine.Object targetObject, string propertyPath)
{
return targetObject?.GetType()?.GetField(propertyPath, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}

private static bool PropertyEquals(UnityEngine.Object targetObject, PropertyModification mod)
{
if (targetObject == null) return false;

var field = GetModificationField(targetObject, mod.propertyPath);
if (field == null) return false; // ToDo: array

return field.GetValue(mod.target) == field.GetValue(targetObject);
}

private static void SetObjectField(ObjectField field, UnityEngine.Object value)
{
if (field == null) return;
field.objectType = value?.GetType() ?? typeof(UnityEngine.Object);
field.value = value;
}

private void OnSelectionChange() => Refresh();

private void Refresh(bool revertAll = false)
{
var modificationList = rootVisualElement.Q<VisualElement>("modifications");
var modificationTemplate = Resources.Load<VisualTreeAsset>("PrefabRefineryModification");

modificationList.Clear();

var modifications = Selection.gameObjects
.Where(PrefabUtility.IsPartOfAnyPrefab)
.Where(PrefabUtility.IsOutermostPrefabInstanceRoot)
.Select(PrefabUtility.GetNearestPrefabInstanceRoot)
.Distinct()
.SelectMany(prefabInstanceRoot =>
(PrefabUtility.GetPropertyModifications(prefabInstanceRoot) ?? Enumerable.Empty<PropertyModification>())
.Select(modification => (modification, targetObject: GetTargetObject(prefabInstanceRoot, modification)))
.Where(t => !PrefabUtility.IsDefaultOverride(t.modification) && PropertyEquals(t.targetObject, t.modification))
.Select(t => (prefabInstanceRoot, t.modification, t.targetObject))
);
foreach (var (prefabInstanceRoot, modification, targetObject) in modifications)
{
var item = modificationTemplate.CloneTree()[0];
SetObjectField(item.Q<ObjectField>("prefab-instance-root"), prefabInstanceRoot);
SetObjectField(item.Q<ObjectField>("target-object"), targetObject);
SetObjectField(item.Q<ObjectField>("target"), modification.target);
item.Q<TextField>("property-path").value = modification.propertyPath;

var field = GetModificationField(targetObject, modification.propertyPath);
if (field?.FieldType?.IsSubclassOf(typeof(UnityEngine.Object)) ?? false)
{
SetObjectField(item.Q<ObjectField>("prefab-object-reference"), field.GetValue(modification.target) as UnityEngine.Object);
SetObjectField(item.Q<ObjectField>("object-reference"), field.GetValue(targetObject) as UnityEngine.Object);
item.Remove(item.Q<TextField>("value"));
item.Remove(item.Q<TextField>("prefab-value"));
}
else
{
item.Q<TextField>("prefab-value").value = field.GetValue(modification.target)?.ToString() ?? "null";
item.Q<TextField>("value").value = field.GetValue(targetObject)?.ToString() ?? "null";
item.Remove(item.Q<ObjectField>("object-reference"));
item.Remove(item.Q<ObjectField>("prefab-object-reference"));
}

void Revert()
{
PrefabUtility.RevertPropertyOverride(new SerializedObject(targetObject).FindProperty(modification.propertyPath), InteractionMode.UserAction);
EditorUtility.SetDirty(targetObject);
}
if (revertAll) Revert();
else
{
item.Q<Button>("revert").clicked += () =>
{
Revert();
AssetDatabase.Refresh();
Refresh();
};
}
modificationList.Add(item);
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
<uie:Toolbar style="justify-content: center;">
<ui:Label text="Prefab" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; -unity-text-align: middle-left; left: 4px;" />
<ui:Label text="Modified Object" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; -unity-text-align: middle-left; left: 4px;" />
<ui:Label text="Original Object" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; -unity-text-align: middle-left; left: 4px;" />
<ui:Label text="Property Path" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; -unity-text-align: middle-left; left: 4px;" />
<ui:Label text="Value" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; -unity-text-align: middle-left; left: 4px;" />
<ui:Label text="Overriden Value" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; -unity-text-align: middle-left; left: 4px;" />
<ui:Button name="revert-all" text="Revert" />
</uie:Toolbar>
<ui:ScrollView style="flex-grow: 1;">
<ui:VisualElement name="modifications" style="flex-grow: 1; flex-shrink: 0; justify-content: flex-end;" />
<ui:Label text="Select Prefab on Hierarchy or Project Window" style="-unity-text-align: middle-center;" />
</ui:ScrollView>
</ui:UXML>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
<ui:VisualElement name="modification" style="min-height: 20px; height: 20px; flex-direction: row;">
<uie:ObjectField name="prefab-instance-root" style="flex-shrink: 1; flex-basis: 0; flex-grow: 1; overflow: hidden;" />
<uie:ObjectField name="target-object" style="flex-shrink: 1; flex-basis: 0; flex-grow: 1; overflow: hidden;" />
<uie:ObjectField name="target" style="flex-shrink: 1; flex-basis: 0; flex-grow: 1; overflow: hidden;" />
<ui:TextField name="property-path" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; overflow: hidden;" />
<ui:TextField name="prefab-value" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; overflow: hidden; width: 53px;" />
<uie:ObjectField name="prefab-object-reference" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; overflow: hidden;" />
<ui:TextField name="value" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; overflow: hidden; width: 53px;" />
<uie:ObjectField name="object-reference" style="flex-shrink: 1; flex-grow: 1; flex-basis: 0; overflow: hidden;" />
<ui:Button text="Revert" name="revert" />
</ui:VisualElement>
</ui:UXML>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f9be3ff

Please sign in to comment.