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,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: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 949cf3c297246c54593be19eac457770
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8484dc58e4e6dec4d8bbec756fa758bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Dirt",
|
||||
"rootNamespace": "",
|
||||
"references": [],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e2a065fa28e72148b0fa49a1061dba9
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,5 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
|
||||
<ui:VisualElement name="InstanceOwnerHierarchy" style="flex-direction: row; align-items: center; margin-top: 10px; margin-bottom: 2px; padding-top: 1px; padding-right: 0; padding-bottom: 1px; padding-left: 0; margin-right: 5px; margin-left: 5px;">
|
||||
<!-- <uie:ObjectField name="InstanceOwner" style="flex-shrink: 1; margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0;" /> -->
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 872d329bdd85298469aecb4acce3d6f2
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,13 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
|
||||
<ui:VisualElement style="flex-direction: row; align-items: center; margin-left: 5px; margin-right: 5px; margin-top: 1px; margin-bottom: 1px;">
|
||||
<uie:ObjectField name="InstanceTarget" style="flex-basis: 260px; margin-top: 0; margin-bottom: 0;" />
|
||||
<ui:Label name="PropertyPath" style="text-overflow: ellipsis; overflow: hidden; margin-top: 0; margin-bottom: 0; flex-grow: 1; flex-shrink: 1; flex-basis: 50%;" />
|
||||
<ui:VisualElement style="flex-direction: row; overflow: hidden; align-items: center; flex-grow: 1; flex-basis: 400px;">
|
||||
<ui:Label name="PrefabValue" text="(nothing)" style="text-overflow: ellipsis; overflow: hidden; margin-top: 0; margin-bottom: 0; -unity-text-align: upper-right; flex-basis: 180px; flex-shrink: 1; flex-grow: 1;" />
|
||||
<uie:ObjectField name="PrefabObjectValue" style="margin-top: 0; margin-bottom: 0; margin-right: 0; margin-left: 0; flex-basis: 180px; flex-shrink: 1; flex-grow: 1;" />
|
||||
<ui:Label text="►" style="color: rgba(255, 255, 255, 0.2); flex-basis: 20px; -unity-text-align: upper-center;" />
|
||||
<ui:Label name="InstanceValue" text="(nothing)" style="text-overflow: ellipsis; overflow: hidden; -unity-font-style: bold; margin-top: 0; margin-bottom: 0; flex-basis: 180px; flex-shrink: 1; flex-grow: 1;" />
|
||||
<uie:ObjectField name="InstanceObjectValue" style="margin-top: 0; margin-bottom: 0; margin-right: 0; margin-left: 0; -unity-font-style: bold; flex-basis: 180px; flex-shrink: 1; flex-grow: 1;" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e96daeb60bacf3d48b0e9e4ccbbe167c
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b13b7b6e39ea0774996f0d40477f4aec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences:
|
||||
- m_ViewDataDictionary: {instanceID: 0}
|
||||
- visualTreeAsset: {fileID: 9197481963319205126, guid: affec65379c5c4d4e934ab8463f489e4, type: 3}
|
||||
- headerAsset: {fileID: 9197481963319205126, guid: 872d329bdd85298469aecb4acce3d6f2, type: 3}
|
||||
- rowAsset: {fileID: 9197481963319205126, guid: e96daeb60bacf3d48b0e9e4ccbbe167c, type: 3}
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
EditorPanelRootElement > VisualElement > TemplateContainer {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
#Content {
|
||||
background-color: #383838;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #292929;
|
||||
}
|
||||
|
||||
#Search #unity-text-input {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#Content #unity-content-container {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
Button.link {
|
||||
align-self: flex-start;
|
||||
margin-top: 2px;
|
||||
margin-right: 0px;
|
||||
margin-left: 273px;
|
||||
margin-bottom: 2px;
|
||||
padding-top: 0px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 1px;
|
||||
background-color: transparent;
|
||||
border-top-width: 0px;
|
||||
border-right-width: 0px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: transparent;
|
||||
border-left-width: 0;
|
||||
color: rgb(105, 205, 255);
|
||||
}
|
||||
|
||||
Button.link:hover {
|
||||
color: rgb(155, 205, 255);
|
||||
border-bottom-color: rgb(155, 205, 255);
|
||||
}
|
||||
|
||||
.row {
|
||||
color: rgb(205, 205, 205);
|
||||
}
|
||||
|
||||
.row:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
.row.unchanged-but-dirty {
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgba(155, 255, 155, .1);
|
||||
}
|
||||
|
||||
.row.unchanged-but-dirty:hover {
|
||||
background-color: rgba(155, 255, 155, .2);
|
||||
}
|
||||
|
||||
.row.excluded {
|
||||
color: rgba(255, 155, 155, .5);
|
||||
}
|
||||
|
||||
.row.excluded:hover {
|
||||
background-color: rgba(255, 205, 205, .05);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ed142f8c193d8544995bb04fd255893
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
@@ -0,0 +1,5 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
|
||||
<Style src="project://database/Assets/Dirt/DirtWindow.uss?fileID=7433441132597879392&guid=0ed142f8c193d8544995bb04fd255893&type=3#DirtWindow" />
|
||||
<ui:TextField placeholder-text="Search" name="Search" style="margin-right: 2px; margin-left: 2px;" />
|
||||
<ui:ScrollView name="Content" style="flex-grow: 1; height: 385px;" />
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: affec65379c5c4d4e934ab8463f489e4
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Dirt
|
||||
{
|
||||
public class DirtWindowSettings : ScriptableObject
|
||||
{
|
||||
public static DirtWindowSettings instance { get; private set; }
|
||||
public static void Load([CallerFilePath] string path = null)
|
||||
{
|
||||
path = Path.Combine("Assets", $"{nameof(DirtWindowSettings)}.asset");
|
||||
|
||||
instance = AssetDatabase.LoadAssetAtPath<DirtWindowSettings>(path);
|
||||
|
||||
if (instance)
|
||||
return;
|
||||
|
||||
instance = CreateInstance<DirtWindowSettings>();
|
||||
AssetDatabase.CreateAsset(instance, path);
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
public Vector2Int modificationLimit = new(3, 10);
|
||||
|
||||
public VisibilityMask visibility
|
||||
= VisibilityMask.ChangedDirtyValues
|
||||
| VisibilityMask.UnchangedDirtyValues;
|
||||
|
||||
[Serializable]
|
||||
public class Exclusion
|
||||
{
|
||||
public GameObject owner;
|
||||
public string targetType;
|
||||
public bool useTargetPath;
|
||||
public string targetPath;
|
||||
public string propertyPath;
|
||||
public bool propertyPathStartsWith;
|
||||
public bool propertyPathRegex;
|
||||
|
||||
public bool Match(Modification modification)
|
||||
{
|
||||
if (owner)
|
||||
{
|
||||
var prefabOwner = modification.prefab.GetOwner();
|
||||
if (prefabOwner && prefabOwner != owner)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetType != string.Empty)
|
||||
{
|
||||
var prefabTarget = modification.prefab.GetTarget();
|
||||
if (prefabTarget)
|
||||
{
|
||||
var prefabType = prefabTarget.GetType().FullName;
|
||||
if (prefabType != targetType)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (useTargetPath)
|
||||
{
|
||||
var prefabTargetPath = modification.prefab.targetPath;
|
||||
if (prefabTargetPath != targetPath)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (propertyPath != string.Empty)
|
||||
{
|
||||
if (propertyPathRegex)
|
||||
{
|
||||
if (!Regex.IsMatch(modification.propertyPath, propertyPath))
|
||||
return false;
|
||||
}
|
||||
else if (propertyPathStartsWith)
|
||||
{
|
||||
if (!modification.propertyPath.StartsWith(propertyPath))
|
||||
return false;
|
||||
|
||||
}
|
||||
else if (modification.propertyPath != propertyPath)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
[SerializeField] List<Exclusion> exclusions = new();
|
||||
|
||||
public void ToggleExclusion(Modification modification)
|
||||
{
|
||||
if (modification.excluded)
|
||||
{
|
||||
modification.excluded = false;
|
||||
exclusions.RemoveAll(x => x.Match(modification));
|
||||
return;
|
||||
}
|
||||
|
||||
var target = modification.prefab.GetTarget();
|
||||
|
||||
modification.excluded = true;
|
||||
exclusions.Add(new()
|
||||
{
|
||||
owner = modification.prefab.GetOwner(),
|
||||
targetType = target ? target.GetType().FullName : string.Empty,
|
||||
useTargetPath = true,
|
||||
targetPath = modification.prefab.targetPath,
|
||||
propertyPath = modification.propertyPath
|
||||
});
|
||||
}
|
||||
|
||||
public bool IsExcluded(Modification modification)
|
||||
{
|
||||
return exclusions.Any(x => x.Match(modification));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b5401099ed7f1049a6f1339094e387e
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Linq;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Dirt
|
||||
{
|
||||
public class Modification
|
||||
{
|
||||
public string propertyPath;
|
||||
|
||||
public ModificationReference instance;
|
||||
public ModificationReference prefab;
|
||||
public bool excluded;
|
||||
bool appliedOrReverted;
|
||||
|
||||
public Modification(GameObject prefabOwner, GameObject instanceOwner, PropertyModification propertyModification)
|
||||
{
|
||||
propertyPath = propertyModification.propertyPath;
|
||||
|
||||
prefab = new(prefabOwner, propertyModification.target);
|
||||
|
||||
var componentIndex = prefab.Setup(this);
|
||||
|
||||
var settings = DirtWindowSettings.instance;
|
||||
|
||||
excluded = settings.IsExcluded(this);
|
||||
|
||||
if (excluded && !settings.visibility.HasFlag(VisibilityMask.Exclusions))
|
||||
return;
|
||||
|
||||
var instanceTransform = instanceOwner.transform;
|
||||
instance = new(instanceOwner, instanceTransform);
|
||||
|
||||
var target = instanceTransform;
|
||||
|
||||
if (prefab.targetPath != string.Empty)
|
||||
target = target.Find(prefab.targetPath);
|
||||
|
||||
if (!target)
|
||||
{
|
||||
target = instanceTransform;
|
||||
var indexed = prefab.targetIndexedPath.ToList();
|
||||
while (target && indexed.Count > 0)
|
||||
{
|
||||
var i = indexed[0];
|
||||
target = i < target.childCount ? target.GetChild(i) : null;
|
||||
indexed.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (target)
|
||||
{
|
||||
|
||||
if (prefab.GetTarget() is GameObject)
|
||||
instance.SetTarget(target.gameObject);
|
||||
else if (componentIndex > -1)
|
||||
{
|
||||
var components = target.GetComponents<Component>();
|
||||
if (componentIndex < components.Length)
|
||||
instance.SetTarget(components[componentIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
instance.Setup(this);
|
||||
}
|
||||
|
||||
public bool IsUnchangedButDirty()
|
||||
{
|
||||
if (appliedOrReverted)
|
||||
return false;
|
||||
|
||||
if (prefab.isNothing && instance.isNothing)
|
||||
return true;
|
||||
|
||||
if (!prefab.hasValue)
|
||||
return false;
|
||||
|
||||
if (!instance.hasValue)
|
||||
return false;
|
||||
|
||||
if (prefab.targetPropertyValue != instance.targetPropertyValue)
|
||||
return false;
|
||||
|
||||
if (prefab.targetPropertyObjectValue != instance.targetPropertyObjectValue)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
appliedOrReverted = true;
|
||||
|
||||
PrefabUtility.ApplyPropertyOverride(
|
||||
instance.GetTargetProperty(this),
|
||||
AssetDatabase.GetAssetPath(prefab.GetOwner()),
|
||||
InteractionMode.UserAction
|
||||
);
|
||||
}
|
||||
|
||||
public void Revert()
|
||||
{
|
||||
appliedOrReverted = true;
|
||||
|
||||
var p = instance.GetTargetProperty(this);
|
||||
if (p is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
PrefabUtility.RevertPropertyOverride(p, InteractionMode.UserAction);
|
||||
}
|
||||
catch (System.ArgumentException ex)
|
||||
{
|
||||
Debug.LogWarning(ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PrefabUtility.RemoveUnusedOverrides(
|
||||
new[] { instance.GetOwner() },
|
||||
InteractionMode.UserAction
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bac8ea813b5bd654590e6733af511c58
|
||||
@@ -0,0 +1,213 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Dirt
|
||||
{
|
||||
public class ModificationReference
|
||||
{
|
||||
GameObject owner;
|
||||
Object target;
|
||||
|
||||
public string targetPath { get; private set; }
|
||||
public List<int> targetIndexedPath { get; private set; }
|
||||
public string targetPropertyValue { get; private set; }
|
||||
public bool hasTargetPropertyObjectValue { get; private set; }
|
||||
public Object targetPropertyObjectValue { get; private set; }
|
||||
public bool isNothing { get; private set; }
|
||||
public bool hasValue { get; private set; }
|
||||
|
||||
public ModificationReference(GameObject owner, Object target)
|
||||
{
|
||||
SetOwner(owner);
|
||||
SetTarget(target);
|
||||
}
|
||||
|
||||
public void SetOwner(GameObject owner)
|
||||
{
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public GameObject GetOwner()
|
||||
{
|
||||
if (!owner)
|
||||
{
|
||||
Debug.LogWarning("Reference has no owner");
|
||||
}
|
||||
|
||||
return owner;
|
||||
}
|
||||
|
||||
public List<Object> GetOwnerHierarchy()
|
||||
{
|
||||
var scenes = AssetDatabase.FindAssets("t:scene")
|
||||
.Select(AssetDatabase.GUIDToAssetPath)
|
||||
.Select(AssetDatabase.LoadAssetAtPath<SceneAsset>)
|
||||
.ToList();
|
||||
|
||||
var hierarchy = new List<Object>();
|
||||
|
||||
var current = GetOwner();
|
||||
|
||||
if (current)
|
||||
{
|
||||
var scene = current.scene;
|
||||
|
||||
hierarchy.Add(current);
|
||||
|
||||
while (current)
|
||||
{
|
||||
var parent = current.transform.parent;
|
||||
|
||||
if (parent)
|
||||
{
|
||||
current = parent.gameObject;
|
||||
hierarchy.Add(current);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (scene.IsValid())
|
||||
{
|
||||
hierarchy.Add(scenes.FirstOrDefault(x => x.name == scene.name));
|
||||
}
|
||||
}
|
||||
|
||||
hierarchy.Reverse();
|
||||
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
public void SetTarget(Object target)
|
||||
{
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public Object GetTarget()
|
||||
{
|
||||
if (!target)
|
||||
{
|
||||
Debug.LogWarning($"{(owner ? $"{owner.name}: " : "")}Reference has no target");
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
public SerializedProperty GetTargetProperty(Modification modification)
|
||||
{
|
||||
var target = GetTarget();
|
||||
if (!target)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var prefabObject = new SerializedObject(target);
|
||||
return prefabObject.FindProperty(modification.propertyPath);
|
||||
}
|
||||
|
||||
public int Setup(Modification modification)
|
||||
{
|
||||
var componentIndex = -1;
|
||||
|
||||
var owner = GetOwner();
|
||||
var target = GetTarget();
|
||||
|
||||
if (owner && target)
|
||||
{
|
||||
var parent = target is Transform transform
|
||||
? transform
|
||||
: target is GameObject gameObject
|
||||
? gameObject.transform
|
||||
: (target as Component).transform;
|
||||
|
||||
var components = parent.GetComponents<Component>();
|
||||
componentIndex = ArrayUtility.IndexOf(components, target);
|
||||
|
||||
var path = new List<string>();
|
||||
targetIndexedPath = new List<int>();
|
||||
|
||||
while (parent && parent != owner.transform)
|
||||
{
|
||||
path.Insert(0, parent.name);
|
||||
targetIndexedPath.Insert(0, parent.GetSiblingIndex());
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
targetPath = string.Join("/", path);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetPath = string.Empty;
|
||||
}
|
||||
|
||||
var p = GetTargetProperty(modification);
|
||||
string v;
|
||||
|
||||
if (p is null)
|
||||
{
|
||||
isNothing = true;
|
||||
v = "<color=#777777>(nothing)</color>";
|
||||
}
|
||||
else
|
||||
{
|
||||
hasValue = true;
|
||||
|
||||
switch (p.type)
|
||||
{
|
||||
case "bool":
|
||||
v = $"<color=#9999ff>{p.boolValue}</color>";
|
||||
break;
|
||||
|
||||
case "short":
|
||||
case "int":
|
||||
case "uint":
|
||||
v = $"<color=#33ff33>{p.intValue}</color>";
|
||||
break;
|
||||
|
||||
case "float":
|
||||
v = $"<color=#99ff99>{p.floatValue:0.00}</color>";
|
||||
break;
|
||||
|
||||
case "string":
|
||||
v = p.stringValue == string.Empty
|
||||
? $"<color=#cc9900>(empty)</color>"
|
||||
: $"<color=#ffcc66>{p.stringValue}</color>";
|
||||
break;
|
||||
|
||||
case "Enum":
|
||||
if (0 <= p.enumValueIndex && p.enumValueIndex < p.enumNames.Length)
|
||||
v = $"<color=#ffff00>{p.enumNames[p.enumValueIndex]}</color>";
|
||||
else
|
||||
v = $"<color=#ffff00>[type Enum] {p.enumValueIndex}</color>";
|
||||
break;
|
||||
|
||||
case "ArraySize":
|
||||
v = p.intValue.ToString();
|
||||
break;
|
||||
|
||||
default:
|
||||
if (Regex.IsMatch(p.type, @"^PPtr\<.+\>$"))
|
||||
{
|
||||
hasTargetPropertyObjectValue = true;
|
||||
targetPropertyObjectValue = p.objectReferenceValue;
|
||||
v = string.Empty;
|
||||
break;
|
||||
}
|
||||
|
||||
v = $"[type {p.type}]";
|
||||
v = $"<color=#999999>{v}</color>";
|
||||
hasValue = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
targetPropertyValue = v;
|
||||
|
||||
return componentIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 006baea4fd256d548aab072abfa194a8
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Dirt
|
||||
{
|
||||
[Flags]
|
||||
public enum VisibilityMask
|
||||
{
|
||||
ChangedDirtyValues = 1,
|
||||
UnchangedDirtyValues = 2,
|
||||
Exclusions = 4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6f1c54ec55190448b9feaebea2ee3e7
|
||||
Reference in new issue
Block a user