#define SHOW_NAME_CONTAINERS using System; using System.Collections.Concurrent; using UnityEngine; using Debug = UnityEngine.Debug; #if UNITY_EDITOR using System.Diagnostics.CodeAnalysis; using UnityEditor; #endif namespace Phantom.XRMOD.UnityFusion.Editor { /// /// Component that acts as a container for the custom name given to a component. /// [AddComponentMenu(DontShowInMenu)] internal class NameContainer : MonoBehaviour { #pragma warning disable CS0414 private const string DontShowInMenu = ""; internal static bool NowRenaming; internal static Component StartingToRename; private static readonly ConcurrentDictionary instances = new(); [SerializeField] private string nameOverride = ""; [SerializeField] private string tooltipOverride = ""; [SerializeField] private Component target = null; #pragma warning restore CS0414 #if UNITY_EDITOR internal const string ContainerName = "NameContainer"; internal const string ContainerTag = "EditorOnly"; internal string NameOverride { get => nameOverride; set { if(value == nameOverride) { return; } nameOverride = value; EditorUtility.SetDirty(this); } } internal string TooltipOverride { get => tooltipOverride; set { if(value == tooltipOverride) { return; } tooltipOverride = value; EditorUtility.SetDirty(this); } } private void Awake() => OnValidate(); private void OnValidate() { if(NowRenaming) { return; } UpdateHideFlags(); if(!target) { #if DEV_MODE Debug.Log($"Destroying {name} under parent \"{(transform.parent ? transform.parent.name : "null")}\" @ \"{AssetDatabase.GetAssetOrScenePath(gameObject)}\" because target is null..."); #endif EditorApplication.delayCall += () => { if(this && !target) { Remove(ModifyOptions.Immediate | ModifyOptions.NonUndoable); } }; return; } if(transform.parent != target.transform) { if(transform == target.transform) { #if DEV_MODE Debug.Log($"Destroying {name} because transform == target.transform..."); #endif target = null; Remove(ModifyOptions.NonUndoable); return; } #if DEV_MODE Debug.Log($"Destroying {name} because transform.parent != target.transform..."); #endif EditorApplication.delayCall += () => { if (!this) { return; } if(!target) { Remove(ModifyOptions.Immediate | ModifyOptions.NonUndoable); return; } if(NameContainer.TryGet(target, out var existingNameContainer) && existingNameContainer != this) { #if DEV_MODE Debug.LogWarning($"Destroying NameContainer {name} with name \"{nameOverride}\" targeting {target.GetType().Name} on game object \"{target.name}\", because target already has another NameContainer {existingNameContainer.name} targeting it."); #endif Remove(ModifyOptions.Immediate | ModifyOptions.NonUndoable); return; } if(transform.parent == target.transform) { return; } // Avoid error 'Setting the parent of a transform which resides in a Prefab Asset is disabled to prevent data corruption'. bool thisIsPrefabAsset = PrefabUtility.IsPartOfPrefabAsset(this); bool targetIsPrefabAsset = PrefabUtility.IsPartOfPrefabAsset(target); if(thisIsPrefabAsset || targetIsPrefabAsset) { if(PrefabUtils.TrySetParent(transform, target.transform, false) is { IsSuccess: false } setParentResult) { HandleLogWarning(transform, setParentResult); if(!PrefabUtility.IsPartOfPrefabAsset(this)) { #if DEV_MODE Debug.Log($"DestroyImmediate({name}) @ {AssetDatabase.GetAssetOrScenePath(this)}"); #endif DestroyImmediate(gameObject); } } return; } #if !UNITY_2022_3_OR_NEWER // In older versions of Unity it's not possible to reparent/remove game objects inside prefab instances. // Would need to unpack the prefab, perform the modifications on the unpacked game object, and apply // the changes on top of the old prefab asset - which can get quite complex and error-prone. if(PrefabUtility.IsPartOfPrefabInstance(gameObject) && !PrefabUtility.IsOutermostPrefabInstanceRoot(gameObject)) { #if DEV_MODE Debug.Log($"Won't reparent NameContainer(\"{nameOverride}\") because it's part of a prefab instance.", transform.parent); #endif return; } #endif transform.SetParent(target.transform, false); }; } if(instances.TryGetValue(target, out var existingContainer) && existingContainer != this && existingContainer && existingContainer.target == target) { #if DEV_MODE Debug.Log($"Destroying {name} because existing duplicate instance found instances.TryGetValue..."); #endif // Copy over name and tooltip from this container to the other one and destroy this one. // It is likely that this container contains the name/tooltip for a prefab instance and the other // one contains it for the prefab asset. // In this situation we want to convert the name and tooltips into instance value overrides // instead of having two different name containers for one target. // In any case we never want to have two different name containers when it can be avoided. existingContainer.NameOverride = NameOverride; existingContainer.TooltipOverride = TooltipOverride; Remove(ModifyOptions.NonUndoable); return; } instances[target] = this; if(ComponentName.IsNullEmptyOrDefault(target, nameOverride)) { if(TooltipOverride.Length == 0) { #if DEV_MODE Debug.Log($"Destroying {name} because name and tooltip are empty"); #endif Remove(ModifyOptions.Defaults); return; } ComponentName.ResetToDefault(target, ModifyOptions.DontUpdateNameContainer); } else { EditorApplication.delayCall += ()=> { if(!this) { return; } if(!target) { #if DEV_MODE Debug.Log($"Destroying {name} because target was null after delay."); #endif Remove(ModifyOptions.Immediate | ModifyOptions.NonUndoable); return; } if(ComponentName.IsNullEmptyOrDefault(target, nameOverride)) { if(TooltipOverride.Length == 0) { Remove(ModifyOptions.Immediate); return; } ComponentName.ResetToDefault(target, ModifyOptions.Immediate | ModifyOptions.DontUpdateNameContainer); return; } ComponentTooltip.Set(target, tooltipOverride, ModifyOptions.Immediate | ModifyOptions.DontUpdateNameContainer); ComponentName.Set(target, nameOverride, ModifyOptions.Immediate | ModifyOptions.DontUpdateNameContainer); }; } if(string.Equals(name, "NameContainer(EditorOnly)")) { name = GetNameContainerGameObjectName(target); } } internal static void StartRenaming(Component component) { #if DEV_MODE Debug.Assert(component); Debug.Assert(component is not NameContainer); #endif NowRenaming = true; StartingToRename = component; EditorGUIUtility.editingTextField = true; } internal static void TryGetOrCreate([AllowNull] Component component, ModifyOptions modifyOptions, Action onAcquired, string initialName, string initialTooltip) { if(modifyOptions.IsDelayed()) { EditorApplication.delayCall += TryGetOrCreateNow; } else { TryGetOrCreateNow(); } void TryGetOrCreateNow() { if(!component) { return; } // if(TryGetOrCreateImmediate(component, out var nameContainer, initialName, initialTooltip)) // { // onAcquired?.Invoke(nameContainer); // } } } private static bool TryGetOrCreateImmediate([AllowNull] Component target, [MaybeNullWhen(false), NotNullWhen(true)] out NameContainer nameContainer, string initialName = null, string initialTooltip = null) { if(!target) { nameContainer = null; return false; } var gameObjectWithComponent = target.gameObject; if(!gameObjectWithComponent) { nameContainer = null; return false; } if(TryGet(target, out nameContainer)) { return nameContainer; } if(!PrefabUtils.CanAddChild(target.transform, target)) { return false; } var name = GetNameContainerGameObjectName(target); var containerGameObject = new GameObject(name); bool wasRenaming = NowRenaming; NowRenaming = true; var prefabPath = AssetDatabase.GetAssetPath(gameObjectWithComponent); try { if(!Application.isPlaying || PrefabUtility.IsPartOfPrefabAsset(target) || UnityEditor.SceneManagement.PrefabStageUtility.GetPrefabStage(target.gameObject)) { Undo.RegisterCreatedObjectUndo(containerGameObject, "Set Component Name"); } nameContainer = containerGameObject.AddComponent(); nameContainer.UpdateHideFlags(); containerGameObject.tag = ContainerTag; nameContainer.target = target; if(initialName != null) { nameContainer.NameOverride = initialName; } if(initialTooltip != null) { nameContainer.TooltipOverride = initialTooltip; } if(PrefabUtils.TrySetParent(containerGameObject.transform, gameObjectWithComponent.transform, false) is { IsSuccess: false } setParentResult) { HandleLogWarning(target, setParentResult); if(nameContainer) { nameContainer.RemoveImmediate(true); } return false; } } catch(Exception e) { Debug.LogWarning(e); if(nameContainer) { nameContainer.RemoveImmediate(true); } return false; } finally { if(!wasRenaming) { NowRenaming = false; } } if(nameContainer) { instances[target] = nameContainer; EditorUtility.SetDirty(nameContainer); } else if(prefabPath is { Length: > 0 }) { var prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); if(prefabAsset) { var containerTransform = prefabAsset.transform.Find(name); if(containerTransform && containerTransform.TryGetComponent(out nameContainer)) { instances[target] = nameContainer; EditorUtility.SetDirty(nameContainer); } } } return true; } private static string GetNameContainerGameObjectName(Component target) => ContainerName + " " + target.GetType().Name + " " + Guid.NewGuid(); private static void HandleLogWarning(Component component, ModifyPrefabResult result) => Debug.LogWarning($"Unable to set name of component {component.GetType().Name}, because " + result.FailReason switch { #if !UNITY_2022_3_OR_NEWER FailModifyPrefabReason.PartOfPrefabInstance => $"its target \"{result.Context}\" in scene {result.PrefabPath} is part of a prefab instance. Change the name of the component once in Prefab Mode to resolve the issue.", #endif FailModifyPrefabReason.MissingComponent => $"the prefab {result.PrefabPath} contains a missing component. Fix or remove the missing component to resolve the issue.", FailModifyPrefabReason.MultipleComponentsOfSameType => $"the prefab {result.PrefabPath} has a game object with more than one component of the same type {result.Context}. Change the name of the component once in Prefab Mode to resolve the issue.", FailModifyPrefabReason.MultipleGameObjectsWithSameName => $"the prefab {result.PrefabPath} contains multiple game objects with the same name \"{result.Context}\". Change the name of the component once in Prefab Mode to resolve the issue.", FailModifyPrefabReason.SaveAsPrefabAssetFailed => $"unable to save changes onto the prefab {result.PrefabPath}. Change the name of the component once in Prefab Mode to resolve the issue.", _ => "something went wrong." }, component); internal static bool TryGet(Component target, out NameContainer nameContainer) { if(instances.TryGetValue(target, out nameContainer)) { if(!nameContainer || nameContainer.target != target) { nameContainer = null; } // prioritize containers that are direct children of the target else if(nameContainer.transform.parent == target.transform) { return nameContainer; } } var parent = target.transform; for(int i = 0, childCount = parent.childCount; i < childCount; i++) { if(parent.GetChild(i).TryGetComponent(out NameContainer someNameContainer) && someNameContainer.target == target) { nameContainer = someNameContainer; return true; } } return nameContainer; } internal void Remove(ModifyOptions modifyOptions) { if(!modifyOptions.IsRemovingNameContainerAllowed()) { nameOverride = ""; tooltipOverride = ""; return; } bool isUndoable = modifyOptions.IsUndoable(); if(modifyOptions.IsDelayed()) { EditorApplication.delayCall += ()=> RemoveImmediate(isUndoable); } else { RemoveImmediate(isUndoable); } } private void RemoveImmediate(bool undoable) { if(!this) { return; } #if !UNITY_2022_3_OR_NEWER // In older versions of Unity it's not possible to simply remove game objects from prefab instances. // Would need to unpack the prefab, perform the modifications on the unpacked game object, and apply // the changes on top of the old prefab asset - which can get quite complex and error-prone. if(PrefabUtility.IsPartOfPrefabInstance(gameObject) && !PrefabUtility.IsOutermostPrefabInstanceRoot(gameObject)) { #if DEV_MODE && DEBUG_DESTROY Debug.Log($"Won't destroy NameContainer(\"{nameOverride}\") because it's part of a prefab instance.", transform.parent); #endif nameOverride = ""; tooltipOverride = ""; return; } #endif #if DEV_MODE && DEBUG_DESTROY Debug.Log($"Destroying NameContainer(\"{nameOverride}\")"); #endif if(gameObject.GetComponents().Length > 2) { #if DEV_MODE Debug.LogWarning($"Destroying only NameContainer component, instead of the whole game object, because the game object it is attached to contains extra components: {string.Join(", ", GetComponents().Select(c => c?.GetType().Name))}.", transform); #endif ObjectUtility.Destroy(this, undoable); return; } if(transform.childCount > 0) { #if DEV_MODE Debug.LogWarning($"Destroying only NameContainer component, instead of the whole game object, because the game object contains child game objects.\nFirst Child:{transform.GetChild(0)}.", transform); #endif ObjectUtility.Destroy(this, undoable); return; } #if DEV_MODE && DEBUG_DESTROY Debug.Log($"Destroying NameContainer(\"{nameOverride}\").", transform.parent); #endif ObjectUtility.Destroy(gameObject, undoable); } private void UpdateHideFlags() { if(IsAttachedToNameContainerGameObject()) { #if DEV_MODE && SHOW_NAME_CONTAINERS gameObject.hideFlags = HideFlags.None; hideFlags = HideFlags.None; #else gameObject.hideFlags = HideFlags.HideInHierarchy; #endif return; } hideFlags = HideFlags.None; } private bool IsAttachedToNameContainerGameObject() => name.StartsWith(ContainerName) && gameObject.GetComponents().Length == 2 && transform.childCount == 0; #endif } }