// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.SceneSystem; using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Editor { /// /// Draws the scene info struct and populates its hidden fields. /// [CustomPropertyDrawer(typeof(SceneInfo))] public class SceneInfoDrawer : PropertyDrawer { /// /// Used to control whether to draw the tag property. /// All scenes can have tags, but they're not always relevant based on how the scene is being used. /// Not sure how much I like this method of controlling property drawing since it could result in unpredictable behavior in inspectors. /// We could add an enum or bool to the SceneInfo struct to control this, but that seemed like unnecessary clutter. /// public static bool DrawTagProperty { get; set; } const float iconWidth = 20f; const float totalPropertyWidth = 410; const float assetPropertyWidth = 400; const float tagPropertyWidth = 400; const float buttonPropertyWidth = 400; const float assetLabelWidth = 150; const float tagLabelWidth = 40; const string enabledIconContent = "TestPassed"; const string missingIconContent = "TestIgnored"; const string disabledIconContent = "TestNormal"; const string warningIconContent = "TestInconclusive"; const string errorIconContent = "TestFailed"; static RectOffset boxOffset; static GUIStyle italicStyle; public static float GetPropertyHeight(bool drawTagProperty) { return (EditorGUIUtility.standardVerticalSpacing + EditorGUIUtility.singleLineHeight) * (drawTagProperty ? 4 : 3); } public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return GetPropertyHeight(DrawTagProperty); } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { DrawProperty(position, property, label); } public static void DrawProperty(Rect position, SerializedProperty property, GUIContent label, bool isActive = false, bool isSelected = false) { SerializedProperty assetProperty, nameProperty, pathProperty, buildIndexProperty, includedProperty, tagProperty; SceneInfoUtils.GetSceneInfoRelativeProperties(property, out assetProperty, out nameProperty, out pathProperty, out buildIndexProperty, out includedProperty, out tagProperty); // Set up our properties and settings boxOffset = EditorStyles.helpBox.padding; if (italicStyle == null) { italicStyle = new GUIStyle(EditorStyles.label); } bool lastMode = EditorGUIUtility.wideMode; int lastIndentLevel = EditorGUI.indentLevel; EditorGUIUtility.wideMode = true; EditorGUI.BeginProperty(position, label, property); GUI.color = isActive ? Color.gray : GUI.backgroundColor; GUI.color = isSelected ? Color.blue : GUI.backgroundColor; // Indent our rect, then reset indent to 0 so sub-properties don't get doubly indented position = EditorGUI.IndentedRect(position); EditorGUI.indentLevel = 0; // Draw a box around our item Rect boxPosition = position; boxPosition.height = (EditorGUIUtility.singleLineHeight * (DrawTagProperty ? 4 : 3)) - EditorGUIUtility.standardVerticalSpacing; GUI.Box(boxPosition, GUIContent.none, EditorStyles.helpBox); position = boxOffset.Remove(position); Rect iconRect = position; iconRect.width = iconWidth; Rect assetRect = position; assetRect.width = position.width - iconWidth; assetRect.height = EditorGUIUtility.singleLineHeight; assetRect.x += iconWidth; Rect buttonRect = position; buttonRect.height = EditorGUIUtility.singleLineHeight; buttonRect.y += (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing); Rect tagRect = position; tagRect.height = EditorGUIUtility.singleLineHeight; tagRect.y += ((EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing) * 2) + EditorGUIUtility.standardVerticalSpacing; bool changed = false; UnityEngine.Object asset = assetProperty.objectReferenceValue; if (!Application.isPlaying && !EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isCompiling) { // This is expensive so don't refresh during play mode or while other stuff is going on changed = SceneInfoUtils.RefreshSceneInfo(asset, nameProperty, pathProperty, buildIndexProperty, includedProperty, tagProperty); } GUIContent labelContent = null; GUIContent iconContent = null; italicStyle.fontStyle = FontStyle.Normal; bool buttonsDisabled = false; bool tagDisabled = false; if (asset == null) { string missingSceneName = nameProperty.stringValue; if (!string.IsNullOrEmpty(missingSceneName)) { labelContent = new GUIContent(" (" + missingSceneName + ")"); labelContent.tooltip = "The scene " + missingSceneName + " is missing. It will not be available to load."; italicStyle.fontStyle = FontStyle.Italic; tagDisabled = true; iconContent = EditorGUIUtility.IconContent(missingIconContent); } else { labelContent = new GUIContent(" (Empty)"); labelContent.tooltip = "This scene is empty. You should assign a scene object before building."; buttonsDisabled = true; tagDisabled = true; iconContent = EditorGUIUtility.IconContent(missingIconContent); } } else { if (includedProperty.boolValue) { if (buildIndexProperty.intValue >= 0) { labelContent = new GUIContent(" Build index: " + buildIndexProperty.intValue); labelContent.tooltip = "This scene is in build settings at index " + buildIndexProperty.intValue; iconContent = EditorGUIUtility.IconContent(enabledIconContent); } else { labelContent = new GUIContent(" (Disabled)"); labelContent.tooltip = "This scene is in build settings at index " + buildIndexProperty.intValue + ", but it has been disabled and will not be available to load."; iconContent = EditorGUIUtility.IconContent(disabledIconContent); } } else { labelContent = new GUIContent(" (Not included in build)"); labelContent.tooltip = "This scene is not included in build settings and will not be available to load."; iconContent = EditorGUIUtility.IconContent(errorIconContent); } } // Draw our icon EditorGUI.LabelField(iconRect, iconContent); // Draw our object field EditorGUI.BeginDisabledGroup(Application.isPlaying); EditorGUIUtility.labelWidth = assetLabelWidth; asset = EditorGUI.ObjectField(assetRect, labelContent, assetProperty.objectReferenceValue, typeof(SceneAsset), false); EditorGUI.EndDisabledGroup(); if (DrawTagProperty) { // Draw our tag field EditorGUI.BeginDisabledGroup(tagDisabled || Application.isPlaying); EditorGUIUtility.labelWidth = tagLabelWidth; changed |= EditorGUI.PropertyField(tagRect, tagProperty); EditorGUI.EndDisabledGroup(); } // Draw our button EditorGUI.BeginDisabledGroup(buttonsDisabled || Application.isPlaying); if (!string.IsNullOrEmpty(pathProperty.stringValue) && asset == null) { // The scene is missing // This may be due to a local file ID mismatch // Try to find it based on guid first asset = AssetDatabase.LoadAssetAtPath(pathProperty.stringValue); } if (!string.IsNullOrEmpty(nameProperty.stringValue) && asset == null) { // If we still can't find it, draw a button that lets people attempt to recover it if (GUI.Button(buttonRect, "Search for missing scene", EditorStyles.toolbarButton)) { changed |= SceneInfoUtils.FindScene(nameProperty, pathProperty, ref asset); } } else { // It's not included in build settings if (!includedProperty.boolValue) { // The scene exists but it isn't in our build settings // Show a button that lets us add it if (GUI.Button(buttonRect, "Add to build settings", EditorStyles.toolbarButton) && asset != null) { List scenes = new List(EditorBuildSettings.scenes); scenes.Add(new EditorBuildSettingsScene(pathProperty.stringValue, true)); includedProperty.boolValue = true; EditorBuildSettings.scenes = scenes.ToArray(); SceneInfoUtils.RefreshCachedScenes(); changed = true; } } else { bool enabledInBuild = buildIndexProperty.intValue >= 0; // The scene exists and is in build settings // Show enable / disable toggle if (GUI.Button(buttonRect, enabledInBuild ? "Disable in build settings" : "Enable in build settings", EditorStyles.toolbarButton)) { enabledInBuild = !enabledInBuild; // Modify a local copy of our scenes instead of using the cached scenes EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes; // Find the scene in our build settings and enable / disable it int sceneCount = 0; int buildIndex = -1; for (int i = 0; i < SceneInfoUtils.CachedScenes.Length; i++) { if (scenes[i].path == pathProperty.stringValue) { scenes[i].enabled = enabledInBuild; if (scenes[i].enabled) { // Only store the build index if it's enabled buildIndex = sceneCount; } break; } if (scenes[i].enabled) { // Disabled scenes don't count toward scene count sceneCount++; } } EditorBuildSettings.scenes = scenes; SceneInfoUtils.RefreshCachedScenes(); buildIndexProperty.intValue = buildIndex; changed = true; } } } EditorGUI.EndDisabledGroup(); if (asset != assetProperty.objectReferenceValue) { assetProperty.objectReferenceValue = asset; changed = true; } if (changed) { property.serializedObject.ApplyModifiedProperties(); } EditorGUIUtility.wideMode = lastMode; EditorGUI.indentLevel = lastIndentLevel; EditorGUI.EndProperty(); } } }