575 lines
22 KiB
C#
575 lines
22 KiB
C#
using UnityEditor;
|
|
using UnityEngine;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEditor.IMGUI.Controls;
|
|
|
|
namespace Editor.Tools
|
|
{
|
|
public class ComponentSearchReplaceWindow : EditorWindow
|
|
{
|
|
private Type selectedSearchType;
|
|
private Type selectedReplaceType;
|
|
private List<ComponentInfo> foundComponents = new List<ComponentInfo>();
|
|
private Vector2 scrollPos;
|
|
private int selectedComponentIndex = -1;
|
|
private string searchTypeName = "Select a Component...";
|
|
private string replaceTypeName = "Select a Component...";
|
|
private List<Type> allMonoBehaviourTypes = new List<Type>();
|
|
private bool includeDerivedTypes = true;
|
|
|
|
[MenuItem("Tools/Component Search & Replace")]
|
|
public static void ShowWindow()
|
|
{
|
|
var window = GetWindow<ComponentSearchReplaceWindow>("Component Search & Replace");
|
|
window.minSize = new Vector2(700, 500);
|
|
window.maxSize = new Vector2(1400, 1000);
|
|
window.position = new Rect(120, 120, 800, 600);
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
CacheMonoBehaviourTypes();
|
|
EditorSceneManager.sceneOpened += (scene, mode) => ClearSearch();
|
|
SceneManager.activeSceneChanged += (oldScene, newScene) => ClearSearch();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
EditorSceneManager.sceneOpened -= (scene, mode) => ClearSearch();
|
|
SceneManager.activeSceneChanged -= (oldScene, newScene) => ClearSearch();
|
|
}
|
|
|
|
private void CacheMonoBehaviourTypes()
|
|
{
|
|
allMonoBehaviourTypes.Clear();
|
|
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
|
|
|
foreach (var assembly in assemblies)
|
|
{
|
|
try
|
|
{
|
|
var types = assembly.GetTypes()
|
|
.Where(t => (t.IsSubclassOf(typeof(MonoBehaviour)) || t.IsSubclassOf(typeof(Component)))
|
|
&& !t.IsAbstract
|
|
&& t != typeof(Transform)) // Exclude Transform as it can't be removed
|
|
.OrderBy(t => t.Name);
|
|
allMonoBehaviourTypes.AddRange(types);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Some assemblies may throw exceptions when getting types
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ClearSearch()
|
|
{
|
|
foundComponents.Clear();
|
|
selectedComponentIndex = -1;
|
|
Repaint();
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
GUILayout.Space(10);
|
|
|
|
// Search Section
|
|
EditorGUILayout.BeginVertical("box");
|
|
EditorGUILayout.LabelField("Search for Component", EditorStyles.boldLabel);
|
|
GUILayout.Space(5);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField("Component Type:", GUILayout.Width(120));
|
|
|
|
if (GUILayout.Button(searchTypeName, EditorStyles.popup, GUILayout.ExpandWidth(true)))
|
|
{
|
|
var dropdown = new ComponentTypeDropdown(
|
|
new AdvancedDropdownState(),
|
|
(type) =>
|
|
{
|
|
selectedSearchType = type;
|
|
searchTypeName = type != null ? type.Name : "Select a Component...";
|
|
ClearSearch();
|
|
Repaint();
|
|
});
|
|
dropdown.Show(GUILayoutUtility.GetLastRect());
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
GUILayout.Space(5);
|
|
|
|
// Include Derived Types checkbox
|
|
includeDerivedTypes = EditorGUILayout.Toggle(
|
|
new GUIContent("Include Derived Types",
|
|
"When enabled, searches for the selected type and all types that inherit from it. " +
|
|
"When disabled, searches only for the exact type."),
|
|
includeDerivedTypes);
|
|
|
|
GUILayout.Space(5);
|
|
|
|
EditorGUI.BeginDisabledGroup(selectedSearchType == null);
|
|
if (GUILayout.Button("Search Scene", GUILayout.Height(30)))
|
|
{
|
|
SearchForComponents();
|
|
}
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
GUILayout.Space(10);
|
|
|
|
// Results Section
|
|
EditorGUILayout.BeginVertical("box");
|
|
EditorGUILayout.LabelField($"Search Results ({foundComponents.Count} found)", EditorStyles.boldLabel);
|
|
GUILayout.Space(5);
|
|
|
|
scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.ExpandHeight(true));
|
|
|
|
if (foundComponents.Count == 0)
|
|
{
|
|
EditorGUILayout.HelpBox("No components found. Select a component type and click 'Search Scene'.", MessageType.Info);
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < foundComponents.Count; i++)
|
|
{
|
|
var info = foundComponents[i];
|
|
if (info.component == null) continue;
|
|
|
|
bool isSelected = selectedComponentIndex == i;
|
|
Color originalColor = GUI.backgroundColor;
|
|
|
|
if (isSelected)
|
|
{
|
|
GUI.backgroundColor = new Color(0.3f, 0.6f, 1f, 0.5f);
|
|
}
|
|
|
|
EditorGUILayout.BeginVertical("box");
|
|
GUI.backgroundColor = originalColor;
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
// Selection toggle
|
|
bool newSelected = GUILayout.Toggle(isSelected, "", GUILayout.Width(20));
|
|
if (newSelected != isSelected)
|
|
{
|
|
selectedComponentIndex = newSelected ? i : -1;
|
|
}
|
|
|
|
EditorGUILayout.BeginVertical();
|
|
EditorGUILayout.LabelField($"GameObject: {info.gameObject.name}", EditorStyles.boldLabel);
|
|
EditorGUILayout.LabelField($"Path: {info.hierarchyPath}", EditorStyles.miniLabel);
|
|
EditorGUILayout.LabelField($"Component: {info.component.GetType().Name}", EditorStyles.miniLabel);
|
|
EditorGUILayout.EndVertical();
|
|
|
|
if (GUILayout.Button("Ping", GUILayout.Width(80)))
|
|
{
|
|
Selection.activeObject = info.gameObject;
|
|
EditorGUIUtility.PingObject(info.gameObject);
|
|
SceneView.FrameLastActiveSceneView();
|
|
}
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
EditorGUILayout.EndVertical();
|
|
|
|
GUILayout.Space(2);
|
|
}
|
|
}
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
EditorGUILayout.EndVertical();
|
|
|
|
GUILayout.Space(10);
|
|
|
|
// Replace Section
|
|
EditorGUILayout.BeginVertical("box");
|
|
EditorGUILayout.LabelField("Replace Component", EditorStyles.boldLabel);
|
|
GUILayout.Space(5);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField("Replace With:", GUILayout.Width(120));
|
|
|
|
if (GUILayout.Button(replaceTypeName, EditorStyles.popup, GUILayout.ExpandWidth(true)))
|
|
{
|
|
var dropdown = new ComponentTypeDropdown(
|
|
new AdvancedDropdownState(),
|
|
(type) =>
|
|
{
|
|
selectedReplaceType = type;
|
|
replaceTypeName = type != null ? type.Name : "Select a Component...";
|
|
Repaint();
|
|
});
|
|
dropdown.Show(GUILayoutUtility.GetLastRect());
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
GUILayout.Space(5);
|
|
|
|
bool canReplace = selectedComponentIndex >= 0 &&
|
|
selectedComponentIndex < foundComponents.Count &&
|
|
selectedReplaceType != null;
|
|
|
|
EditorGUI.BeginDisabledGroup(!canReplace);
|
|
|
|
if (!canReplace && selectedComponentIndex >= 0 && selectedReplaceType == null)
|
|
{
|
|
EditorGUILayout.HelpBox("Select a component type to replace with.", MessageType.Warning);
|
|
}
|
|
else if (!canReplace && selectedReplaceType != null)
|
|
{
|
|
EditorGUILayout.HelpBox("Select an object from the search results above.", MessageType.Warning);
|
|
}
|
|
|
|
if (GUILayout.Button("Replace Selected Component", GUILayout.Height(30)))
|
|
{
|
|
ReplaceComponent();
|
|
}
|
|
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
GUILayout.Space(5);
|
|
}
|
|
|
|
private void SearchForComponents()
|
|
{
|
|
if (selectedSearchType == null)
|
|
{
|
|
Debug.LogWarning("No component type selected for search.");
|
|
return;
|
|
}
|
|
|
|
foundComponents.Clear();
|
|
selectedComponentIndex = -1;
|
|
|
|
var allObjects = FindObjectsByType<GameObject>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
|
|
foreach (var go in allObjects)
|
|
{
|
|
Component component = null;
|
|
|
|
if (includeDerivedTypes)
|
|
{
|
|
// Search for the type and all derived types
|
|
component = go.GetComponent(selectedSearchType);
|
|
}
|
|
else
|
|
{
|
|
// Search for exact type only
|
|
var components = go.GetComponents<Component>();
|
|
component = components.FirstOrDefault(c => c != null && c.GetType() == selectedSearchType);
|
|
}
|
|
|
|
if (component != null)
|
|
{
|
|
foundComponents.Add(new ComponentInfo
|
|
{
|
|
gameObject = go,
|
|
component = component,
|
|
hierarchyPath = GetHierarchyPath(go)
|
|
});
|
|
}
|
|
}
|
|
|
|
foundComponents = foundComponents.OrderBy(c => c.hierarchyPath).ToList();
|
|
|
|
string searchMode = includeDerivedTypes ? "including derived types" : "exact type only";
|
|
Debug.Log($"Found {foundComponents.Count} objects with component type '{selectedSearchType.Name}' ({searchMode})");
|
|
Repaint();
|
|
}
|
|
|
|
private void ReplaceComponent()
|
|
{
|
|
if (selectedComponentIndex < 0 || selectedComponentIndex >= foundComponents.Count)
|
|
{
|
|
Debug.LogError("No component selected to replace.");
|
|
return;
|
|
}
|
|
|
|
if (selectedReplaceType == null)
|
|
{
|
|
Debug.LogError("No replacement component type selected.");
|
|
return;
|
|
}
|
|
|
|
var info = foundComponents[selectedComponentIndex];
|
|
var go = info.gameObject;
|
|
var oldComponent = info.component;
|
|
|
|
if (go == null || oldComponent == null)
|
|
{
|
|
Debug.LogError("Selected component or GameObject is null.");
|
|
foundComponents.RemoveAt(selectedComponentIndex);
|
|
selectedComponentIndex = -1;
|
|
Repaint();
|
|
return;
|
|
}
|
|
|
|
// Confirm the replacement
|
|
bool confirmed = EditorUtility.DisplayDialog(
|
|
"Replace Component",
|
|
$"Are you sure you want to replace '{selectedSearchType.Name}' with '{selectedReplaceType.Name}' on GameObject '{go.name}'?\n\n" +
|
|
"This action can be undone with Ctrl+Z.",
|
|
"Replace",
|
|
"Cancel"
|
|
);
|
|
|
|
if (!confirmed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Record undo
|
|
Undo.RegisterCompleteObjectUndo(go, "Replace Component");
|
|
|
|
// Copy serialized data if possible (optional enhancement)
|
|
// This attempts to preserve common fields between components
|
|
var serializedOldComponent = new SerializedObject(oldComponent);
|
|
|
|
// Remove old component
|
|
Undo.DestroyObjectImmediate(oldComponent);
|
|
|
|
// Add new component
|
|
var newComponent = Undo.AddComponent(go, selectedReplaceType);
|
|
|
|
// Try to copy matching serialized properties
|
|
var serializedNewComponent = new SerializedObject(newComponent);
|
|
CopyMatchingSerializedProperties(serializedOldComponent, serializedNewComponent);
|
|
serializedNewComponent.ApplyModifiedProperties();
|
|
|
|
// Mark scene as dirty
|
|
EditorSceneManager.MarkSceneDirty(go.scene);
|
|
|
|
Debug.Log($"Replaced '{selectedSearchType.Name}' with '{selectedReplaceType.Name}' on '{go.name}'");
|
|
|
|
// Update the found components list
|
|
foundComponents[selectedComponentIndex] = new ComponentInfo
|
|
{
|
|
gameObject = go,
|
|
component = newComponent,
|
|
hierarchyPath = info.hierarchyPath
|
|
};
|
|
|
|
// Update search type if needed
|
|
if (selectedSearchType != selectedReplaceType)
|
|
{
|
|
foundComponents.RemoveAt(selectedComponentIndex);
|
|
selectedComponentIndex = -1;
|
|
}
|
|
|
|
Repaint();
|
|
}
|
|
|
|
private void CopyMatchingSerializedProperties(SerializedObject source, SerializedObject destination)
|
|
{
|
|
SerializedProperty iterator = source.GetIterator();
|
|
bool enterChildren = true;
|
|
|
|
while (iterator.NextVisible(enterChildren))
|
|
{
|
|
enterChildren = false;
|
|
|
|
// Skip script reference
|
|
if (iterator.propertyPath == "m_Script")
|
|
continue;
|
|
|
|
SerializedProperty destProp = destination.FindProperty(iterator.propertyPath);
|
|
if (destProp != null && destProp.propertyType == iterator.propertyType)
|
|
{
|
|
try
|
|
{
|
|
destination.CopyFromSerializedProperty(iterator);
|
|
}
|
|
catch
|
|
{
|
|
// Some properties may not be copyable
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private string GetHierarchyPath(GameObject go)
|
|
{
|
|
string path = go.name;
|
|
Transform t = go.transform.parent;
|
|
while (t != null)
|
|
{
|
|
path = t.name + "/" + path;
|
|
t = t.parent;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
private class ComponentInfo
|
|
{
|
|
public GameObject gameObject;
|
|
public Component component;
|
|
public string hierarchyPath;
|
|
}
|
|
}
|
|
|
|
// Advanced Dropdown for Component Type Selection (including Unity built-in components)
|
|
public class ComponentTypeDropdown : AdvancedDropdown
|
|
{
|
|
private Action<Type> onTypeSelected;
|
|
private static List<Type> cachedTypes;
|
|
|
|
public ComponentTypeDropdown(AdvancedDropdownState state, Action<Type> onTypeSelected)
|
|
: base(state)
|
|
{
|
|
this.onTypeSelected = onTypeSelected;
|
|
minimumSize = new Vector2(300, 400);
|
|
|
|
if (cachedTypes == null)
|
|
{
|
|
CacheTypes();
|
|
}
|
|
}
|
|
|
|
private static void CacheTypes()
|
|
{
|
|
cachedTypes = new List<Type>();
|
|
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
|
|
|
foreach (var assembly in assemblies)
|
|
{
|
|
try
|
|
{
|
|
var types = assembly.GetTypes()
|
|
.Where(t => (t.IsSubclassOf(typeof(MonoBehaviour)) || t.IsSubclassOf(typeof(Component)))
|
|
&& !t.IsAbstract
|
|
&& t != typeof(Transform)) // Exclude Transform as it can't be removed
|
|
.OrderBy(t => t.Name);
|
|
cachedTypes.AddRange(types);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Some assemblies may throw exceptions when getting types
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override AdvancedDropdownItem BuildRoot()
|
|
{
|
|
var root = new AdvancedDropdownItem("Component Types");
|
|
|
|
// Add "None" option
|
|
var noneItem = new TypeDropdownItem("None", null);
|
|
root.AddChild(noneItem);
|
|
|
|
root.AddSeparator();
|
|
|
|
// Separate Unity built-in components from custom ones
|
|
var unityComponents = cachedTypes.Where(t =>
|
|
t.Namespace != null && t.Namespace.StartsWith("UnityEngine")).ToList();
|
|
var customComponents = cachedTypes.Except(unityComponents).ToList();
|
|
|
|
// Add Unity Components section
|
|
if (unityComponents.Any())
|
|
{
|
|
var unitySection = new AdvancedDropdownItem("Unity Components");
|
|
|
|
// Group Unity components by category (second part of namespace)
|
|
var unityGrouped = unityComponents.GroupBy(t =>
|
|
{
|
|
if (t.Namespace == "UnityEngine")
|
|
return "Core";
|
|
var parts = t.Namespace.Split('.');
|
|
return parts.Length > 1 ? parts[1] : "Other";
|
|
}).OrderBy(g => g.Key);
|
|
|
|
foreach (var group in unityGrouped)
|
|
{
|
|
if (group.Count() <= 5)
|
|
{
|
|
// Add directly if few items
|
|
foreach (var type in group.OrderBy(t => t.Name))
|
|
{
|
|
var typeItem = new TypeDropdownItem(type.Name, type);
|
|
unitySection.AddChild(typeItem);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Create subcategory
|
|
var categoryItem = new AdvancedDropdownItem(group.Key);
|
|
foreach (var type in group.OrderBy(t => t.Name))
|
|
{
|
|
var typeItem = new TypeDropdownItem(type.Name, type);
|
|
categoryItem.AddChild(typeItem);
|
|
}
|
|
unitySection.AddChild(categoryItem);
|
|
}
|
|
}
|
|
|
|
root.AddChild(unitySection);
|
|
}
|
|
|
|
// Add Custom Components section
|
|
if (customComponents.Any())
|
|
{
|
|
root.AddSeparator();
|
|
|
|
// Group by namespace
|
|
var groupedTypes = customComponents.GroupBy(t =>
|
|
string.IsNullOrEmpty(t.Namespace) ? "(No Namespace)" : t.Namespace);
|
|
|
|
foreach (var group in groupedTypes.OrderBy(g => g.Key))
|
|
{
|
|
// For small namespaces or no namespace, add directly to root
|
|
if (group.Count() <= 3 || group.Key == "(No Namespace)")
|
|
{
|
|
foreach (var type in group)
|
|
{
|
|
var typeItem = new TypeDropdownItem(
|
|
group.Key == "(No Namespace)" ? type.Name : $"{group.Key}.{type.Name}",
|
|
type);
|
|
root.AddChild(typeItem);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Create namespace folder
|
|
var namespaceItem = new AdvancedDropdownItem(group.Key);
|
|
|
|
foreach (var type in group)
|
|
{
|
|
var typeItem = new TypeDropdownItem(type.Name, type);
|
|
namespaceItem.AddChild(typeItem);
|
|
}
|
|
|
|
root.AddChild(namespaceItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
return root;
|
|
}
|
|
|
|
protected override void ItemSelected(AdvancedDropdownItem item)
|
|
{
|
|
var typeItem = item as TypeDropdownItem;
|
|
if (typeItem != null)
|
|
{
|
|
onTypeSelected?.Invoke(typeItem.Type);
|
|
}
|
|
}
|
|
|
|
private class TypeDropdownItem : AdvancedDropdownItem
|
|
{
|
|
public Type Type { get; private set; }
|
|
|
|
public TypeDropdownItem(string name, Type type) : base(name)
|
|
{
|
|
Type = type;
|
|
}
|
|
}
|
|
}
|
|
}
|