#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace Phantom.XRMOD.UnityFusion.Editor { /// /// Utility class for getting and setting component names. /// internal static class ComponentName { internal static event Action InspectorTitleChanged; private static readonly Dictionary originalInspectorTitles = new(); private static readonly Dictionary inspectorTitleOverrides = new(); /// /// Should class name be added as suffix in the inspector by default? /// internal static bool AddClassNameAsInspectorSuffixByDefault { get => EditorPrefs.GetBool("ComponentNames.AddClassNameInspectorSuffix", true); set => EditorPrefs.SetBool("ComponentNames.AddClassNameInspectorSuffix", value); } /// /// The format for inspector suffixes. /// internal static string InspectorSuffixFormat { get => EditorPrefs.GetString("ComponentNames.InspectorSuffixFormat", " ({0})"); set => EditorPrefs.SetString("ComponentNames.InspectorSuffixFormat", value); } /// /// Remove the default "(Script)" suffix from MonoBehaviour inspector titles? /// internal static bool RemoveDefaultScriptSuffix { get => EditorPrefs.GetBool("ComponentNames.RemoveDefaultScriptSuffix", true); set => EditorPrefs.GetBool("ComponentNames.RemoveDefaultScriptSuffix", value); } private static bool TryGetOverride([DisallowNull] Component component, out NameWithSuffix nameOverride) => inspectorTitleOverrides.TryGetValue(component, out nameOverride); internal static NameWithSuffix Get([DisallowNull] Component component) => TryGetOverride(component, out var @override) ? @override : GetDefault(component, withoutScriptSuffix:RemoveDefaultScriptSuffix); internal static void Set([DisallowNull] Component component, [AllowNull] string name, ModifyOptions modifyOptions) { #if DEV_MODE Debug.Assert(component, name); #endif if(IsNullEmptyOrDefault(component, name)) { ResetToDefault(component, modifyOptions); return; } if(name.EndsWith(')')) { int i = name.IndexOf('('); if(i != -1) { string suffix = name.Substring(i + 1, name.Length - i - 2); name = name.Substring(0, i).Trim(); if(name.Length == 0) { name = GetDefault(component, true); } Set(component, name, suffix, modifyOptions); return; } } Set(component, name, AddClassNameAsInspectorSuffixByDefault, modifyOptions); } internal static void Set([DisallowNull] Component component, [AllowNull] string name, bool addTypeNameSuffix, ModifyOptions modifyOptions) { #if DEV_MODE Debug.Assert(component, name); #endif if(IsNullEmptyOrDefault(component, name)) { ResetToDefault(component, modifyOptions); return; } if(!addTypeNameSuffix) { Set(component, name, "", modifyOptions); return; } var suffix = GetDefault(component, withoutScriptSuffix:true).name; // Avoid situations like "Cube" => "Cube (Cube (Mesh Filter))" if(suffix.EndsWith(')')) { int i = suffix.IndexOf('(') + 1; if(i > 0) { suffix = suffix.Substring(i, suffix.Length - i - 1); } } if(string.Equals(name, suffix)) { Set(component, name, null, modifyOptions); return; } Set(component, name, suffix, modifyOptions); } internal static void Set([DisallowNull] Component component, [AllowNull] string name, [AllowNull] string suffix, ModifyOptions modifyOptions) { #if DEV_MODE Debug.Assert(component, name); #endif if(IsNullEmptyOrDefault(component, name, suffix)) { ResetToDefault(component, modifyOptions); return; } var titleAndSuffix = new NameWithSuffix(component, name, suffix); inspectorTitleOverrides[component] = titleAndSuffix; var tileAndSuffixJoined = titleAndSuffix.ToString(false); NameContainer.TryGetOrCreate(component, modifyOptions, nameContainer => { nameContainer.NameOverride = titleAndSuffix.ToString(false); }, tileAndSuffixJoined, null); InspectorTitleChanged?.Invoke(component, titleAndSuffix); } private static void Set([DisallowNull] Component component, NameWithSuffix nameWithSuffix, ModifyOptions modifyOptions) { #if DEV_MODE Debug.Assert(component, nameWithSuffix); #endif if(IsNullEmptyOrDefault(component, nameWithSuffix)) { ResetToDefault(component, modifyOptions); return; } var tileAndSuffixJoined = nameWithSuffix.ToString(false); NameContainer.TryGetOrCreate(component, modifyOptions, nameContainer => { nameContainer.NameOverride = tileAndSuffixJoined; }, tileAndSuffixJoined, null); InspectorTitleChanged?.Invoke(component, nameWithSuffix); } internal static NameWithSuffix GetInspectorTitle([DisallowNull] Component component) { if(inspectorTitleOverrides.TryGetValue(component, out var inspectorTitle)) { return inspectorTitle; } return GetDefault(component, RemoveDefaultScriptSuffix); } internal static NameWithSuffix GetDefault([DisallowNull] Component component, bool withoutScriptSuffix) { var componentType = component.GetType(); // TODO: Is it bad that the tooltip gets discarded here? do we need to then call this twice to also get the tooltip? // Maybe not, if it's only updated on mouse enter for example. Investigate! var (title, suffix, _) = HeaderContentUtility.GetCustomHeaderContent(component, componentType); if(title.IsDefault) { var @default = GetDefaultBuiltIn(component, componentType, withoutScriptSuffix:false); title = @default.name; if(suffix.IsDefault) { suffix = withoutScriptSuffix && string.Equals(@default.suffix, "Script") ? "" : @default.suffix; } } else if(suffix.IsDefault) { suffix = GetDefaultBuiltIn(component, componentType, withoutScriptSuffix:false).name; } return new(title, suffix); } internal static NameWithSuffix GetDefaultBuiltIn(Component component, Type componentType, bool withoutScriptSuffix) { if(originalInspectorTitles.TryGetValue(componentType, out var result)) { return result; } if(componentType.GetCustomAttribute(false) is AddComponentMenu addComponentMenu && addComponentMenu.componentMenu is {Length: > 0} menuPath) { int lastCategoryEnd = menuPath.LastIndexOf('/'); result = new(lastCategoryEnd != -1 ? menuPath.Substring(lastCategoryEnd + 1) : menuPath, ""); return CacheAndReturn(result); } var title = ObjectNames.GetInspectorTitle(component); if(title.EndsWith(')')) { int suffixStart = title.LastIndexOf('('); if(suffixStart != -1) { var name = title.Substring(0, suffixStart).TrimEnd(' '); var suffix = title.Substring(suffixStart + 1, title.Length - suffixStart - 2); if(withoutScriptSuffix && string.Equals(suffix, "Script")) { return CacheAndReturn(new(name, "")); } return CacheAndReturn(new(name, suffix)); } } return CacheAndReturn(new(title, "")); NameWithSuffix CacheAndReturn(in NameWithSuffix result) { originalInspectorTitles.Add(componentType, result); return result; } } internal static void EnsureOriginalInspectorTitleIsCached([DisallowNull] Component component) => _ = GetDefaultBuiltIn(component, component.GetType(), RemoveDefaultScriptSuffix); internal static void EnsureOriginalInspectorTitleIsCached([DisallowNull] Component component, [DisallowNull] Type componentType) => _ = GetDefaultBuiltIn(component, componentType, RemoveDefaultScriptSuffix); /// /// Resets the title of the component to its default value. /// /// The component whose title to reset. /// /// can be used when calling from the main thread, outside of deserialization context. /// /// can be used when to make the action non-undoable. /// /// /// Both and flags are disabled by default. /// /// internal static void ResetToDefault([DisallowNull] Component component, ModifyOptions modifyOptions) { bool invokeInspectorTitleChanged = inspectorTitleOverrides.Remove(component); if(modifyOptions.IsUpdatingContainerAllowed()) { RemoveOrResetNameContainer(component, modifyOptions, ref invokeInspectorTitleChanged); } if(invokeInspectorTitleChanged) { InspectorTitleChanged?.Invoke(component, new NameWithSuffix(component)); } static void RemoveOrResetNameContainer(Component component, ModifyOptions modifyOptions, ref bool invokeInspectorTitleChanged) { if(modifyOptions.IsDelayed()) { bool invokeInspectorTitleChangedDelayed = invokeInspectorTitleChanged; EditorApplication.delayCall += ()=> { RemoveOrResetNameContainer(component, modifyOptions.Immediately(), ref invokeInspectorTitleChangedDelayed); if(invokeInspectorTitleChangedDelayed) { InspectorTitleChanged?.Invoke(component, new NameWithSuffix(component)); } }; invokeInspectorTitleChanged = false; return; } if(!NameContainer.TryGet(component, out var nameContainer)) { return; } if(string.IsNullOrEmpty(nameContainer.TooltipOverride) && modifyOptions.IsRemovingNameContainerAllowed()) { nameContainer.Remove(modifyOptions); invokeInspectorTitleChanged = true; return; } if(!string.IsNullOrEmpty(nameContainer.NameOverride)) { nameContainer.NameOverride = ""; invokeInspectorTitleChanged = true; } } } internal static bool IsNullEmptyOrDefault(Component component, string name) { if(string.IsNullOrEmpty(name)) { return true; } if(string.Equals(WithoutScriptSuffix(name), GetDefault(component, withoutScriptSuffix:true).ToString(richText:false))) { return true; } return false; } internal static bool IsNullEmptyOrDefault(Component component, NameWithSuffix nameWithSuffix) { if(string.IsNullOrEmpty(nameWithSuffix.name)) { return true; } if(string.Equals(WithoutScriptSuffix(nameWithSuffix.name), GetDefault(component, withoutScriptSuffix:true).ToString(richText:false))) { return true; } return false; } private static bool IsNullEmptyOrDefault(Component component, string name, string suffix) { if(string.IsNullOrEmpty(name)) { return suffix is null || string.Equals(suffix, "Script") || string.Equals(suffix, GetDefault(component, withoutScriptSuffix:true).name); } var defaultName = GetDefault(component, withoutScriptSuffix:true); if(string.Equals(name, defaultName.name)) { return suffix is null || string.Equals(suffix, "Script"); } return false; } internal static string WithoutRichTextTags([NotNull] string text) { const int MaxIterations = 10; for(int iteration = 1; iteration < MaxIterations; iteration++) { int index = text.IndexOf("', index + " 0) { text = text.Remove(index, endIndex + 1 - index); } index = text.IndexOf("", StringComparison.OrdinalIgnoreCase); if(index == -1) { return text; } text = text.Remove(index, "".Length); } return text; } private static string WithoutScriptSuffix(string title) => title.EndsWith(" (Script)") ? title.Substring(0, title.Length - " (Script)".Length) : title; } } #endif