Skip to content

Commit 933c6e6

Browse files
authored
Add InspectorFilter for filtering component fields
This script provides a filtering mechanism for Unity's inspector, allowing users to filter fields of components based on a string input. It includes UI elements for inputting filters and highlights matched fields.
1 parent 6a258fe commit 933c6e6

File tree

1 file changed

+289
-0
lines changed

1 file changed

+289
-0
lines changed
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
// filters the fields of all components of a GameObject based on a user-provided string
2+
// matching against both field names and types, with a UI to input the filter and visual highlights for matched fields.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using UnityEditor;
7+
using UnityEngine;
8+
9+
namespace UnityLibrary.EditorTools
10+
{
11+
[InitializeOnLoad]
12+
public static class InspectorFilter
13+
{
14+
private static readonly Dictionary<int, string> FiltersByGameObjectId = new Dictionary<int, string>();
15+
private static readonly Dictionary<int, List<string>> MatchedFieldsByComponentId = new Dictionary<int, List<string>>();
16+
17+
static InspectorFilter()
18+
{
19+
Editor.finishedDefaultHeaderGUI += OnFinishedDefaultHeaderGUI;
20+
Selection.selectionChanged += ApplyFilterForCurrentSelection;
21+
Undo.undoRedoPerformed += ApplyFilterForCurrentSelection;
22+
EditorApplication.delayCall += ApplyFilterForCurrentSelection;
23+
}
24+
25+
internal static bool TryGetFilterForGameObject(GameObject go, out string filter)
26+
{
27+
filter = string.Empty;
28+
if (go == null)
29+
{
30+
return false;
31+
}
32+
33+
if (!FiltersByGameObjectId.TryGetValue(go.GetInstanceID(), out string value) || string.IsNullOrWhiteSpace(value))
34+
{
35+
return false;
36+
}
37+
38+
filter = value.Trim();
39+
return filter.Length > 0;
40+
}
41+
42+
internal static bool IsPropertyMatch(SerializedProperty property, string filter)
43+
{
44+
if (property == null || string.IsNullOrWhiteSpace(filter))
45+
{
46+
return false;
47+
}
48+
49+
if (property.propertyPath == "m_Script")
50+
{
51+
return false;
52+
}
53+
54+
return property.name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0
55+
|| property.displayName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
56+
}
57+
58+
private static void OnFinishedDefaultHeaderGUI(Editor editor)
59+
{
60+
if (editor.target is GameObject go)
61+
{
62+
DrawGameObjectFilterUI(go);
63+
return;
64+
}
65+
66+
if (!(editor.target is Component component))
67+
{
68+
return;
69+
}
70+
71+
int gameObjectId = component.gameObject.GetInstanceID();
72+
if (!FiltersByGameObjectId.TryGetValue(gameObjectId, out string filter) || string.IsNullOrWhiteSpace(filter))
73+
{
74+
return;
75+
}
76+
77+
if (!MatchedFieldsByComponentId.TryGetValue(component.GetInstanceID(), out List<string> matchedFields) || matchedFields.Count == 0)
78+
{
79+
return;
80+
}
81+
82+
Color old = GUI.color;
83+
GUI.color = new Color(1f, 0.95f, 0.55f, 1f);
84+
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
85+
GUI.color = old;
86+
EditorGUILayout.LabelField("Matched fields: " + string.Join(", ", matchedFields), EditorStyles.miniLabel);
87+
EditorGUILayout.EndVertical();
88+
}
89+
90+
private static void DrawGameObjectFilterUI(GameObject go)
91+
{
92+
int id = go.GetInstanceID();
93+
FiltersByGameObjectId.TryGetValue(id, out string currentFilter);
94+
currentFilter ??= string.Empty;
95+
96+
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
97+
98+
const string filterControlName = "InspectorFilter_FilterField";
99+
EditorGUILayout.BeginHorizontal();
100+
EditorGUI.BeginChangeCheck();
101+
GUI.SetNextControlName(filterControlName);
102+
string newFilter = EditorGUILayout.TextField("Filter", currentFilter);
103+
bool clearClicked = GUILayout.Button("x", EditorStyles.miniButton, GUILayout.Width(20f));
104+
bool changed = EditorGUI.EndChangeCheck();
105+
EditorGUILayout.EndHorizontal();
106+
107+
Event e = Event.current;
108+
bool escapePressed = e.type == EventType.KeyDown
109+
&& e.keyCode == KeyCode.Escape
110+
&& GUI.GetNameOfFocusedControl() == filterControlName;
111+
112+
if (clearClicked || escapePressed)
113+
{
114+
newFilter = string.Empty;
115+
changed = true;
116+
GUI.FocusControl(null);
117+
if (escapePressed)
118+
{
119+
e.Use();
120+
}
121+
}
122+
123+
if (changed)
124+
{
125+
if (string.IsNullOrWhiteSpace(newFilter))
126+
{
127+
FiltersByGameObjectId.Remove(id);
128+
newFilter = string.Empty;
129+
}
130+
else
131+
{
132+
FiltersByGameObjectId[id] = newFilter;
133+
}
134+
135+
ApplyFilter(go, newFilter);
136+
ActiveEditorTracker.sharedTracker.ForceRebuild();
137+
}
138+
else
139+
{
140+
ApplyFilter(go, currentFilter);
141+
}
142+
143+
EditorGUILayout.EndVertical();
144+
}
145+
146+
private static void ApplyFilterForCurrentSelection()
147+
{
148+
if (!(Selection.activeGameObject is GameObject go))
149+
{
150+
return;
151+
}
152+
153+
int id = go.GetInstanceID();
154+
FiltersByGameObjectId.TryGetValue(id, out string filter);
155+
ApplyFilter(go, filter);
156+
ActiveEditorTracker.sharedTracker.ForceRebuild();
157+
}
158+
159+
private static void ApplyFilter(GameObject go, string filter)
160+
{
161+
ActiveEditorTracker tracker = ActiveEditorTracker.sharedTracker;
162+
Editor[] editors = tracker.activeEditors;
163+
if (editors == null || editors.Length == 0)
164+
{
165+
return;
166+
}
167+
168+
bool hasFilter = !string.IsNullOrWhiteSpace(filter);
169+
string normalizedFilter = hasFilter ? filter.Trim() : string.Empty;
170+
171+
for (int i = 0; i < editors.Length; i++)
172+
{
173+
UnityEngine.Object target = editors[i].target;
174+
if (!(target is Component component) || component.gameObject != go)
175+
{
176+
tracker.SetVisible(i, 1);
177+
continue;
178+
}
179+
180+
int componentId = component.GetInstanceID();
181+
182+
if (!hasFilter)
183+
{
184+
MatchedFieldsByComponentId.Remove(componentId);
185+
tracker.SetVisible(i, 1);
186+
continue;
187+
}
188+
189+
bool typeMatch = component.GetType().Name.IndexOf(normalizedFilter, StringComparison.OrdinalIgnoreCase) >= 0;
190+
List<string> fieldMatches = GetMatchingSerializedFields(editors[i], normalizedFilter);
191+
bool fieldMatch = fieldMatches.Count > 0;
192+
193+
if (fieldMatch)
194+
{
195+
MatchedFieldsByComponentId[componentId] = fieldMatches;
196+
}
197+
else
198+
{
199+
MatchedFieldsByComponentId.Remove(componentId);
200+
}
201+
202+
tracker.SetVisible(i, (typeMatch || fieldMatch) ? 1 : 0);
203+
}
204+
}
205+
206+
private static List<string> GetMatchingSerializedFields(Editor editor, string filter)
207+
{
208+
List<string> matches = new List<string>();
209+
210+
SerializedObject serializedObject = editor.serializedObject;
211+
if (serializedObject == null)
212+
{
213+
return matches;
214+
}
215+
216+
SerializedProperty iterator = serializedObject.GetIterator();
217+
bool enterChildren = true;
218+
219+
while (iterator.NextVisible(enterChildren))
220+
{
221+
enterChildren = false;
222+
223+
if (!IsPropertyMatch(iterator, filter))
224+
{
225+
continue;
226+
}
227+
228+
string label = iterator.displayName;
229+
if (!matches.Contains(label))
230+
{
231+
matches.Add(label);
232+
}
233+
}
234+
235+
return matches;
236+
}
237+
}
238+
239+
[CustomEditor(typeof(Component), true, isFallback = true)]
240+
[CanEditMultipleObjects]
241+
public class InspectorFilterComponentEditor : Editor
242+
{
243+
private static readonly Color HighlightColor = new Color(0.5058824f, 0.7058824f, 1f, 1f);
244+
245+
public override void OnInspectorGUI()
246+
{
247+
Component component = target as Component;
248+
if (component == null || !InspectorFilter.TryGetFilterForGameObject(component.gameObject, out string filter))
249+
{
250+
DrawDefaultInspector();
251+
return;
252+
}
253+
254+
serializedObject.Update();
255+
256+
SerializedProperty property = serializedObject.GetIterator();
257+
bool enterChildren = true;
258+
259+
while (property.NextVisible(enterChildren))
260+
{
261+
enterChildren = false;
262+
263+
using (new EditorGUI.DisabledScope(property.propertyPath == "m_Script"))
264+
{
265+
float height = EditorGUI.GetPropertyHeight(property, true);
266+
Rect rect = EditorGUILayout.GetControlRect(true, height);
267+
268+
if (InspectorFilter.IsPropertyMatch(property, filter))
269+
{
270+
DrawBorder(rect, HighlightColor, 1f);
271+
}
272+
273+
EditorGUI.PropertyField(rect, property, true);
274+
}
275+
}
276+
277+
serializedObject.ApplyModifiedProperties();
278+
}
279+
280+
private static void DrawBorder(Rect rect, Color color, float thickness)
281+
{
282+
EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, rect.width, thickness), color);
283+
EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMax - thickness, rect.width, thickness), color);
284+
EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, thickness, rect.height), color);
285+
EditorGUI.DrawRect(new Rect(rect.xMax - thickness, rect.yMin, thickness, rect.height), color);
286+
}
287+
}
288+
289+
}

0 commit comments

Comments
 (0)