#if UNITY_2018_3_OR_NEWER #define SETTINGS_PROVIDER_ENABLED #endif using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using UnityEngine; #if SETTINGS_PROVIDER_ENABLED #if UNITY_2019_1_OR_NEWER using UnityEngine.UIElements; #else using UnityEngine.Experimental.UIElements; #endif #endif namespace UnityEditor.SettingsManagement { /// /// A implementation that creates an interface from settings reflected /// from a collection of assemblies. /// #if SETTINGS_PROVIDER_ENABLED public sealed class UserSettingsProvider : SettingsProvider #else public sealed class UserSettingsProvider #endif { /// /// Category string constant to check whether Unity is running in developer (internal) mode. /// public const string developerModeCategory = "Developer Mode"; const string k_SettingsName = "UserSettingsProviderSettings"; #if SETTINGS_PROVIDER_ENABLED const int k_LabelWidth = 240; static int labelWidth { get { if (s_DefaultLabelWidth != null) return (int)((float)s_DefaultLabelWidth.GetValue(null, null)); return k_LabelWidth; } } static int defaultLayoutMaxWidth { get { if (s_DefaultLayoutMaxWidth != null) return (int)((float)s_DefaultLayoutMaxWidth.GetValue(null, null)); return 0; } } #else const int k_LabelWidth = 180; int labelWidth { get { return k_LabelWidth; } } int defaultLayoutMaxWidth { get { return 0; } } #endif List m_Categories; Dictionary> m_Settings; Dictionary> m_SettingBlocks; #if !SETTINGS_PROVIDER_ENABLED HashSet keywords = new HashSet(); #endif static readonly string[] s_SearchContext = new string[1]; EventType m_SettingsBlockKeywordsInitialized; Assembly[] m_Assemblies; static Settings s_Settings; Settings m_SettingsInstance; #if SETTINGS_PROVIDER_ENABLED static PropertyInfo s_DefaultLabelWidth; static PropertyInfo s_DefaultLayoutMaxWidth; #endif static Settings userSettingsProviderSettings { get { if (s_Settings == null) s_Settings = new Settings(new[] { new UserSettingsRepository() }); return s_Settings; } } internal static UserSetting showHiddenSettings = new UserSetting(userSettingsProviderSettings, "settings.showHidden", false, SettingsScope.User); internal static UserSetting showUnregisteredSettings = new UserSetting(userSettingsProviderSettings, "settings.showUnregistered", false, SettingsScope.User); internal static UserSetting listByKey = new UserSetting(userSettingsProviderSettings, "settings.listByKey", false, SettingsScope.User); internal static UserSetting showUserSettings = new UserSetting(userSettingsProviderSettings, "settings.showUserSettings", true, SettingsScope.User); internal static UserSetting showProjectSettings = new UserSetting(userSettingsProviderSettings, "settings.showProjectSettings", true, SettingsScope.User); #if SETTINGS_PROVIDER_ENABLED /// /// Initializes and returns a new `UserSettingsProvider` instance. /// /// The settings menu path. /// The Settings instance that this provider is inspecting. /// A collection of assemblies to scan for and attributes. /// Which scopes this provider is valid for. /// Thrown if settings or assemblies is null. public UserSettingsProvider(string path, Settings settings, Assembly[] assemblies, SettingsScope scopes = SettingsScope.User) : base(path, scopes) #else /// /// Initializes and returns a new `UserSettingsProvider` instance. /// /// The instance that this provider is inspecting. /// A collection of assemblies to scan for and attributes. public UserSettingsProvider(Settings settings, Assembly[] assemblies) #endif { if (settings == null) throw new ArgumentNullException("settings"); if (assemblies == null) throw new ArgumentNullException("assemblies"); m_SettingsInstance = settings; m_Assemblies = assemblies; #if !SETTINGS_PROVIDER_ENABLED SearchForUserSettingAttributes(); #endif } #if SETTINGS_PROVIDER_ENABLED /// /// Invoked by the when activated in the Editor. /// /// /// Search context in the search box on the /// [Settings](https://docs.unity3d.com/Manual/comp-ManagerGroup.html) window. /// /// /// Root of the UIElements tree. If you add to this root, the SettingsProvider uses /// [UIElements](https://docs.unity3d.com/ScriptReference/UnityEngine.UIElementsModule.html) /// instead of calling to build the UI. /// See for details. /// public override void OnActivate(string searchContext, VisualElement rootElement) { base.OnActivate(searchContext, rootElement); SearchForUserSettingAttributes(); var window = GetType().GetProperty("settingsWindow", BindingFlags.Instance | BindingFlags.NonPublic); if (window != null) { s_DefaultLabelWidth = window.PropertyType.GetProperty("s_DefaultLabelWidth", BindingFlags.Public | BindingFlags.Static); s_DefaultLayoutMaxWidth = window.PropertyType.GetProperty("s_DefaultLayoutMaxWidth", BindingFlags.Public | BindingFlags.Static); } } #endif struct PrefEntry { GUIContent m_Content; IUserSetting m_Pref; public GUIContent content { get { return m_Content; } } public IUserSetting pref { get { return m_Pref; } } public PrefEntry(GUIContent content, IUserSetting pref) { m_Content = content; m_Pref = pref; } } void SearchForUserSettingAttributes() { var isDeveloperMode = EditorPrefs.GetBool("DeveloperMode", false); var keywordsHash = new HashSet(); if (m_Settings != null) m_Settings.Clear(); else m_Settings = new Dictionary>(); if (m_SettingBlocks != null) m_SettingBlocks.Clear(); else m_SettingBlocks = new Dictionary>(); var types = m_Assemblies.SelectMany(x => x.GetTypes()); // collect instance fields/methods too, but only so we can throw a warning that they're invalid. var fields = types.SelectMany(x => x.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static) .Where(prop => Attribute.IsDefined(prop, typeof(UserSettingAttribute)))); var methods = types.SelectMany(x => x.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) .Where(y => Attribute.IsDefined(y, typeof(UserSettingBlockAttribute)))); foreach (var field in fields) { if (!field.IsStatic) { Debug.LogWarning("Cannot create setting entries for instance fields. Skipping \"" + field.Name + "\"."); continue; } var attrib = (UserSettingAttribute)Attribute.GetCustomAttribute(field, typeof(UserSettingAttribute)); if (!attrib.visibleInSettingsProvider) continue; var pref = (IUserSetting)field.GetValue(null); if (pref == null) { Debug.LogWarning("[UserSettingAttribute] is only valid for types implementing the IUserSetting interface. Skipping \"" + field.Name + "\""); continue; } var category = string.IsNullOrEmpty(attrib.category) ? "Uncategorized" : attrib.category; var content = listByKey ? new GUIContent(pref.key) : attrib.title; if (developerModeCategory.Equals(category) && !isDeveloperMode) continue; List settings; if (m_Settings.TryGetValue(category, out settings)) settings.Add(new PrefEntry(content, pref)); else m_Settings.Add(category, new List() { new PrefEntry(content, pref) }); } foreach (var method in methods) { var attrib = (UserSettingBlockAttribute)Attribute.GetCustomAttribute(method, typeof(UserSettingBlockAttribute)); var category = string.IsNullOrEmpty(attrib.category) ? "Uncategorized" : attrib.category; if (developerModeCategory.Equals(category) && !isDeveloperMode) continue; List blocks; var parameters = method.GetParameters(); if (!method.IsStatic || parameters.Length < 1 || parameters[0].ParameterType != typeof(string)) { Debug.LogWarning("[UserSettingBlockAttribute] is only valid for static functions with a single string parameter. Ex, `static void MySettings(string searchContext)`. Skipping \"" + method.Name + "\""); continue; } if (m_SettingBlocks.TryGetValue(category, out blocks)) blocks.Add(method); else m_SettingBlocks.Add(category, new List() { method }); } if (showHiddenSettings) { var unlisted = new List(); m_Settings.Add("Unlisted", unlisted); foreach (var pref in UserSettings.FindUserSettings(m_Assemblies, SettingVisibility.Unlisted | SettingVisibility.Hidden)) unlisted.Add(new PrefEntry(new GUIContent(pref.key), pref)); } if (showUnregisteredSettings) { var unregistered = new List(); m_Settings.Add("Unregistered", unregistered); foreach (var pref in UserSettings.FindUserSettings(m_Assemblies, SettingVisibility.Unregistered)) unregistered.Add(new PrefEntry(new GUIContent(pref.key), pref)); } foreach (var cat in m_Settings) { foreach (var entry in cat.Value) { var content = entry.content; if (content != null && !string.IsNullOrEmpty(content.text)) { foreach (var word in content.text.Split(' ')) keywordsHash.Add(word); } } } keywords = keywordsHash; m_Categories = m_Settings.Keys.Union(m_SettingBlocks.Keys).ToList(); m_Categories.Sort(); } #if SETTINGS_PROVIDER_ENABLED /// /// Invoked by the SettingsProvider container when drawing the UI header. /// public override void OnTitleBarGUI() { if (GUILayout.Button(GUIContent.none, SettingsGUIStyles.settingsGizmo)) DoContextMenu(); } #endif void InitSettingsBlockKeywords() { // Have to let the blocks run twice - one for Layout, one for Repaint. if (m_SettingsBlockKeywordsInitialized == EventType.Repaint) return; m_SettingsBlockKeywordsInitialized = Event.current.type; // Allows SettingsGUILayout.SettingsField to populate keywords SettingsGUILayout.s_Keywords = new HashSet(keywords); // Set a dummy value so that GUI blocks with conditional foldouts will behave as though searching. s_SearchContext[0] = "Search"; foreach (var category in m_SettingBlocks) { foreach (var block in category.Value) block.Invoke(null, s_SearchContext); } keywords = SettingsGUILayout.s_Keywords; SettingsGUILayout.s_Keywords = null; s_SearchContext[0] = ""; } void DoContextMenu() { var menu = new GenericMenu(); menu.AddItem(new GUIContent("Reset All"), false, () => { if (!UnityEditor.EditorUtility.DisplayDialog("Reset All Settings", "Reset all settings? This is not undo-able.", "Reset", "Cancel")) return; // Do not reset SettingVisibility.Unregistered foreach (var pref in UserSettings.FindUserSettings(m_Assemblies, SettingVisibility.Visible | SettingVisibility.Hidden | SettingVisibility.Unlisted)) pref.Reset(); m_SettingsInstance.Save(); }); if (EditorPrefs.GetBool("DeveloperMode", false)) { menu.AddSeparator(""); menu.AddItem(new GUIContent("Developer/List Settings By Key"), listByKey, () => { listByKey.SetValue(!listByKey, true); SearchForUserSettingAttributes(); }); menu.AddSeparator("Developer/"); menu.AddItem(new GUIContent("Developer/Show User Settings"), showUserSettings, () => { showUserSettings.SetValue(!showUserSettings, true); SearchForUserSettingAttributes(); }); menu.AddItem(new GUIContent("Developer/Show Project Settings"), showProjectSettings, () => { showProjectSettings.SetValue(!showProjectSettings, true); SearchForUserSettingAttributes(); }); menu.AddSeparator("Developer/"); menu.AddItem(new GUIContent("Developer/Show Unlisted Settings"), showHiddenSettings, () => { showHiddenSettings.SetValue(!showHiddenSettings, true); SearchForUserSettingAttributes(); }); menu.AddItem(new GUIContent("Developer/Show Unregistered Settings"), showUnregisteredSettings, () => { showUnregisteredSettings.SetValue(!showUnregisteredSettings, true); SearchForUserSettingAttributes(); }); menu.AddSeparator("Developer/"); menu.AddItem(new GUIContent("Developer/Open Project Settings File"), false, () => { var project = m_SettingsInstance.GetRepository(SettingsScope.Project); if (project != null) { var path = Path.GetFullPath(project.path); System.Diagnostics.Process.Start(path); } }); menu.AddItem(new GUIContent("Developer/Print All Settings"), false, () => { Debug.Log(UserSettings.GetSettingsString(m_Assemblies)); }); #if UNITY_2019_1_OR_NEWER menu.AddSeparator("Developer/"); #if UNITY_2019_3_OR_NEWER menu.AddItem(new GUIContent("Developer/Recompile Scripts"), false, EditorUtility.RequestScriptReload); #else menu.AddItem(new GUIContent("Developer/Recompile Scripts"), false, UnityEditorInternal.InternalEditorUtility.RequestScriptReload); #endif #endif } menu.ShowAsContext(); } #if SETTINGS_PROVIDER_ENABLED /// /// Called when the Settings window opens in the Editor. /// /// /// Search context in the search box on the /// [Settings](https://docs.unity3d.com/Manual/comp-ManagerGroup.html) window. /// /// public override void OnGUI(string searchContext) #else /// /// Called when the Settings window opens in the Editor. /// /// /// Search context in the search box on the /// [Settings](https://docs.unity3d.com/Manual/comp-ManagerGroup.html) window. /// /// public void OnGUI(string searchContext) #endif { #if !SETTINGS_PROVIDER_ENABLED var evt = Event.current; if (evt.type == EventType.ContextClick) DoContextMenu(); #endif InitSettingsBlockKeywords(); EditorGUIUtility.labelWidth = labelWidth; EditorGUI.BeginChangeCheck(); var maxWidth = defaultLayoutMaxWidth; if (maxWidth != 0) GUILayout.BeginVertical(SettingsGUIStyles.settingsArea, GUILayout.MaxWidth(maxWidth)); else GUILayout.BeginVertical(SettingsGUIStyles.settingsArea); var hasSearchContext = !string.IsNullOrEmpty(searchContext); s_SearchContext[0] = searchContext; if (hasSearchContext) { // todo - Improve search comparison var searchKeywords = searchContext.Split(' '); foreach (var settingField in m_Settings) { foreach (var setting in settingField.Value) { if (searchKeywords.Any(x => !string.IsNullOrEmpty(x) && setting.content.text.IndexOf(x, StringComparison.InvariantCultureIgnoreCase) > -1)) DoPreferenceField(setting.content, setting.pref); } } foreach (var settingsBlock in m_SettingBlocks) { foreach (var block in settingsBlock.Value) { block.Invoke(null, s_SearchContext); } } } else { foreach (var key in m_Categories) { GUILayout.Label(key, EditorStyles.boldLabel); List settings; if (m_Settings.TryGetValue(key, out settings)) foreach (var setting in settings) DoPreferenceField(setting.content, setting.pref); List blocks; if (m_SettingBlocks.TryGetValue(key, out blocks)) foreach (var block in blocks) block.Invoke(null, s_SearchContext); GUILayout.Space(8); } } EditorGUIUtility.labelWidth = 0; GUILayout.EndVertical(); if (EditorGUI.EndChangeCheck()) { m_SettingsInstance.Save(); } } void DoPreferenceField(GUIContent title, IUserSetting pref) { if (EditorPrefs.GetBool("DeveloperMode", false)) { if (pref.scope == SettingsScope.Project && !showProjectSettings) return; if (pref.scope == SettingsScope.User && !showUserSettings) return; } if (pref is UserSetting) { var cast = (UserSetting)pref; cast.value = EditorGUILayout.FloatField(title, cast.value); } else if (pref is UserSetting) { var cast = (UserSetting)pref; cast.value = EditorGUILayout.IntField(title, cast.value); } else if (pref is UserSetting) { var cast = (UserSetting)pref; cast.value = EditorGUILayout.Toggle(title, cast.value); } else if (pref is UserSetting) { var cast = (UserSetting)pref; cast.value = EditorGUILayout.TextField(title, cast.value); } else if (pref is UserSetting) { var cast = (UserSetting)pref; cast.value = EditorGUILayout.ColorField(title, cast.value); } #if UNITY_2018_3_OR_NEWER else if (pref is UserSetting) { var cast = (UserSetting)pref; cast.value = EditorGUILayout.GradientField(title, cast.value); } #endif else if (pref is UserSetting) { var cast = (UserSetting)pref; cast.value = EditorGUILayout.Vector2Field(title, cast.value); } else if (pref is UserSetting) { var cast = (UserSetting)pref; cast.value = EditorGUILayout.Vector3Field(title, cast.value); } else if (pref is UserSetting) { var cast = (UserSetting)pref; cast.value = EditorGUILayout.Vector4Field(title, cast.value); } else if (typeof(Enum).IsAssignableFrom(pref.type)) { Enum val = (Enum)pref.GetValue(); EditorGUI.BeginChangeCheck(); if (Attribute.IsDefined(pref.type, typeof(FlagsAttribute))) val = EditorGUILayout.EnumFlagsField(title, val); else val = EditorGUILayout.EnumPopup(title, val); if (EditorGUI.EndChangeCheck()) pref.SetValue(val); } else if (typeof(UnityEngine.Object).IsAssignableFrom(pref.type)) { var obj = (UnityEngine.Object)pref.GetValue(); EditorGUI.BeginChangeCheck(); obj = EditorGUILayout.ObjectField(title, obj, pref.type, false); if (EditorGUI.EndChangeCheck()) pref.SetValue(obj); } else { GUILayout.BeginHorizontal(); GUILayout.Label(title, GUILayout.Width(EditorGUIUtility.labelWidth - EditorStyles.label.margin.right * 2)); var obj = pref.GetValue(); GUILayout.Label(obj == null ? "null" : pref.GetValue().ToString()); GUILayout.EndHorizontal(); } SettingsGUILayout.DoResetContextMenuForLastRect(pref); } } }