diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/AssetsConverter.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/AssetsConverter.cs index 3a13a224f72..5cf8c7d8a20 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/AssetsConverter.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/AssetsConverter.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Text; +using UnityEditor.SceneManagement; +using UnityEditor.Search; using UnityEngine; +using UnityEngine.SceneManagement; namespace UnityEditor.Rendering.Converter { @@ -57,22 +60,11 @@ void OnSearchFinish() searchQueries, (item, description) => { - var unityObject = item.ToObject(); - - if (unityObject == null) - return; - - GameObject go = null; - - if (unityObject is GameObject gameObject) - go = gameObject; - else if (unityObject is Component component) - go = component.gameObject; - else + var newAssetItem = CreateAssetItem(item); + if (newAssetItem == null) return; - var gid = GlobalObjectId.GetGlobalObjectIdSlow(go); - var gidString = gid.ToString(); + var gidString = newAssetItem.GlobalObjectId; string groupName = GetGroupNameFromDescription(description); @@ -89,14 +81,7 @@ void OnSearchFinish() bool isNewAsset = processedIds.Add(gidString); if (isNewAsset) { - int type = gid.identifierType; - - assetItem = new RenderPipelineConverterAssetItem(gidString) - { - name = $"{unityObject.name} ({(type == GlobalObjectIdentifierType.ImportedAsset ? "Prefab" : "SceneObject")})", - info = type == GlobalObjectIdentifierType.ImportedAsset ? AssetDatabase.GetAssetPath(unityObject) : go.scene.path, - }; - + assetItem = newAssetItem; assets.Add(assetItem); assetLookup[gidString] = assetItem; } @@ -125,6 +110,77 @@ void OnSearchFinish() } } + internal static RenderPipelineConverterAssetItem CreateAssetItem(SearchItem item) + { + var unityObject = item.ToObject(); + + if (unityObject == null) + return CreateUnloadedSceneObjectItem(item); + + GameObject go = null; + + if (unityObject is GameObject gameObject) + go = gameObject; + else if (unityObject is Component component) + go = component.gameObject; + else + return null; + + var gid = GlobalObjectId.GetGlobalObjectIdSlow(go); + + if (gid.identifierType == GlobalObjectIdentifierType.ImportedAsset) + { + return new RenderPipelineConverterAssetItem(gid.ToString()) + { + name = $"{unityObject.name} (Prefab)", + info = AssetDatabase.GetAssetPath(unityObject), + }; + } + + return new RenderPipelineConverterAssetItem(gid.ToString()) + { + name = GetSceneObjectName(UnityEditor.Search.SearchUtils.GetTransformPath(go.transform)), + info = go.scene.path, + }; + } + + // Named with the path in the hierarchy, the only name Search has for objects in closed scenes. GameObject + // names can contain '/', so the object's own name can't be taken from it. + internal static string GetSceneObjectName(string transformPath) + { + var path = transformPath.StartsWith('/') ? transformPath.Substring(1) : transformPath; + return $"{path} (SceneObject)"; + } + + // Returns null unless the id is of an object in a scene that is not loaded + internal static string GetUnloadedScenePath(string globalObjectId) + { + if (!GlobalObjectId.TryParse(globalObjectId, out var gid) || gid.identifierType != GlobalObjectIdentifierType.SceneObject) + return null; + + var scenePath = AssetDatabase.GUIDToAssetPath(gid.assetGUID); + if (string.IsNullOrEmpty(scenePath) || SceneManager.GetSceneByPath(scenePath).isLoaded) + return null; + + return scenePath; + } + + // A scene object only loads while its scene is open, so build the item from the search result and let + // LoadObject open the scene at Convert time + static RenderPipelineConverterAssetItem CreateUnloadedSceneObjectItem(SearchItem item) + { + var scenePath = GetUnloadedScenePath(item.id); + if (scenePath == null) + return null; + + // Search labels scene objects with their path in the hierarchy + return new RenderPipelineConverterAssetItem(item.id) + { + name = GetSceneObjectName(item.GetLabel(item.context, true) ?? string.Empty), + info = scenePath, + }; + } + /// /// Categorizes scan results into a tree structure. /// Override this method to customize how items are organized. @@ -159,7 +215,10 @@ protected virtual string GetGroupNameFromDescription(string description) return description.Replace(" is being referenced", ""); } - public virtual void BeforeConvert() { } + public virtual void BeforeConvert() + { + m_ScenesOpenedForConversion?.Clear(); + } protected abstract Status ConvertObject(UnityEngine.Object obj, StringBuilder message); @@ -173,6 +232,10 @@ public Status Convert(IRenderPipelineConverterItem item, out string message) return Status.Error; } + var scenePath = GetUnloadedScenePath(assetItem.GlobalObjectId); + if (scenePath != null) + (m_ScenesOpenedForConversion ??= new HashSet()).Add(scenePath); + var obj = assetItem.LoadObject(); if (obj == null) @@ -184,10 +247,33 @@ public Status Convert(IRenderPipelineConverterItem item, out string message) var errorString = new StringBuilder(); var status = ConvertObject(obj, errorString); + SaveSceneOpenedForConversion(obj); message = errorString.ToString(); return status; } - public virtual void AfterConvert() { } + // Scenes opened to convert their objects are closed again without saving when the next one opens, or when + // the conversion returns to the scene it started from. Only tracked for the current conversion, so that a + // later one leaves saving them to the user. + [NonSerialized] + HashSet m_ScenesOpenedForConversion; + + void SaveSceneOpenedForConversion(UnityEngine.Object obj) + { + var scene = obj switch + { + GameObject go => go.scene, + Component component => component.gameObject.scene, + _ => default + }; + + if (scene.IsValid() && scene.isDirty && m_ScenesOpenedForConversion != null && m_ScenesOpenedForConversion.Contains(scene.path)) + EditorSceneManager.SaveScene(scene); + } + + public virtual void AfterConvert() + { + m_ScenesOpenedForConversion?.Clear(); + } } } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterItemState.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterItemState.cs index 75c648a7fba..d0f717d5a2d 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterItemState.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterItemState.cs @@ -16,29 +16,34 @@ internal static class CheckboxStateHelper /// public static (bool isChecked, bool isIndeterminate) CalculateCheckboxState(IEnumerable childItems) { - int selectedCount = 0; - int enabledCount = 0; + bool hasSelected = false; + bool hasUnselected = false; foreach (var child in childItems) { if (child.item.isEnabled) { - enabledCount++; if (child.isSelected == true) - selectedCount++; + hasSelected = true; + else + hasUnselected = true; + + // Early exit: mixed state found + if (hasSelected && hasUnselected) + return (false, true); } } - if (enabledCount == 0) - return (false, false); // No enabled children - - if (selectedCount == 0) - return (false, false); // None selected + // No enabled children + if (!hasSelected && !hasUnselected) + return (false, false); - if (selectedCount == enabledCount) - return (true, false); // All selected + // All selected (hasSelected && !hasUnselected) + if (hasSelected) + return (true, false); - return (false, true); // Mixed state (some selected) + // None selected (!hasSelected && hasUnselected) + return (false, false); } /// @@ -59,6 +64,7 @@ public static (bool isChecked, bool isIndeterminate) CalculateCheckboxState(IEnu interface IConverterItemState { bool? isSelected { get; set; } + bool isExpanded { get; set; } IRenderPipelineConverterItem item { get; } (Status Status, string Message) conversionResult { get; } bool hasConverted { get; } @@ -70,6 +76,7 @@ interface IConverterItemState void SetSelectionChangedHandler(Action handler); void Clear(); int CountItemsWithStatus(Status status); + int CountSelectedItemsWithStatus(Status status); int GetTotalItemCount(); int GetSelectedItemCount(); bool IsFolder(); @@ -89,6 +96,7 @@ public Action onIsSelectedChanged set => m_OnIsSelectedChanged = value; } + [SerializeField] private bool m_IsSelected; public bool? isSelected { @@ -103,16 +111,27 @@ public bool? isSelected } } + // Leaf items are never expanded + public bool isExpanded { get => false; set { } } + + [field:SerializeReference] public IRenderPipelineConverterItem item { get; set; } - [NonSerialized] - private (Status Status, string Message) m_ConversionResult = (Status.Pending, string.Empty); + [SerializeField] + private Status m_ConversionStatus = Status.Pending; + [SerializeField] + private string m_ConversionMessage = string.Empty; + public (Status Status, string Message) conversionResult { - get => m_ConversionResult; - set => m_ConversionResult = value; + get => (m_ConversionStatus, m_ConversionMessage); + set + { + m_ConversionStatus = value.Status; + m_ConversionMessage = value.Message; + } } - public bool hasConverted => m_ConversionResult.Status != Status.Pending; + public bool hasConverted => m_ConversionStatus != Status.Pending; public void OnSelectionChanged(ClickEvent _) { @@ -136,7 +155,12 @@ public void SetSelectionChangedHandler(Action handler) public int CountItemsWithStatus(Status status) { - return m_ConversionResult.Status == status ? 1 : 0; + return m_ConversionStatus == status ? 1 : 0; + } + + public int CountSelectedItemsWithStatus(Status status) + { + return (m_IsSelected && m_ConversionStatus == status) ? 1 : 0; } public int GetTotalItemCount() @@ -160,15 +184,26 @@ public bool IsFolder() public int CompareTo(IConverterItemState other) { + if (other == null) + return 1; + if (other.IsFolder()) return 1; + // Handle null items gracefully + if (item == null && other.item == null) + return 0; + if (item == null) + return -1; + if (other.item == null) + return 1; + return string.Compare(item.name, other.item.name, StringComparison.Ordinal); } public DisplayFilter GetDisplayFilter() { - return m_ConversionResult.Status switch + return m_ConversionStatus switch { Status.Pending => DisplayFilter.Pending, Status.Warning => DisplayFilter.Warnings, @@ -209,14 +244,30 @@ public bool? isSelected } } + [SerializeField] + private bool m_IsExpanded = true; // Default to true so folders are expanded by default + public bool isExpanded + { + get => m_IsExpanded; + set => m_IsExpanded = value; + } + + [field:SerializeReference] public IRenderPipelineConverterItem item { get; set; } - [NonSerialized] - private (Status Status, string Message) m_ConversionResult = (Status.Pending, string.Empty); + [SerializeField] + private Status m_ConversionStatus = Status.Pending; + [SerializeField] + private string m_ConversionMessage = string.Empty; + public (Status Status, string Message) conversionResult { - get => m_ConversionResult; - set => m_ConversionResult = value; + get => (m_ConversionStatus, m_ConversionMessage); + set + { + m_ConversionStatus = value.Status; + m_ConversionMessage = value.Message; + } } public bool hasConverted => false; @@ -265,6 +316,30 @@ public void SubscribeToChildren() } } + /// + /// Recalculates folder selection state from children. + /// Called after deserialization since nullable bool can't be serialized. + /// + public void RecalculateSelectionFromChildren() + { + // Recursively recalculate for nested folders first, so this folder + // aggregates up-to-date child states + foreach (var child in children) + { + if (child is FolderItemState childFolder) + childFolder.RecalculateSelectionFromChildren(); + } + + m_IsSelected = CheckboxStateHelper.CalculateSelectionState(children); + + // Recursively recalculate for nested folders + foreach (var child in children) + { + if (child is FolderItemState childFolder) + childFolder.RecalculateSelectionFromChildren(); + } + } + public void UnsubscribeFromChildren() { foreach (var child in children) @@ -315,6 +390,16 @@ public int CountItemsWithStatus(Status status) return count; } + public int CountSelectedItemsWithStatus(Status status) + { + int count = 0; + foreach (var child in children) + { + count += child.CountSelectedItemsWithStatus(status); + } + return count; + } + public int GetTotalItemCount() { int count = 0; @@ -347,9 +432,20 @@ public bool IsFolder() public int CompareTo(IConverterItemState other) { + if (other == null) + return 1; + if (!other.IsFolder()) return -1; + // Handle null items gracefully + if (item == null && other.item == null) + return 0; + if (item == null) + return -1; + if (other.item == null) + return 1; + return string.Compare(item.name, other.item.name, StringComparison.Ordinal); } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterState.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterState.cs index 16f592ede19..a64b85439d0 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterState.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterState.cs @@ -89,7 +89,19 @@ private int CountItemWithFlag(Status status) } return count; } + + private int CountSelectedItemWithFlag(Status status) + { + int count = 0; + foreach (var itemState in items) + { + count += itemState.CountSelectedItemsWithStatus(status); + } + return count; + } + public int pending => CountItemWithFlag(Status.Pending); + public int selectedPending => CountSelectedItemWithFlag(Status.Pending); public int warnings => CountItemWithFlag(Status.Warning); public int errors => CountItemWithFlag(Status.Error); public int success => CountItemWithFlag(Status.Success); @@ -111,6 +123,10 @@ public void Clear() m_FilteredItemsTree.Clear(); m_CachedFullTree.Clear(); m_TreeCacheDirty = true; + + // Reset filter to show all items when re-scanning + // (prevents blank tree when filter doesn't match new scan results) + currentFilter = DisplayFilter.All; } private bool IsVisible(DisplayFilter filter) @@ -133,6 +149,76 @@ internal void SetupRootEventHandlers(Action onRootSelectionChanged) } } + /// + /// Cleans up invalid items after deserialization. + /// Called automatically after assembly reload. + /// Note: Root event handlers are re-established by the window during CreateGUI. + /// Note: Tree cache rebuild is deferred - window should call ApplyFilter() before using filteredItemsTree. + /// + internal void OnAfterDeserialize() + { + // Recursively validate and clean up items + bool hasValidItems = ValidateAndCleanItems(items); + + if (!hasValidItems) + { + isInitialized = false; + return; + } + + // Re-subscribe folder items to their children's selection changes + // and recalculate folder selection states from children + foreach (var itemState in items) + { + if (itemState is FolderItemState folderState) + { + folderState.SubscribeToChildren(); + folderState.RecalculateSelectionFromChildren(); + } + } + + // Mark tree cache as dirty - window will call ApplyFilter() when needed + m_TreeCacheDirty = true; + } + + /// + /// Recursively validates items and removes invalid ones. + /// Returns true if any valid items remain. + /// + private bool ValidateAndCleanItems(List itemList) + { + bool hasValidItems = false; + + for (int i = itemList.Count - 1; i >= 0; i--) + { + var itemState = itemList[i]; + if (itemState == null || itemState.item == null) + { + itemList.RemoveAt(i); + } + else + { + hasValidItems = true; + + // Recursively validate children of folders + if (itemState is FolderItemState folderState && folderState.children != null && folderState.children.Count > 0) + { + ValidateAndCleanItems(folderState.children); + } + } + } + + return hasValidItems; + } + + /// + /// No-op - event handlers are [NonSerialized] and don't need cleanup. + /// Kept for ISerializationCallbackReceiver contract. + /// + internal void OnBeforeSerialize() + { + } + /// /// Recursively creates the appropriate state type (FolderItemState or ConverterItemState) for an item /// diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ReadonlyMaterialConverter/ReadonlyMaterialConverter.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ReadonlyMaterialConverter/ReadonlyMaterialConverter.cs index 55568b8f016..52ddb2f0fd7 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ReadonlyMaterialConverter/ReadonlyMaterialConverter.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ReadonlyMaterialConverter/ReadonlyMaterialConverter.cs @@ -7,12 +7,19 @@ namespace UnityEditor.Rendering.Converter { internal class ReadonlyMaterialMap { + const string k_BuiltInExtraResourcesPath = "Resources/unity_builtin_extra"; + public bool TryGetMappingMaterial(Material material, out Material mappingMaterial) { mappingMaterial = material; - if (m_BuiltInMaterialsMappings.TryGetValue(material.name, out var mapping)) + if (material != null && + sourceMaterials.TryGetValue(material.name, out var sourceMaterial) && + sourceMaterial == material && + m_BuiltInMaterialsMappings.TryGetValue(material.name, out var mapping)) + { mappingMaterial = mapping(); + } return mappingMaterial != null; } @@ -22,6 +29,10 @@ public bool TryGetMappingMaterial(Material material, out Material mappingMateria public IEnumerable Keys => m_BuiltInMaterialsMappings.Keys; Dictionary> m_BuiltInMaterialsMappings = new(); + Func> m_SourceMaterialsProvider; + Dictionary m_SourceMaterials; + + Dictionary sourceMaterials => m_SourceMaterials ??= CollectSourceMaterials(); public List<(string materialName, string searchQuery)> GetMaterialSearchList() { @@ -34,9 +45,30 @@ public bool TryGetMappingMaterial(Material material, out Material mappingMateria return list; } - public ReadonlyMaterialMap(Dictionary> mappings) + public ReadonlyMaterialMap(Dictionary> mappings, Func> sourceMaterialsProvider = null) { m_BuiltInMaterialsMappings = mappings; + m_SourceMaterialsProvider = sourceMaterialsProvider ?? LoadBuiltInExtraMaterials; + } + + public static IEnumerable LoadBuiltInExtraMaterials() + { + foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(k_BuiltInExtraResourcesPath)) + { + if (asset is Material material) + yield return material; + } + } + + Dictionary CollectSourceMaterials() + { + var materials = new Dictionary(); + foreach (var material in m_SourceMaterialsProvider()) + { + if (material != null && m_BuiltInMaterialsMappings.ContainsKey(material.name)) + materials.TryAdd(material.name, material); + } + return materials; } public Material[] GetBuiltInMaterials() @@ -45,34 +77,9 @@ public Material[] GetBuiltInMaterials() { foreach (var materialName in Keys) { - var name = materialName + ".mat"; - - Material mat = null; - foreach (var material in AssetDatabaseHelper.FindAssets()) + if (!sourceMaterials.TryGetValue(materialName, out var mat) || mat == null) { - if (material.name == materialName) - { - mat = material; - break; - } - } - - if (mat == null) - { - mat = AssetDatabase.GetBuiltinExtraResource(name); - if (mat == null) - { - mat = Resources.GetBuiltinResource(name); - if (mat == null) - { - mat = Resources.Load(name); - } - } - } - - if (mat == null) - { - Debug.LogError($"Material '{materialName}' not found in built-in resources or project assets."); + Debug.LogError($"Material '{materialName}' not found in built-in resources."); continue; } @@ -88,6 +95,8 @@ internal abstract class ReadonlyMaterialConverter : AssetsConverter { protected virtual Dictionary> materialMappings { get; } + protected virtual IEnumerable GetSourceMaterials() => ReadonlyMaterialMap.LoadBuiltInExtraMaterials(); + protected override List<(string query, string description)> contextSearchQueriesAndIds { get => mappings.GetMaterialSearchList(); @@ -140,13 +149,14 @@ internal ReadonlyMaterialMap mappings { get { - m_Mappings ??= new ReadonlyMaterialMap(materialMappings); + m_Mappings ??= new ReadonlyMaterialMap(materialMappings, GetSourceMaterials); return m_Mappings; } } public override void BeforeConvert() { + base.BeforeConvert(); m_MaterialReferenceChanger = new MaterialReferenceChanger(mappings); } @@ -154,6 +164,7 @@ public override void AfterConvert() { m_MaterialReferenceChanger?.Dispose(); m_MaterialReferenceChanger = null; + base.AfterConvert(); } protected override Status ConvertObject(UnityEngine.Object obj, StringBuilder message) diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterAssetItem.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterAssetItem.cs index bb4626e60bd..e12f2d34b3f 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterAssetItem.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterAssetItem.cs @@ -79,8 +79,21 @@ public string info set => m_Info = value; } - public bool isEnabled { get; set; } = true; - public string isDisabledMessage { get; set; } = string.Empty; + [SerializeField] + private bool m_IsEnabled = true; + public bool isEnabled + { + get => m_IsEnabled; + set => m_IsEnabled = value; + } + + [SerializeField] + private string m_IsDisabledMessage = string.Empty; + public string isDisabledMessage + { + get => m_IsDisabledMessage; + set => m_IsDisabledMessage = value; + } public Texture2D icon { diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterManager.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterManager.cs index 7dcfc84eea9..7756030fa7f 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterManager.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterManager.cs @@ -71,15 +71,17 @@ internal void Reset() public void OnBeforeSerialize() { - // TODO: As the converters have data stored inside during initialization we need to clear the states - // Once we keep the items to be classes and converters can inherit them to store any kind of data this can be removed + // Clear event handlers before serialization + // They will be re-established by the window during CreateGUI after reload foreach (var state in converterStates) - state.Clear(); + { + state.OnBeforeSerialize(); + } } public void OnAfterDeserialize() { - // Something null, remove it + // Remove null states for (int i = converterStates.Count - 1; i >= 0; i--) { if (converterStates[i] == null || converterStates[i].converter == null) @@ -87,6 +89,12 @@ public void OnAfterDeserialize() converterStates.RemoveAt(i); } } + + // Clean up invalid items and rebuild trees + foreach (var state in converterStates) + { + state.OnAfterDeserialize(); + } } } } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterMaterialUpgrader.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterMaterialUpgrader.cs index 5c9bb00b331..265638d5077 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterMaterialUpgrader.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterMaterialUpgrader.cs @@ -8,15 +8,18 @@ namespace UnityEditor.Rendering.Converter [Serializable] internal class RenderPipelineConverterMaterialUpgraderItem : IRenderPipelineConverterItem { - public string assetPath { get; } - public List variantsPaths { get; } - - public string name { get; } - - public string info { get; } - - public bool isEnabled { get; set; } = true; - public string isDisabledMessage { get; set; } = string.Empty; + [SerializeField] + private string m_AssetPath; + public string assetPath => m_AssetPath; + + [SerializeField] + private List m_VariantsPaths; + public List variantsPaths => m_VariantsPaths; + + [field:SerializeField] public string name { get; set; } + [field:SerializeField] public string info { get; set; } + [field:SerializeField] public bool isEnabled { get; set; } = true; + [field:SerializeField] public string isDisabledMessage { get; set; } public Texture2D icon { @@ -42,8 +45,8 @@ public RenderPipelineConverterMaterialUpgraderItem(string shaderPath, string mat if (string.IsNullOrEmpty(materialPath)) throw new ArgumentException(nameof(materialPath)); - assetPath = materialPath; - this.variantsPaths = variantsPaths; + m_AssetPath = materialPath; + m_VariantsPaths = variantsPaths; if (material == null) throw new ArgumentException($"Unable to load material at path {materialPath}"); @@ -170,6 +173,9 @@ public void Scan(Action> onScanFinish) public Status Convert(IRenderPipelineConverterItem item, out string message) { + // Lazy initialization of upgraders cache (needed after assembly reload since it's not serialized) + m_UpgradersCache ??= upgraders; + if (item is not RenderPipelineConverterMaterialUpgraderItem materialUpgraderItem) { message = $"Item is not a {nameof(RenderPipelineConverterMaterialUpgraderItem)}."; diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs index c089b1f8c6f..6bdd92e38e9 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs @@ -15,13 +15,40 @@ internal static class RenderPipelineConverterUtility /// /// Helper class to represent a folder item with children /// + [Serializable] internal class FolderItem : IFolderRenderPipelineConverterItem { - public string name { get; set; } - public string info { get; set; } - public bool isEnabled { get; set; } = true; - public string isDisabledMessage { get; set; } - public IList children { get; set; } = new List(); + [field:SerializeField] public string name { get; set; } + [field:SerializeField] public string info { get; set; } + + /// + /// Folder is enabled if at least one child is enabled. + /// This follows Unity Editor Design System guidelines: disable controls that aren't available right now. + /// + public bool isEnabled + { + get + { + // A folder is enabled if any of its children are enabled + foreach (var child in m_Children) + { + if (child.isEnabled) + return true; + } + return false; + } + set { /* Computed property, setter is no-op */ } + } + + [field:SerializeField] public string isDisabledMessage { get; set; } + + [SerializeReference] + private List m_Children = new List(); + public IList children + { + get => m_Children; + set => m_Children = value == null ? new List() : (value as List ?? new List(value)); + } public virtual Texture2D icon => s_FolderIconCache ??= EditorGUIUtility.FindTexture("Folder Icon"); @@ -32,21 +59,64 @@ public void OnClicked() { } /// Helper class to represent an asset group (not a file system folder) with children /// Shows an asset type icon instead of a folder icon /// - internal class AssetGroupItem : IFolderRenderPipelineConverterItem + [Serializable] + internal class AssetGroupItem : IFolderRenderPipelineConverterItem, ISerializationCallbackReceiver { - public string name { get; set; } - public string info { get; set; } - public bool isEnabled { get; set; } = true; - public string isDisabledMessage { get; set; } - public IList children { get; set; } = new List(); + [field:SerializeField] public string name { get; set; } + [field:SerializeField] public string info { get; set; } + + /// + /// Asset group is enabled if at least one child is enabled. + /// This follows Unity Editor Design System guidelines: disable controls that aren't available right now. + /// + public bool isEnabled + { + get + { + // An asset group is enabled if any of its children are enabled + foreach (var child in m_Children) + { + if (child.isEnabled) + return true; + } + return false; + } + set { /* Computed property, setter is no-op */ } + } + + [field:SerializeField] public string isDisabledMessage { get; set; } + + [SerializeReference] + private List m_Children = new List(); + public IList children + { + get => m_Children; + set => m_Children = value == null ? new List() : (value as List ?? new List(value)); + } /// /// The asset type to use for the icon (e.g., typeof(Material), typeof(Shader)) /// public System.Type assetType { get; set; } + [SerializeField] + private string m_AssetTypeAssemblyQualifiedName; + private Texture2D m_CachedIcon; + public void OnBeforeSerialize() + { + m_AssetTypeAssemblyQualifiedName = assetType?.AssemblyQualifiedName; + } + + public void OnAfterDeserialize() + { + if (!string.IsNullOrEmpty(m_AssetTypeAssemblyQualifiedName)) + { + assetType = System.Type.GetType(m_AssetTypeAssemblyQualifiedName); + } + } + public virtual Texture2D icon { get @@ -66,11 +136,12 @@ public void Reset() { name = null; info = null; - isEnabled = true; + // isEnabled is now computed from children, no need to reset it isDisabledMessage = null; assetType = null; + m_AssetTypeAssemblyQualifiedName = null; m_CachedIcon = null; - children.Clear(); + m_Children.Clear(); } } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderSettingsConverter.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderSettingsConverter.cs index e67b25f6499..86ea0d80000 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderSettingsConverter.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderSettingsConverter.cs @@ -20,19 +20,12 @@ internal static class RenderPipelineTarget [Serializable] internal class RenderSettingsConverterItem : IRenderPipelineConverterItem { - public int qualityLevelIndex { get; set; } - - public string name { get; set; } - - public string info { get; set; } - - public bool isEnabled { get; set; } - public string isDisabledMessage { get; set; } - - /// - /// Settings path to open when clicked (e.g., "Project/Quality" or "Project/Graphics") - /// - public string settingsPath { get; set; } = "Project/Quality"; + [field:SerializeField] public int qualityLevelIndex { get; set; } + [field:SerializeField] public string name { get; set; } + [field:SerializeField] public string info { get; set; } + [field:SerializeField] public bool isEnabled { get; set; } = true; + [field:SerializeField] public string isDisabledMessage { get; set; } + [field:SerializeField] public string settingsPath { get; set; } = "Project/Quality"; // Settings path to open when clicked (e.g., "Project/Quality" or "Project/Graphics") private static Texture2D s_CachedIcon; @@ -135,89 +128,74 @@ public Status Convert(IRenderPipelineConverterItem item, out string message) { message = string.Empty; - if (item is RenderSettingsConverterItem qualityLevelItem) + if (item is not RenderSettingsConverterItem qualityLevelItem) { - if (CreateRPAssetForQualityLevel(qualityLevelItem.qualityLevelIndex, out message)) - { - if (qualityLevelItem.qualityLevelIndex == RenderPipelineTarget.DefaultGraphicsSettings) - { - message = "Default Render Pipeline Asset created and assigned to Graphics Settings."; - } - else - { - message = "Each Quality Level now has a new, unique Render Pipeline Asset, but all share identical settings. Modify each asset to restore your performance/quality tiers."; - } - return Status.Warning; - } + message = $"Item is not a {nameof(RenderSettingsConverterItem)}."; + return Status.Error; } - return Status.Error; + return CreateRPAssetForQualityLevel(qualityLevelItem.qualityLevelIndex, out message); } - private bool CreateRPAssetForQualityLevel(int qualityIndex, out string message) + private Status CreateRPAssetForQualityLevel(int qualityIndex, out string message) { - bool ok = false; message = string.Empty; if (qualityIndex == RenderPipelineTarget.DefaultGraphicsSettings) { if (GraphicsSettings.defaultRenderPipeline is RenderPipelineAsset rpAsset) { - message = $"Graphics Settings already reference a default Render Pipeline Asset: {rpAsset.name}."; + message = $"Graphics Settings already references a Render Pipeline Asset: {rpAsset.name}."; + return Status.Warning; } - else + + var asset = CreateAsset("DefaultRenderPipeline"); + if (asset == null) { - var asset = CreateAsset("DefaultRenderPipeline"); - - if (asset != null) - { - SetPipelineSettings(asset); - EditorUtility.SetDirty(asset); - AssetDatabase.SaveAssetIfDirty(asset); - - GraphicsSettings.defaultRenderPipeline = asset; - ok = true; - } - else - { - message = "Failed to create Render Pipeline Asset for Graphics Settings."; - } + message = "Failed to create Render Pipeline Asset for Graphics Settings."; + return Status.Error; } - return ok; + SetPipelineSettings(asset); + EditorUtility.SetDirty(asset); + AssetDatabase.SaveAssetIfDirty(asset); + GraphicsSettings.defaultRenderPipeline = asset; + + message = "Default Render Pipeline Asset created and assigned to Graphics Settings."; + return Status.Warning; } var currentQualityLevel = QualitySettings.GetQualityLevel(); - QualitySettings.SetQualityLevel(qualityIndex); - if (QualitySettings.renderPipeline is RenderPipelineAsset qualityRPAsset) + try { - message = $"Quality Level {qualityIndex} already references a Render Pipeline Asset: {qualityRPAsset.name}."; - } - else - { - var asset = CreateAsset($"{QualitySettings.names[qualityIndex]}"); - - if (asset != null) + if (QualitySettings.renderPipeline is RenderPipelineAsset qualityRPAsset) { - SetPipelineSettings(asset); - EditorUtility.SetDirty(asset); - AssetDatabase.SaveAssetIfDirty(asset); - - QualitySettings.renderPipeline = asset; - ok = true; + message = $"Quality Level {qualityIndex} already references a Render Pipeline Asset: {qualityRPAsset.name}."; + return Status.Warning; } - else + + var asset = CreateAsset($"{QualitySettings.names[qualityIndex]}"); + if (asset == null) { message = "Failed to create Render Pipeline Asset."; + return Status.Error; } - } - // Restore back the quality level - QualitySettings.SetQualityLevel(currentQualityLevel); + SetPipelineSettings(asset); + EditorUtility.SetDirty(asset); + AssetDatabase.SaveAssetIfDirty(asset); + QualitySettings.renderPipeline = asset; - return ok; + message = "Render Pipeline Asset created. Modify each asset to restore your performance/quality tiers."; + return Status.Warning; + } + finally + { + // Restore back the quality level + QualitySettings.SetQualityLevel(currentQualityLevel); + } } protected abstract RenderPipelineAsset CreateAsset(string name); diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/SearchServiceUtils.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/SearchServiceUtils.cs index 157fe36abb4..462c266c5fd 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/SearchServiceUtils.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/SearchServiceUtils.cs @@ -14,15 +14,18 @@ public enum IndexingOptions PackageIndexing = 1 << 1 } + // Set while a caller blocks the Editor, so searches have to finish before RunQueuedSearch returns + public static bool synchronous { get; set; } + public static void RunQueuedSearch( IndexingOptions neededOptions, List<(string query, string description)> contextSearchQueriesAndIds, Action onAssetGUIDFound, - Action onSearchsFinished) + Action onSearchesFinished) { if (contextSearchQueriesAndIds == null || contextSearchQueriesAndIds.Count == 0) { - onSearchsFinished?.Invoke(); + onSearchesFinished?.Invoke(); return; } @@ -32,6 +35,41 @@ public static void RunQueuedSearch( bool isDeepSearchEnabled = Search.SearchService.IsDeepIndexingEnabled(); bool isPackageIndexingEnabled = Search.SearchService.IsPackageIndexingEnabled(); + if (synchronous) + { + // Changing the indexing settings rebuilds the index over several Editor updates, which a blocked caller can't wait for + if (isDeepSearchEnabled != needsDeepSearch || isPackageIndexingEnabled != needsPackageIndexing) + { + throw new InvalidOperationException( + "A synchronous scan can't change the Search indexing settings. In Preferences > Search > Indexing, " + + $"set 'Deep scene and prefab indexing' to {(needsDeepSearch ? "enabled" : "disabled")} and 'Packages indexing' to {(needsPackageIndexing ? "enabled" : "disabled")}, then scan again."); + } + + foreach (var (query, id) in contextSearchQueriesAndIds) + { + using (var context = Search.SearchService.CreateContext(query)) + { + Search.SearchService.Request(context, (searchContext, searchItems) => + { + foreach (var item in searchItems) + { + try + { + onAssetGUIDFound.Invoke(item, id); + } + catch (Exception e) + { + UnityEngine.Debug.LogException(e); + } + } + }, Search.SearchFlags.Synchronous); + } + } + + onSearchesFinished?.Invoke(); + return; + } + int index = 0; void ProcessNextSearch() { @@ -44,12 +82,12 @@ void ProcessNextSearch() Search.SearchService.ChangeIndexingSettings( deepIndexing: isDeepSearchEnabled, packageIndexing: isPackageIndexingEnabled, - () => onSearchsFinished?.Invoke()); + () => onSearchesFinished?.Invoke()); } else { // No need to rollback the index info, the initialization is done now - onSearchsFinished?.Invoke(); + onSearchesFinished?.Invoke(); } return; } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.cs index 0bc67d2d24a..5b5513cd966 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.cs @@ -7,6 +7,16 @@ namespace UnityEditor.Rendering.Converter { + /// + /// User data stored in toggle elements for folder items in the tree view. + /// Contains the node data and optional callback for event cleanup. + /// + internal struct FolderToggleUserData + { + public TreeNodeData nodeData; + public EventCallback> callback; + } + internal class RenderPipelineConverterVisualElement : VisualElement { const string k_Uxml = "Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.uxml"; @@ -95,12 +105,14 @@ public RenderPipelineConverterVisualElement(Node converterInfo) { SetItemsActive(true); Refresh(); + converterSelected?.Invoke(); }); var noneLabel = m_RootVisualElement.Q