using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; using UnityEditor.AnimatedValues; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Rendering; using UnityEngine.SceneManagement; using UnityEditor.PackageManager; namespace UnityEditor.Rendering { using UnityObject = UnityEngine.Object; /// Utility class for Editor public static class CoreEditorUtils { static GraphicsDeviceType[] m_BuildTargets; /// Build targets public static GraphicsDeviceType[] buildTargets => m_BuildTargets ?? (m_BuildTargets = PlayerSettings.GetGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget)); static CoreEditorUtils() { LoadSkinAndIconMethods(); } // Serialization helpers /// /// To use with extreme caution. It not really get the property but try to find a field with similar name /// Hence inheritance override of property is not supported. /// Also variable rename will silently break the search. /// /// Entry type of expr /// Type of the value /// Expression returning the value seeked /// serialization path of the seeked property public static string FindProperty(Expression> expr) { // Get the field path as a string MemberExpression me; switch (expr.Body.NodeType) { case ExpressionType.MemberAccess: me = expr.Body as MemberExpression; break; default: throw new InvalidOperationException(); } var members = new List(); while (me != null) { // For field, get the field name // For properties, get the name of the backing field var name = me.Member is FieldInfo ? me.Member.Name : "m_" + me.Member.Name.Substring(0, 1).ToUpperInvariant() + me.Member.Name.Substring(1); members.Add(name); me = me.Expression as MemberExpression; } var sb = new StringBuilder(); for (int i = members.Count - 1; i >= 0; i--) { sb.Append(members[i]); if (i > 0) sb.Append('.'); } return sb.ToString(); } // UI Helpers /// Creates a 1x1 with a plain /// The color to fill the texture /// The name of the texture /// a public static Texture2D CreateColoredTexture2D(Color color, string textureName) { Texture2D tex2 = new Texture2D(1, 1) { hideFlags = HideFlags.HideAndDontSave, name = textureName }; tex2.SetPixel(1, 1, color); tex2.Apply(); return tex2; } const float k_IndentMargin = 15.0f; const float k_HighlightDuration = 2.0f; static float s_HighlightStart = -1.0f; static Texture2D s_HighlightBackground; static object s_View; static readonly FieldInfo k_ViewInfo = typeof(Highlighter).GetField("s_View", BindingFlags.Static | BindingFlags.NonPublic); static readonly FieldInfo k_HighlightStyleInfo = typeof(Highlighter).GetField("s_HighlightStyle", BindingFlags.Static | BindingFlags.NonPublic); static readonly FieldInfo k_WindowBackendInfo = Type.GetType("UnityEditor.GUIView,UnityEditor").GetField("m_WindowBackend", BindingFlags.NonPublic | BindingFlags.Instance); static readonly EventInfo k_GUIHandlerInfo = Type.GetType("UnityEditor.UIElements.DefaultEditorWindowBackend,UnityEditor").GetEvent("overlayGUIHandler", (BindingFlags)(-1)); static readonly MethodInfo k_Repaint = Type.GetType("UnityEditor.GUIView,UnityEditor").GetMethod("Repaint", (BindingFlags)(-1)); static void HighlightTimeout() { if (!Highlighter.active) { if (s_HighlightBackground != null) (k_HighlightStyleInfo.GetValue(null) as GUIStyle).normal.background = s_HighlightBackground; s_HighlightBackground = null; EditorApplication.update -= HighlightTimeout; s_HighlightStart = -1.0f; return; } // Item is in view for the first time, register highlight drawer delegate if (Highlighter.activeVisible && s_HighlightStart <= 0.0f) { s_HighlightStart = Time.realtimeSinceStartup; s_View = k_ViewInfo.GetValue(null); if (s_View != null) { var windowBackend = k_WindowBackendInfo.GetValue(s_View); k_GUIHandlerInfo.AddEventHandler(windowBackend, (Action)ControlHighlightGUI); var style = k_HighlightStyleInfo.GetValue(null) as GUIStyle; s_HighlightBackground = style.normal.background; style.normal.background = null; } else { Highlighter.Stop(); } } if (s_HighlightStart > 0.0f) { if (Time.realtimeSinceStartup - s_HighlightStart > k_HighlightDuration) { Highlighter.Stop(); var windowBackend = k_WindowBackendInfo.GetValue(s_View); k_GUIHandlerInfo.RemoveEventHandler(windowBackend, (Action)ControlHighlightGUI); } } } static void ControlHighlightGUI() { if (Event.current.type != EventType.Repaint) return; var color = CoreEditorStyles.backgroundHighlightColor; color.a = Mathf.Min(1.0f - (Time.realtimeSinceStartup - s_HighlightStart) / k_HighlightDuration, 0.8f); EditorGUI.DrawRect(GUIUtility.ScreenToGUIRect(Highlighter.activeRect), color); } /// Highlights an element in the editor for a short period of time. /// The title of the window the element is inside. /// The text to identify the element with. /// Optional mode to specify how to search for the element. public static void Highlight(string windowTitle, string text, HighlightSearchMode mode = HighlightSearchMode.Auto) { if (s_HighlightStart >= 0.0f) return; s_HighlightStart = 0.0f; Highlighter.Highlight(windowTitle, text, mode); EditorApplication.update += HighlightTimeout; } /// Draw a help box with the Fix button. /// The message text. /// When the user clicks the button, Unity performs this action. public static void DrawFixMeBox(string message, Action action) { DrawFixMeBox(message, MessageType.Warning, "Fix", action); } /// Draw a help box with the Fix button. /// The message text. /// The type of the message. /// When the user clicks the button, Unity performs this action. public static void DrawFixMeBox(string message, MessageType messageType, Action action) { DrawFixMeBox(EditorGUIUtility.TrTextContentWithIcon(message, CoreEditorStyles.GetMessageTypeIcon(messageType)), "Fix", action); } /// Draw a help box with the Fix button. /// The message text. /// The type of the message. /// The button text. /// When the user clicks the button, Unity performs this action. public static void DrawFixMeBox(string message, MessageType messageType, string buttonLabel, Action action) { DrawFixMeBox(EditorGUIUtility.TrTextContentWithIcon(message, CoreEditorStyles.GetMessageTypeIcon(messageType)), buttonLabel, action); } /// Draw a help box with the Fix button. /// The message with icon if needed. /// When the user clicks the button, Unity performs this action. public static void DrawFixMeBox(GUIContent message, Action action) { DrawFixMeBox(message, "Fix", action); } /// Draw a help box with the Fix button. /// The message with icon if needed. /// The button text. /// When the user clicks the button, Unity performs this action. public static void DrawFixMeBox(GUIContent message, string buttonLabel, Action action) { EditorGUILayout.BeginHorizontal(); float indent = EditorGUI.indentLevel * k_IndentMargin - EditorStyles.helpBox.margin.left; GUILayoutUtility.GetRect(indent, EditorGUIUtility.singleLineHeight, EditorStyles.helpBox, GUILayout.ExpandWidth(false)); Rect leftRect = GUILayoutUtility.GetRect(new GUIContent(buttonLabel), EditorStyles.miniButton, GUILayout.MinWidth(60), GUILayout.ExpandWidth(false)); Rect rect = GUILayoutUtility.GetRect(message, EditorStyles.helpBox); Rect boxRect = new Rect(leftRect.x, rect.y, rect.xMax - leftRect.xMin, rect.height); int oldIndent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; if (Event.current.type == EventType.Repaint) EditorStyles.helpBox.Draw(boxRect, false, false, false, false); Rect labelRect = new Rect(boxRect.x + 4, boxRect.y, rect.width - 8, rect.height); EditorGUI.LabelField(labelRect, message, CoreEditorStyles.helpBox); var buttonRect = leftRect; buttonRect.x += rect.width - 2; buttonRect.y = rect.yMin + (rect.height - EditorGUIUtility.singleLineHeight) / 2; bool clicked = GUI.Button(buttonRect, buttonLabel); EditorGUI.indentLevel = oldIndent; EditorGUILayout.EndHorizontal(); if (clicked) action(); } /// /// Draw a multiple field property /// /// Label of the whole /// Properties /// Sub-labels public static void DrawMultipleFields(string label, SerializedProperty[] ppts, GUIContent[] labels) => DrawMultipleFields(EditorGUIUtility.TrTextContent(label), ppts, labels); private static float GetLongestLabelWidth(GUIContent[] labels) { float labelWidth = 0.0f; for (var i = 0; i < labels.Length; ++i) labelWidth = Mathf.Max(EditorStyles.label.CalcSize(labels[i]).x, labelWidth); return labelWidth; } /// /// Draws an for the given property /// /// The type of Enum that the drop down menu will contain. /// The rect where the drop down will be drawn /// The label for the drop down /// The to modify public static void DrawEnumPopup(Rect rect, GUIContent label, SerializedProperty serializedProperty) where TEnum : Enum { using (new EditorGUI.MixedValueScope(serializedProperty.hasMultipleDifferentValues)) { EditorGUI.BeginChangeCheck(); var newValue = (TEnum)EditorGUI.EnumPopup(rect, label, serializedProperty.GetEnumValue()); if (EditorGUI.EndChangeCheck()) serializedProperty.SetEnumValue(newValue); } EditorGUI.EndProperty(); } /// /// Draw a multiple field property /// /// Label of the whole /// Properties /// Sub-labels public static void DrawMultipleFields(GUIContent label, SerializedProperty[] ppts, GUIContent[] labels) { var labelWidth = EditorGUIUtility.labelWidth; using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.PrefixLabel(label); using (new EditorGUILayout.VerticalScope()) { EditorGUIUtility.labelWidth = GetLongestLabelWidth(labels) + CoreEditorConstants.standardHorizontalSpacing; int oldIndentLevel = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; for (var i = 0; i < ppts.Length; ++i) EditorGUILayout.PropertyField(ppts[i], labels[i]); EditorGUI.indentLevel = oldIndentLevel; } } EditorGUIUtility.labelWidth = labelWidth; } /// /// Draw a multiple field property /// /// A valid /// Label of the whole /// The labels mapping the values /// The values to be displayed public static void DrawMultipleFields(GUIContent label, GUIContent[] labels, T[] values) where T : struct { var labelWidth = EditorGUIUtility.labelWidth; using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.PrefixLabel(label); using (new EditorGUILayout.VerticalScope()) { EditorGUIUtility.labelWidth = GetLongestLabelWidth(labels) + CoreEditorConstants.standardHorizontalSpacing; int oldIndentLevel = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; for (var i = 0; i < values.Length; ++i) { // Draw the right field depending on its type. if (typeof(T) == typeof(int)) values[i] = (T)(object)EditorGUILayout.DelayedIntField(labels[i], (int)(object)values[i]); else if (typeof(T) == typeof(bool)) values[i] = (T)(object)EditorGUILayout.Toggle(labels[i], (bool)(object)values[i]); else if (typeof(T) == typeof(float)) values[i] = (T)(object)EditorGUILayout.FloatField(labels[i], (float)(object)values[i]); else if (typeof(T).IsEnum) values[i] = (T)(object)EditorGUILayout.EnumPopup(labels[i], (Enum)(object)values[i]); else throw new ArgumentOutOfRangeException($"<{typeof(T)}> is not a supported type for multi field"); } EditorGUI.indentLevel = oldIndentLevel; } } EditorGUIUtility.labelWidth = labelWidth; } /// Draw a splitter separator /// [Optional] add margin if the splitter is boxed public static void DrawSplitter(bool isBoxed = false) { var rect = GUILayoutUtility.GetRect(1f, 1f); DrawSplitter(rect, isBoxed); } /// Draw a splitter separator /// The rect where to draw the splitter /// [Optional] add margin if the splitter is boxed public static void DrawSplitter(Rect rect, bool isBoxed = false) { // Splitter rect should be full-width if (!isBoxed) { rect = ToFullWidth(rect); } if (Event.current.type != EventType.Repaint) return; EditorGUI.DrawRect(rect, !EditorGUIUtility.isProSkin ? new Color(0.6f, 0.6f, 0.6f, 1.333f) : new Color(0.12f, 0.12f, 0.12f, 1.333f)); } /// Draw a splitter separator which is used after drawing a fouldout header. /// [Optional] add margin if the splitter is boxed public static void DrawFoldoutEndSplitter(bool isBoxed = false) { var rect = GUILayoutUtility.GetRect(1f, 1f); if (!isBoxed) { rect = ToFullWidth(rect); } if (Event.current.type != EventType.Repaint) return; EditorGUI.DrawRect(rect, !EditorGUIUtility.isProSkin ? new Color(0.73f, 0.73f, 0.73f, 1.333f) : new Color(0.19f, 0.19f, 0.19f, 1.333f)); } /// Draw a header /// Title of the header public static void DrawHeader(string title) => DrawHeader(EditorGUIUtility.TrTextContent(title)); /// Draw a header /// Title of the header public static void DrawHeader(GUIContent title) { var backgroundRect = GUILayoutUtility.GetRect(1f, 17f); var labelRect = backgroundRect; labelRect.xMin += 16f; labelRect.xMax -= 20f; var foldoutRect = backgroundRect; foldoutRect.y += 1f; foldoutRect.width = 13f; foldoutRect.height = 13f; // Background rect should be full-width backgroundRect = ToFullWidth(backgroundRect); DrawBackground(backgroundRect, false); // Title EditorGUI.LabelField(labelRect, title, EditorStyles.boldLabel); } /// Draw a foldout header /// The title of the header /// The state of the header /// [optional] is the header contained in a box style ? /// [optional] Delegate used to draw the right state of the advanced button. If null, no button drawn. /// [optional] Callback call when advanced button clicked. Should be used to toggle its state. /// [optional] is this a title header, this setting controls the color used for the foldout /// [optional] The URL that the Unity Editor opens when the user presses the help button on the header. /// [optional] The callback that the Unity Editor executes when the user presses the burger menu on the header. /// [optional] Delegate which adds items to a generic menu when the user presses the burger menu on the header. /// return the state of the foldout header public static bool DrawHeaderFoldout(string title, bool state, bool isBoxed = false, Func hasMoreOptions = null, Action toggleMoreOption = null, bool isTitleHeader = false, string documentationURL = "", Action contextAction = null, Action customMenuContextAction = null) => DrawHeaderFoldout(EditorGUIUtility.TrTextContent(title), state, isBoxed, hasMoreOptions, toggleMoreOption, isTitleHeader, documentationURL, contextAction, customMenuContextAction); /// Draw a foldout header /// The title of the header /// The state of the header /// [optional] is the eader contained in a box style ? /// [optional] Delegate used to draw the right state of the advanced button. If null, no button drawn. /// [optional] Callback call when advanced button clicked. Should be used to toggle its state. /// [optional] is this a title header, this setting controls the color used for the foldout /// [optional] The URL that the Unity Editor opens when the user presses the help button on the header. /// [optional] The callback that the Unity Editor executes when the user presses the burger menu on the header. /// [optional] Delegate which adds items to a generic menu when the user presses the burger menu on the header. /// return the state of the foldout header public static bool DrawHeaderFoldout(GUIContent title, bool state, bool isBoxed = false, Func hasMoreOptions = null, Action toggleMoreOptions = null, bool isTitleHeader = false, string documentationURL = "", Action contextAction = null, Action customMenuContextAction = null) { const float height = 17f; var backgroundRect = GUILayoutUtility.GetRect(1f, height); return DrawHeaderFoldout(backgroundRect, title, state, isBoxed, hasMoreOptions, toggleMoreOptions, isTitleHeader, documentationURL, contextAction, customMenuContextAction); } /// Draw a foldout header /// The rect /// The title of the header /// The state of the header /// [optional] is the eader contained in a box style ? /// [optional] Delegate used to draw the right state of the advanced button. If null, no button drawn. /// [optional] Callback call when advanced button clicked. Should be used to toggle its state. /// [optional] is this a title header, this setting controls the color used for the foldout /// [optional] The URL that the Unity Editor opens when the user presses the help button on the header. /// [optional] The callback that the Unity Editor executes when the user presses the burger menu on the header. /// [optional] Delegate which adds items to a generic menu when the user presses the burger menu on the header. /// return the state of the foldout header public static bool DrawHeaderFoldout(Rect backgroundRect, GUIContent title, bool state, bool isBoxed = false, Func hasMoreOptions = null, Action toggleMoreOptions = null, bool isTitleHeader = false, string documentationURL = "", Action contextAction = null, Action customMenuContextAction = null) { const float iconRectSize = 13f; if (backgroundRect.xMin != 0) // Fix for material editor backgroundRect.xMin = 1 + 15f * (EditorGUI.indentLevel + 1); float xMin = backgroundRect.xMin; var labelRect = backgroundRect; labelRect.xMin += 16f; labelRect.xMax -= 20f; var foldoutRect = backgroundRect; foldoutRect.y += 1f; foldoutRect.width = iconRectSize; foldoutRect.height = iconRectSize; foldoutRect.x = labelRect.xMin + k_IndentMargin * (EditorGUI.indentLevel - 1); //fix for presset if (isBoxed) { labelRect.xMin += 5; foldoutRect.xMin += 5; } else { // Background rect should be full-width backgroundRect = ToFullWidth(backgroundRect); } DrawBackground(backgroundRect, isTitleHeader); // Title EditorGUI.LabelField(labelRect, title, EditorStyles.boldLabel); // Active checkbox state = GUI.Toggle(foldoutRect, state, GUIContent.none, EditorStyles.foldout); // Context menu var menuRect = new Rect(labelRect.xMax + 3f, labelRect.y + 1f, 16, 16); contextAction = CreateMenuContextAction(contextAction, hasMoreOptions, toggleMoreOptions, customMenuContextAction); CreateContextMenu(menuRect, contextAction); // Documentation button ShowHelpButton(menuRect, documentationURL, title); state = HandleEvent(state, backgroundRect, contextAction); return state; } /// Draw a foldout header /// The title of the header /// The state of the header /// [optional] is the eader contained in a box style ? /// [optional] Delegate used to draw the right state of the advanced button. If null, no button drawn. /// [optional] Callback call when advanced button clicked. Should be used to toggle its state. /// return the state of the sub foldout header [Obsolete("'More Options' versions of DrawSubHeaderFoldout are obsolete. Please use DrawSubHeaderFoldout without 'More Options'")] public static bool DrawSubHeaderFoldout(string title, bool state, bool isBoxed = false, Func hasMoreOptions = null, Action toggleMoreOptions = null) => DrawSubHeaderFoldout(EditorGUIUtility.TrTextContent(title), state, isBoxed); /// Draw a foldout header /// The title of the header /// The state of the header /// [optional] is the eader contained in a box style ? /// [optional] Delegate used to draw the right state of the advanced button. If null, no button drawn. /// [optional] Callback call when advanced button clicked. Should be used to toggle its state. /// return the state of the foldout header [Obsolete("'More Options' versions of DrawSubHeaderFoldout are obsolete. Please use DrawSubHeaderFoldout without 'More Options'")] public static bool DrawSubHeaderFoldout(GUIContent title, bool state, bool isBoxed = false, Func hasMoreOptions = null, Action toggleMoreOptions = null) => DrawSubHeaderFoldout(title, state, isBoxed); /// /// Draw a foldout sub header /// /// The title of the header /// The state of the header /// [optional] is the eader contained in a box style ? /// return the state of the sub foldout header public static bool DrawSubHeaderFoldout(string title, bool state, bool isBoxed = false) => DrawSubHeaderFoldout(EditorGUIUtility.TrTextContent(title), state, isBoxed); /// /// Draw a foldout sub header /// /// The title of the header /// The state of the header /// [optional] is the eader contained in a box style ? /// return the state of the sub foldout header public static bool DrawSubHeaderFoldout(GUIContent title, bool state, bool isBoxed = false) { const float height = 17f; const float iconRectSize = 13f; var backgroundRect = GUILayoutUtility.GetRect(1f, height); var labelRect = backgroundRect; labelRect.xMin += 16f; labelRect.xMax -= 20f; var foldoutRect = backgroundRect; foldoutRect.y += 1f; foldoutRect.x += k_IndentMargin * EditorGUI.indentLevel; //GUI do not handle indent. Handle it here foldoutRect.width = iconRectSize; foldoutRect.height = iconRectSize; if (isBoxed) { labelRect.xMin += 5; foldoutRect.xMin += 5; } else { backgroundRect = ToFullWidth(backgroundRect); } // Title EditorGUI.LabelField(labelRect, title, EditorStyles.boldLabel); // Active checkbox state = GUI.Toggle(foldoutRect, state, GUIContent.none, EditorStyles.foldout); state = HandleEvent(state, backgroundRect, null); return state; } /// Draw a header toggle like in Volumes /// The title of the header /// The group of the header /// The active field /// The context action /// Delegate saying if we have MoreOptions /// Callback called when the MoreOptions is toggled /// Documentation URL /// Delegate which adds items to a generic menu. /// States if the header toggle should be boxed. /// [optional] is this a title header, this setting controls the color used for the foldout /// States if the group and active field should update before usage and apply changes to them. /// return the state of the foldout header public static bool DrawHeaderToggle(string title, SerializedProperty group, SerializedProperty activeField, Action contextAction = null, Func hasMoreOptions = null, Action toggleMoreOptions = null, string documentationURL = null, Action customMenuContextAction = null, bool isBoxed = false, bool isTitleHeader = false, bool shouldUpdate = true) => DrawHeaderToggle(EditorGUIUtility.TrTextContent(title), group, activeField, contextAction, hasMoreOptions, toggleMoreOptions, documentationURL, customMenuContextAction, isBoxed, isTitleHeader, shouldUpdate); private static void GetHeaderToggleRects(bool isBoxed, bool hasToggle, out Rect labelRect, out Rect foldoutRect, out Rect toggleRect, out Rect backgroundRect) { backgroundRect = EditorGUI.IndentedRect(GUILayoutUtility.GetRect(1f, 17f)); labelRect = backgroundRect; labelRect.xMin += 32f; labelRect.xMax -= 20f + 16 + 5; if (!hasToggle) { labelRect.xMin -= (EditorGUI.indentLevel+1) * 15f; } foldoutRect = backgroundRect; foldoutRect.y += 1f; foldoutRect.width = 13f; foldoutRect.height = 13f; toggleRect = backgroundRect; toggleRect.x += 16f; toggleRect.y += 2f; toggleRect.width = 13f; toggleRect.height = 13f; if (!isBoxed) { // Background rect should be full-width backgroundRect = ToFullWidth(backgroundRect); } } /// Draw a header toggle like in Volumes /// The title of the header /// The group of the header /// The active field /// The context action /// Delegate saying if we have MoreOptions /// Callback called when the MoreOptions is toggled /// Documentation URL /// Delegate which adds items to a generic menu. /// States if the header toggle should be boxed. /// [optional] is this a title header, this setting controls the color used for the foldout /// States if the group and active field should update before usage and apply changes to them. /// return the state of the foldout header public static bool DrawHeaderToggle(GUIContent title, SerializedProperty group, SerializedProperty activeField, Action contextAction = null, Func hasMoreOptions = null, Action toggleMoreOptions = null, string documentationURL = null, Action customMenuContextAction = null, bool isBoxed = false, bool isTitleHeader = false, bool shouldUpdate = true) { GetHeaderToggleRects(isBoxed, true, out Rect labelRect, out Rect foldoutRect, out Rect toggleRect, out Rect backgroundRect); DrawBackground(backgroundRect, isTitleHeader); // Title using (new EditorGUI.DisabledScope(!activeField.boolValue)) EditorGUI.LabelField(labelRect, title, EditorStyles.boldLabel); if (shouldUpdate) { // Foldout group.serializedObject.Update(); group.isExpanded = GUI.Toggle(foldoutRect, group.isExpanded, GUIContent.none, EditorStyles.foldout); group.serializedObject.ApplyModifiedProperties(); // Active checkbox activeField.serializedObject.Update(); activeField.boolValue = GUI.Toggle(toggleRect, activeField.boolValue, GUIContent.none, CoreEditorStyles.smallTickbox); activeField.serializedObject.ApplyModifiedProperties(); } else { group.isExpanded = GUI.Toggle(foldoutRect, group.isExpanded, GUIContent.none, EditorStyles.foldout); activeField.boolValue = GUI.Toggle(toggleRect, activeField.boolValue, GUIContent.none, CoreEditorStyles.smallTickbox); } contextAction = ContextMenu(title, contextAction, hasMoreOptions, toggleMoreOptions, documentationURL, labelRect); group.isExpanded = HandleEvents(contextAction, backgroundRect, group.isExpanded); return group.isExpanded; } private static void DrawBackground(Rect backgroundRect) { // Background float backgroundTint = EditorGUIUtility.isProSkin ? 0.1f : 1f; EditorGUI.DrawRect(backgroundRect, new Color(backgroundTint, backgroundTint, backgroundTint, 0.2f)); } /// Draw a header toggle like in Volumes /// The title of the header /// If the foldout is expanded /// The property to bind the toggle /// The context action /// Delegate saying if we have MoreOptions /// Callback called when the MoreOptions is toggled /// Documentation URL /// return the state of the foldout header public static bool DrawHeaderToggleFoldout(GUIContent title, bool foldoutExpanded, SerializedProperty toggleProperty, Action contextAction, Func hasMoreOptions, Action toggleMoreOptions, string documentationURL) { bool hasToggle = toggleProperty != null; GetHeaderToggleRects(false, hasToggle, out Rect labelRect, out Rect foldoutRect, out Rect toggleRect, out Rect backgroundRect); DrawBackground(backgroundRect); // Title using (new EditorGUI.DisabledScope(hasToggle && !toggleProperty.boolValue)) EditorGUI.LabelField(labelRect, title, EditorStyles.boldLabel); // Foldout bool expanded = GUI.Toggle(foldoutRect, foldoutExpanded, GUIContent.none, EditorStyles.foldout); // Active checkbox if (hasToggle) { toggleProperty.serializedObject.Update(); toggleProperty.boolValue = GUI.Toggle(toggleRect, toggleProperty.boolValue, GUIContent.none, CoreEditorStyles.smallTickbox); toggleProperty.serializedObject.ApplyModifiedProperties(); } contextAction = ContextMenu(title, contextAction, hasMoreOptions, toggleMoreOptions, documentationURL, labelRect); expanded = HandleEvents(contextAction, backgroundRect, expanded); return expanded; } private static bool HandleEvents(Action contextAction, Rect backgroundRect, bool expanded) { // Handle events var e = Event.current; if (e.type == EventType.MouseDown) { if (backgroundRect.Contains(e.mousePosition)) { // Left click: Expand/Collapse if (e.button == 0) expanded = !expanded; // Right click: Context menu else if (contextAction != null) contextAction(e.mousePosition); e.Use(); } } return expanded; } private static Action ContextMenu(GUIContent title, Action contextAction, Func hasMoreOptions, Action toggleMoreOptions, string documentationURL, Rect labelRect) { const float menuRectSize = 16f; // Context menu var contextMenuRect = new Rect(labelRect.xMax + 3f + 16 + 5, labelRect.y + 1f, menuRectSize, menuRectSize); contextAction = CreateMenuContextAction(contextAction, hasMoreOptions, toggleMoreOptions, null); CreateContextMenu(contextMenuRect, contextAction); // Documentation button ShowHelpButton(contextMenuRect, documentationURL, title); return contextAction; } /// Draw a header section like in Global Settings /// The title of the header /// Documentation URL /// The context action /// Delegate saying if we have MoreOptions /// Callback called when the MoreOptions is toggled public static void DrawSectionHeader(GUIContent title, string documentationURL = null, Action contextAction = null, Func hasMoreOptions = null, Action toggleMoreOptions = null) { const float height = 17f; const float menuRectSize = 16f; var backgroundRect = EditorGUI.IndentedRect(GUILayoutUtility.GetRect(1f, height)); var contextMenuRect = new Rect(backgroundRect.xMax - (menuRectSize + 5), backgroundRect.y + menuRectSize + 8f, menuRectSize, menuRectSize); using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField(title, CoreEditorStyles.sectionHeaderStyle); CreateContextMenu(contextMenuRect, contextAction); ShowHelpButton(contextMenuRect, documentationURL, title); } HandleEvent(false, contextMenuRect, contextAction); } static void DrawBackground(Rect backgroundRect, bool isTitleHeader) { // Background float backgroundTint = isTitleHeader ? (EditorGUIUtility.isProSkin ? 0.24f : 0.78f) : (EditorGUIUtility.isProSkin ? 0.1f : 1f); EditorGUI.DrawRect(backgroundRect, new Color(backgroundTint, backgroundTint, backgroundTint, isTitleHeader ? 1f : 0.2f)); } static Rect ToFullWidth(Rect rect) { rect.xMin = 0f; rect.width += 4f; return rect; } static Action CreateMenuContextAction(Action contextAction, Func hasMoreOptions, Action toggleMoreOptions, Action customMenuContextAction) { if (contextAction == null && (hasMoreOptions != null || customMenuContextAction != null)) { // If no contextual menu add one for the advanced properties. contextAction = pos => { var menu = new GenericMenu(); if (customMenuContextAction != null) customMenuContextAction(menu); if (hasMoreOptions != null) menu.AddAdvancedPropertiesBoolMenuItem(hasMoreOptions, toggleMoreOptions); menu.DropDown(new Rect(pos, Vector2.zero)); }; } return contextAction; } static void CreateContextMenu(Rect contextMenuRect, Action contextAction) { // Context menu if (contextAction != null) { if (GUI.Button(contextMenuRect, CoreEditorStyles.contextMenuIcon, CoreEditorStyles.contextMenuStyle)) contextAction(new Vector2(contextMenuRect.x, contextMenuRect.yMax)); } } static bool HandleEvent(bool state, Rect activationRect, Action contextAction) { var e = Event.current; if (e.type == EventType.MouseDown && activationRect.Contains(e.mousePosition)) { // Left click: Expand/Collapse if (e.button == 0) state = !state; // Right click: Context menu else if (contextAction != null) contextAction(e.mousePosition); e.Use(); } return state; } static void ShowHelpButton(Rect contextMenuRect, string documentationURL, GUIContent title) { if (string.IsNullOrEmpty(documentationURL)) return; var documentationRect = contextMenuRect; documentationRect.x -= 16 + 2; var documentationIcon = new GUIContent(CoreEditorStyles.iconHelp, $"Open Reference for {title.text}."); if (GUI.Button(documentationRect, documentationIcon, CoreEditorStyles.iconHelpStyle)) Help.BrowseURL(documentationURL); } /// /// Draw a Color Field but convert the color to gamma space before displaying it in the shader. /// Using SetColor on a material does the conversion, but setting the color as vector3 in a constant buffer doesn't /// So we have to do it manually, doing it in the UI avoids having to do a migration step for existing fields /// /// The color property /// The label static public void ColorFieldLinear(SerializedProperty property, GUIContent label) { var rect = EditorGUILayout.GetControlRect(); EditorGUI.BeginProperty(rect, label, property); EditorGUI.BeginChangeCheck(); var color = EditorGUI.ColorField(rect, label, property.colorValue.gamma, true, false, false); if (EditorGUI.EndChangeCheck()) property.colorValue = color.linear; EditorGUI.EndProperty(); } static readonly GUIContent[][] k_DrawVector6_Label = { new[] { new GUIContent(" X"), new GUIContent(" Y"), new GUIContent(" Z"), }, new[] { new GUIContent("-X"), new GUIContent("-Y"), new GUIContent("-Z"), }, }; const int k_DrawVector6Slider_LabelSize = 60; const int k_DrawVector6Slider_FieldSize = 80; /// /// Draw a Vector6 field /// /// The label /// The data for +X, +Y and +Z /// The data for -X, -Y and -Z /// Min clamping value along axis /// Max clamping value along axis /// [Optional] Color marks to use /// [Optional] multiplicator on the datas /// [Optional] Allow the face positive values to be smaller than negative ones and vice versa public static void DrawVector6(GUIContent label, SerializedProperty positive, SerializedProperty negative, Vector3 min, Vector3 max, Color[] colors = null, SerializedProperty multiplicator = null, bool allowIntersection = true) { if (colors != null && (colors.Length != 6)) throw new System.ArgumentException("Colors must be a 6 element array. [+X, +Y, +X, -X, -Y, -Z]"); GUILayout.BeginVertical(); const int interline = 2; const int fixAlignSubVector3Labels = -1; Rect wholeArea = EditorGUILayout.GetControlRect(true, 2 * EditorGUIUtility.singleLineHeight + interline); Rect firstLineRect = wholeArea; firstLineRect.height = EditorGUIUtility.singleLineHeight; Rect secondLineRect = firstLineRect; secondLineRect.y += firstLineRect.height + interline; Rect labelRect = firstLineRect; labelRect.width = EditorGUIUtility.labelWidth; Rect firstVectorValueRect = firstLineRect; firstVectorValueRect.xMin += labelRect.width + fixAlignSubVector3Labels; EditorGUI.BeginProperty(wholeArea, label, positive); EditorGUI.BeginProperty(wholeArea, label, negative); { EditorGUI.LabelField(labelRect, label); } EditorGUI.EndProperty(); EditorGUI.EndProperty(); if (!allowIntersection) { max = negative.vector3Value; max.x = 1 - max.x; max.y = 1 - max.y; max.z = 1 - max.z; } DrawVector3(firstVectorValueRect, k_DrawVector6_Label[0], positive, min, max, false, colors == null ? null : new Color[] { colors[0], colors[1], colors[2] }, multiplicator); Rect secondVectorValueRect = secondLineRect; secondVectorValueRect.xMin = firstVectorValueRect.xMin; secondVectorValueRect.xMax = firstVectorValueRect.xMax; if (!allowIntersection) { max = positive.vector3Value; max.x = 1 - max.x; max.y = 1 - max.y; max.z = 1 - max.z; } DrawVector3(secondVectorValueRect, k_DrawVector6_Label[1], negative, min, max, true, colors == null ? null : new Color[] { colors[3], colors[4], colors[5] }, multiplicator); GUILayout.EndVertical(); } static void DrawVector3(Rect rect, GUIContent[] labels, SerializedProperty value, Vector3 min, Vector3 max, bool minusPrefix, Color[] colors, SerializedProperty multiplicator = null) { float[] multifloat = multiplicator == null ? new float[] { value.vector3Value.x, value.vector3Value.y, value.vector3Value.z } : new float[] { value.vector3Value.x * multiplicator.vector3Value.x, value.vector3Value.y * multiplicator.vector3Value.y, value.vector3Value.z * multiplicator.vector3Value.z }; float fieldWidth = rect.width / 3f; const int subLabelWidth = 13; const int colorWidth = 2; const int colorStartDecal = 1; const int valuesSeparator = 2; SerializedProperty[] values = new[] { value.FindPropertyRelative("x"), value.FindPropertyRelative("y"), value.FindPropertyRelative("z"), }; int oldIndentLevel = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; float oldLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = subLabelWidth; for (int i = 0; i < 3; ++i) { Rect localRect = rect; localRect.xMin += i * fieldWidth;// + (i > 0 ? valuesSeparator : 0); localRect.xMax -= (2 - i) * fieldWidth + (i < 2 ? valuesSeparator : 0); Rect colorRect = localRect; colorRect.x = localRect.x + subLabelWidth + colorStartDecal; colorRect.width = colorWidth; colorRect.yMin += 2; colorRect.yMax -= 2; if (minusPrefix) { localRect.xMin -= 3; EditorGUIUtility.labelWidth = subLabelWidth + 3; } else EditorGUIUtility.labelWidth = subLabelWidth; if (multiplicator == null) { EditorGUI.BeginProperty(localRect, labels[i], values[i]); EditorGUI.BeginChangeCheck(); EditorGUI.PropertyField(localRect, values[i], labels[i]); if (EditorGUI.EndChangeCheck()) { values[i].floatValue = Mathf.Clamp(values[i].floatValue, min[i], max[i]); } EditorGUI.EndProperty(); } else { EditorGUI.BeginProperty(localRect, labels[i], values[i]); EditorGUI.BeginChangeCheck(); float localMultiplicator = multiplicator.vector3Value[i]; float multipliedValue = values[i].floatValue * localMultiplicator; multipliedValue = EditorGUI.FloatField(localRect, labels[i], multipliedValue); if (EditorGUI.EndChangeCheck()) { values[i].floatValue = Mathf.Clamp((localMultiplicator < -0.00001 || 0.00001 < localMultiplicator) ? multipliedValue / localMultiplicator : 0f, min[i], max[i]); } EditorGUI.EndProperty(); } EditorGUI.DrawRect(colorRect, colors[i]); } EditorGUIUtility.labelWidth = oldLabelWidth; EditorGUI.indentLevel = oldIndentLevel; } static void DrawVector3_(Rect rect, GUIContent[] labels, SerializedProperty value, Vector3 min, Vector3 max, bool addMinusPrefix, Color[] colors, SerializedProperty multiplicator = null) { float[] multifloat = multiplicator == null ? new float[] { value.vector3Value.x, value.vector3Value.y, value.vector3Value.z } : new float[] { value.vector3Value.x * multiplicator.vector3Value.x, value.vector3Value.y * multiplicator.vector3Value.y, value.vector3Value.z * multiplicator.vector3Value.z }; float fieldWidth = rect.width / 3f; EditorGUI.showMixedValue = value.hasMultipleDifferentValues; EditorGUI.BeginChangeCheck(); EditorGUI.MultiFloatField(rect, labels, multifloat); if (EditorGUI.EndChangeCheck()) { value.vector3Value = multiplicator == null ? new Vector3( Mathf.Clamp(multifloat[0], min.x, max.x), Mathf.Clamp(multifloat[1], min.y, max.y), Mathf.Clamp(multifloat[2], min.z, max.z) ) : new Vector3( Mathf.Clamp((multiplicator.vector3Value.x < -0.00001 || 0.00001 < multiplicator.vector3Value.x) ? multifloat[0] / multiplicator.vector3Value.x : 0f, min.x, max.x), Mathf.Clamp((multiplicator.vector3Value.y < -0.00001 || 0.00001 < multiplicator.vector3Value.y) ? multifloat[1] / multiplicator.vector3Value.y : 0f, min.y, max.y), Mathf.Clamp((multiplicator.vector3Value.z < -0.00001 || 0.00001 < multiplicator.vector3Value.z) ? multifloat[2] / multiplicator.vector3Value.z : 0f, min.z, max.z) ); } EditorGUI.showMixedValue = false; //Suffix is a hack as sublabel only work with 1 character if (addMinusPrefix) { Rect suffixRect = new Rect(rect.x - 4 - k_IndentMargin * EditorGUI.indentLevel, rect.y, 100, rect.height); for (int i = 0; i < 3; ++i) { EditorGUI.LabelField(suffixRect, "-"); suffixRect.x += fieldWidth + .33f; } } //Color is a hack as nothing is done to handle this at the moment if (colors != null) { if (colors.Length != 3) throw new System.ArgumentException("colors must have 3 elements."); Rect suffixRect = new Rect(rect.x + 7 - k_IndentMargin * EditorGUI.indentLevel, rect.y, 100, rect.height); GUIStyle colorMark = new GUIStyle(EditorStyles.label); colorMark.normal.textColor = colors[0]; EditorGUI.LabelField(suffixRect, "|", colorMark); suffixRect.x += 1; EditorGUI.LabelField(suffixRect, "|", colorMark); suffixRect.x += fieldWidth + 0.33f; colorMark.normal.textColor = colors[1]; EditorGUI.LabelField(suffixRect, "|", colorMark); suffixRect.x += 1; EditorGUI.LabelField(suffixRect, "|", colorMark); suffixRect.x += fieldWidth + .33f; colorMark.normal.textColor = colors[2]; EditorGUI.LabelField(suffixRect, "|", colorMark); suffixRect.x += 1; EditorGUI.LabelField(suffixRect, "|", colorMark); } } /// Draw a popup /// the label /// The data displayed /// Options of the dropdown public static void DrawPopup(GUIContent label, SerializedProperty property, string[] options) { var mode = property.intValue; if (mode >= options.Length) Debug.LogError($"Invalid option while trying to set {label.text}"); EditorGUI.BeginChangeCheck(); using (new EditorGUI.MixedValueScope(property.hasMultipleDifferentValues)) { mode = EditorGUILayout.Popup(label, mode, options); } if (EditorGUI.EndChangeCheck()) property.intValue = mode; } /// /// Draw an EnumPopup handling multiEdition /// /// The data displayed /// Type of the property /// The label public static void DrawEnumPopup(SerializedProperty property, System.Type type, GUIContent label = null) { EditorGUI.showMixedValue = property.hasMultipleDifferentValues; EditorGUI.BeginChangeCheck(); var name = System.Enum.GetName(type, property.intValue); var index = System.Array.FindIndex(System.Enum.GetNames(type), n => n == name); var input = (System.Enum)System.Enum.GetValues(type).GetValue(index); var rawResult = EditorGUILayout.EnumPopup(label ?? EditorGUIUtility.TrTextContent(ObjectNames.NicifyVariableName(property.name)), input); var result = ((System.IConvertible)rawResult).ToInt32(System.Globalization.CultureInfo.CurrentCulture); if (EditorGUI.EndChangeCheck()) property.intValue = result; EditorGUI.showMixedValue = false; } /// Remove the keywords on the given materials /// The material to edit public static void RemoveMaterialKeywords(Material material) => material.shaderKeywords = null; /// Get the AdditionalData of the given component /// The type of the AdditionalData component /// The object to seek for AdditionalData /// [Optional] The default value to use if there is no AdditionalData /// return an AdditionalData component public static T[] GetAdditionalData(UnityEngine.Object[] targets, Action initDefault = null) where T : Component { // Handles multi-selection var data = targets.Cast() .Select(t => t.GetComponent()) .ToArray(); for (int i = 0; i < data.Length; i++) { if (data[i] == null) { data[i] = Undo.AddComponent(((Component)targets[i]).gameObject); if (initDefault != null) { initDefault(data[i]); } } } return data; } /// Add the appropriate AdditionalData to the given GameObject and its children containing the original component /// The type of the original component /// The type of the AdditionalData component /// The root object to update /// [Optional] The default value to use if there is no AdditionalData public static void AddAdditionalData(GameObject go, Action initDefault = null) where T : Component where AdditionalT : Component { var components = go.GetComponentsInChildren(typeof(T), true); foreach (var c in components) { if (!c.TryGetComponent(out _)) { var hd = c.gameObject.AddComponent(); if (initDefault != null) initDefault(hd); } } } /// Create a game object /// The parent /// The wanted name (can be updated with a number if a sibling with same name exist /// Required component on this object in addition to Transform /// The created object public static GameObject CreateGameObject(GameObject parent, string name, params Type[] types) => ObjectFactory.CreateGameObject(GameObjectUtility.GetUniqueNameForSibling(parent != null ? parent.transform : null, name), types); /// /// Creates a new GameObject and set it's position to the current view /// /// the name of the new gameobject /// the parent of the gameobject /// the created GameObject public static GameObject CreateGameObject(string name, UnityEngine.Object context) { var parent = context as GameObject; var go = CoreEditorUtils.CreateGameObject(parent, name); GameObjectUtility.SetParentAndAlign(go, context as GameObject); Undo.RegisterCreatedObjectUndo(go, "Create " + go.name); Selection.activeObject = go; if (parent != null) go.transform.localPosition = Vector3.zero; else { if (EditorPrefs.GetBool("Create3DObject.PlaceAtWorldOrigin", false)) go.transform.localPosition = Vector3.zero; else EditorApplication.ExecuteMenuItem("GameObject/Move To View"); } return go; } /// Parse and return current project version /// The version static public string GetCurrentProjectVersion() { string[] readText = File.ReadAllLines("ProjectSettings/ProjectVersion.txt"); // format is m_EditorVersion: 2018.2.0b7 string[] versionText = readText[0].Split(' '); return versionText[1]; } /// Checks out a file from the Version Control System if VCS is enabled. /// A boolean value determining whether Version Control System is enabled or not. /// The UnityObject to be checked out from the Version Control System. static public void CheckOutFile(bool VCSEnabled, UnityObject mat) { if (VCSEnabled) { UnityEditor.VersionControl.Task task = UnityEditor.VersionControl.Provider.Checkout(mat, UnityEditor.VersionControl.CheckoutMode.Both); if (!task.success) { Debug.Log(task.text + " " + task.resultCode); } } } #region IconAndSkin internal enum Skin { Auto, Personnal, Professional, } static Func GetInternalSkinIndex; static Func GetGUIStatePixelsPerPoint; static Func GetTexturePixelPerPoint; static Action SetTexturePixelPerPoint; static void LoadSkinAndIconMethods() { var internalSkinIndexInfo = typeof(EditorGUIUtility).GetProperty("skinIndex", BindingFlags.NonPublic | BindingFlags.Static); var internalSkinIndexLambda = Expression.Lambda>(Expression.Property(null, internalSkinIndexInfo)); GetInternalSkinIndex = internalSkinIndexLambda.Compile(); var guiStatePixelsPerPointInfo = typeof(GUIUtility).GetProperty("pixelsPerPoint", BindingFlags.NonPublic | BindingFlags.Static); var guiStatePixelsPerPointLambda = Expression.Lambda>(Expression.Property(null, guiStatePixelsPerPointInfo)); GetGUIStatePixelsPerPoint = guiStatePixelsPerPointLambda.Compile(); var pixelPerPointParam = Expression.Parameter(typeof(float), "pixelPerPoint"); var texture2DProperty = Expression.Parameter(typeof(Texture2D), "texture2D"); var texture2DPixelsPerPointInfo = typeof(Texture2D).GetProperty("pixelsPerPoint", BindingFlags.NonPublic | BindingFlags.Instance); var texture2DPixelsPerPointProperty = Expression.Property(texture2DProperty, texture2DPixelsPerPointInfo); var texture2DGetPixelsPerPointLambda = Expression.Lambda>(texture2DPixelsPerPointProperty, texture2DProperty); GetTexturePixelPerPoint = texture2DGetPixelsPerPointLambda.Compile(); var texture2DSetPixelsPerPointLambda = Expression.Lambda>(Expression.Assign(texture2DPixelsPerPointProperty, pixelPerPointParam), texture2DProperty, pixelPerPointParam); SetTexturePixelPerPoint = texture2DSetPixelsPerPointLambda.Compile(); } /// Get the skin currently in use static Skin currentSkin => GetInternalSkinIndex() == 0 ? Skin.Personnal : Skin.Professional; // /!\ UIElement do not support well pixel per point at the moment. For this, use the hack forceLowRes /// /// Load an icon regarding skin and editor resolution. /// Icon should be stored as legacy icon resources: /// - "d_" prefix for Professional theme /// - "@2x" suffix for high resolution /// /// Path to seek the icon from Assets/ folder /// Icon name without suffix, prefix or extention /// [Optional] Extention of file (png per default) /// The loaded texture public static Texture2D LoadIcon(string path, string name, string extention = ".png") => LoadIcon(path, name, extention, false); //forceLowRes should be deprecated as soon as this is fixed in UIElement internal static Texture2D LoadIcon(string path, string name, string extention = ".png", bool forceLowRes = false) { if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(name)) return null; string prefix = ""; var skin = currentSkin; if (skin == Skin.Professional) prefix = "d_"; Texture2D icon = null; float pixelsPerPoint = GetGUIStatePixelsPerPoint(); if (pixelsPerPoint > 1.0f && !forceLowRes) { icon = EditorGUIUtility.Load($"{path}/{prefix}{name}@2x{extention}") as Texture2D; if (icon == null && !string.IsNullOrEmpty(prefix)) icon = EditorGUIUtility.Load($"{path}/{name}@2x{extention}") as Texture2D; if (icon != null) SetTexturePixelPerPoint(icon, 2.0f); } if (icon == null) icon = EditorGUIUtility.Load($"{path}/{prefix}{name}{extention}") as Texture2D; if (icon == null && !string.IsNullOrEmpty(prefix)) icon = EditorGUIUtility.Load($"{path}/{name}{extention}") as Texture2D; TryToFixFilterMode(pixelsPerPoint, icon); return icon; } internal static Texture2D FindTexture(string name) { float pixelsPerPoint = GetGUIStatePixelsPerPoint(); Texture2D icon = pixelsPerPoint > 1.0f ? EditorGUIUtility.FindTexture($"{name}@2x") : EditorGUIUtility.FindTexture(name); TryToFixFilterMode(pixelsPerPoint, icon); return icon; } internal static void TryToFixFilterMode(float pixelsPerPoint, Texture2D icon) { if (icon != null && !Mathf.Approximately(GetTexturePixelPerPoint(icon), pixelsPerPoint) && //scaling are different !Mathf.Approximately(pixelsPerPoint % 1, 0)) //screen scaling is non-integer { icon.filterMode = FilterMode.Bilinear; } } #endregion internal static T CreateAssetAt(Scene scene, string targetName) where T : ScriptableObject { string path; if (string.IsNullOrEmpty(scene.path)) { path = "Assets/"; } else { var scenePath = Path.GetDirectoryName(scene.path); var extPath = scene.name; path = scenePath + Path.DirectorySeparatorChar + extPath + Path.DirectorySeparatorChar; CoreUtils.EnsureFolderTreeInAssetFilePath(path); } path += targetName.ReplaceInvalidFileNameCharacters() + ".asset"; path = AssetDatabase.GenerateUniqueAssetPath(path); var profile = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(profile, path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return profile; } internal static bool IsAssetInReadOnlyPackage(string path) { Assert.IsNotNull(path); var info = PackageManager.PackageInfo.FindForAssetPath(path); return info != null && (info.source != PackageSource.Local && info.source != PackageSource.Embedded); } } }