UserSettingsProvider.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. #if UNITY_2018_3_OR_NEWER
  2. #define SETTINGS_PROVIDER_ENABLED
  3. #endif
  4. using System;
  5. using System.Collections.Generic;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Reflection;
  9. using UnityEngine;
  10. #if SETTINGS_PROVIDER_ENABLED
  11. #if UNITY_2019_1_OR_NEWER
  12. using UnityEngine.UIElements;
  13. #else
  14. using UnityEngine.Experimental.UIElements;
  15. #endif
  16. #endif
  17. namespace UnityEditor.SettingsManagement
  18. {
  19. /// <summary>
  20. /// A <see cref="UnityEditor.SettingsProvider"/> implementation that creates an interface from settings reflected
  21. /// from a collection of assemblies.
  22. /// </summary>
  23. #if SETTINGS_PROVIDER_ENABLED
  24. public sealed class UserSettingsProvider : SettingsProvider
  25. #else
  26. public sealed class UserSettingsProvider
  27. #endif
  28. {
  29. public const string developerModeCategory = "Developer Mode";
  30. const string k_SettingsName = "UserSettingsProviderSettings";
  31. #if SETTINGS_PROVIDER_ENABLED
  32. const int k_LabelWidth = 240;
  33. static int labelWidth
  34. {
  35. get
  36. {
  37. if (s_DefaultLabelWidth != null)
  38. return (int)((float)s_DefaultLabelWidth.GetValue(null, null));
  39. return k_LabelWidth;
  40. }
  41. }
  42. static int defaultLayoutMaxWidth
  43. {
  44. get
  45. {
  46. if (s_DefaultLayoutMaxWidth != null)
  47. return (int)((float)s_DefaultLayoutMaxWidth.GetValue(null, null));
  48. return 0;
  49. }
  50. }
  51. #else
  52. const int k_LabelWidth = 180;
  53. int labelWidth
  54. {
  55. get { return k_LabelWidth; }
  56. }
  57. int defaultLayoutMaxWidth
  58. {
  59. get { return 0; }
  60. }
  61. #endif
  62. List<string> m_Categories;
  63. Dictionary<string, List<PrefEntry>> m_Settings;
  64. Dictionary<string, List<MethodInfo>> m_SettingBlocks;
  65. #if !SETTINGS_PROVIDER_ENABLED
  66. HashSet<string> keywords = new HashSet<string>();
  67. #endif
  68. static readonly string[] s_SearchContext = new string[1];
  69. EventType m_SettingsBlockKeywordsInitialized;
  70. Assembly[] m_Assemblies;
  71. static Settings s_Settings;
  72. Settings m_SettingsInstance;
  73. #if SETTINGS_PROVIDER_ENABLED
  74. static PropertyInfo s_DefaultLabelWidth;
  75. static PropertyInfo s_DefaultLayoutMaxWidth;
  76. #endif
  77. static Settings userSettingsProviderSettings
  78. {
  79. get
  80. {
  81. if (s_Settings == null)
  82. s_Settings = new Settings(new [] { new UserSettingsRepository() });
  83. return s_Settings;
  84. }
  85. }
  86. internal static UserSetting<bool> showHiddenSettings = new UserSetting<bool>(userSettingsProviderSettings, "settings.showHidden", false, SettingsScope.User);
  87. internal static UserSetting<bool> showUnregisteredSettings = new UserSetting<bool>(userSettingsProviderSettings, "settings.showUnregistered", false, SettingsScope.User);
  88. internal static UserSetting<bool> listByKey = new UserSetting<bool>(userSettingsProviderSettings, "settings.listByKey", false, SettingsScope.User);
  89. internal static UserSetting<bool> showUserSettings = new UserSetting<bool>(userSettingsProviderSettings, "settings.showUserSettings", true, SettingsScope.User);
  90. internal static UserSetting<bool> showProjectSettings = new UserSetting<bool>(userSettingsProviderSettings, "settings.showProjectSettings", true, SettingsScope.User);
  91. #if SETTINGS_PROVIDER_ENABLED
  92. /// <summary>
  93. /// Create a new UserSettingsProvider.
  94. /// </summary>
  95. /// <param name="path">The settings menu path.</param>
  96. /// <param name="settings">The Settings instance that this provider is inspecting.</param>
  97. /// <param name="assemblies">A collection of assemblies to scan for <see cref="UserSettingAttribute"/> and <see cref="UserSettingBlockAttribute"/> attributes.</param>
  98. /// <param name="scopes">Which scopes this provider is valid for.</param>
  99. /// <exception cref="ArgumentNullException">Thrown if settings or assemblies is null.</exception>
  100. public UserSettingsProvider(string path, Settings settings, Assembly[] assemblies, SettingsScope scopes = SettingsScope.User)
  101. : base(path, scopes)
  102. #else
  103. /// <summary>
  104. /// Create a new UserSettingsProvider.
  105. /// </summary>
  106. /// <param name="settings">The Settings instance that this provider is inspecting.</param>
  107. /// <param name="assemblies">A collection of assemblies to scan for <see cref="UserSettingAttribute"/> and <see cref="UserSettingBlockAttribute"/> attributes.</param>
  108. public UserSettingsProvider(Settings settings, Assembly[] assemblies)
  109. #endif
  110. {
  111. if (settings == null)
  112. throw new ArgumentNullException("settings");
  113. if (assemblies == null)
  114. throw new ArgumentNullException("assemblies");
  115. m_SettingsInstance = settings;
  116. m_Assemblies = assemblies;
  117. #if !SETTINGS_PROVIDER_ENABLED
  118. SearchForUserSettingAttributes();
  119. #endif
  120. }
  121. #if SETTINGS_PROVIDER_ENABLED
  122. /// <summary>
  123. /// Invoked by the SettingsProvider when activated in the Editor.
  124. /// </summary>
  125. /// <param name="searchContext"></param>
  126. /// <param name="rootElement"></param>
  127. public override void OnActivate(string searchContext, VisualElement rootElement)
  128. {
  129. SearchForUserSettingAttributes();
  130. var window = GetType().GetProperty("settingsWindow", BindingFlags.Instance | BindingFlags.NonPublic);
  131. if (window != null)
  132. {
  133. s_DefaultLabelWidth = window.PropertyType.GetProperty("s_DefaultLabelWidth", BindingFlags.Public | BindingFlags.Static);
  134. s_DefaultLayoutMaxWidth = window.PropertyType.GetProperty("s_DefaultLayoutMaxWidth", BindingFlags.Public | BindingFlags.Static);
  135. }
  136. }
  137. #endif
  138. struct PrefEntry
  139. {
  140. GUIContent m_Content;
  141. IUserSetting m_Pref;
  142. public GUIContent content
  143. {
  144. get { return m_Content; }
  145. }
  146. public IUserSetting pref
  147. {
  148. get { return m_Pref; }
  149. }
  150. public PrefEntry(GUIContent content, IUserSetting pref)
  151. {
  152. m_Content = content;
  153. m_Pref = pref;
  154. }
  155. }
  156. void SearchForUserSettingAttributes()
  157. {
  158. var isDeveloperMode = EditorPrefs.GetBool("DeveloperMode", false);
  159. var keywordsHash = new HashSet<string>();
  160. if (m_Settings != null)
  161. m_Settings.Clear();
  162. else
  163. m_Settings = new Dictionary<string, List<PrefEntry>>();
  164. if (m_SettingBlocks != null)
  165. m_SettingBlocks.Clear();
  166. else
  167. m_SettingBlocks = new Dictionary<string, List<MethodInfo>>();
  168. var types = m_Assemblies.SelectMany(x => x.GetTypes());
  169. // collect instance fields/methods too, but only so we can throw a warning that they're invalid.
  170. var fields = types.SelectMany(x =>
  171. x.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
  172. .Where(prop => Attribute.IsDefined(prop, typeof(UserSettingAttribute))));
  173. var methods = types.SelectMany(x => x.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
  174. .Where(y => Attribute.IsDefined(y, typeof(UserSettingBlockAttribute))));
  175. foreach (var field in fields)
  176. {
  177. if (!field.IsStatic)
  178. {
  179. Debug.LogWarning("Cannot create setting entries for instance fields. Skipping \"" + field.Name + "\".");
  180. continue;
  181. }
  182. var attrib = (UserSettingAttribute)Attribute.GetCustomAttribute(field, typeof(UserSettingAttribute));
  183. if (!attrib.visibleInSettingsProvider)
  184. continue;
  185. var pref = (IUserSetting)field.GetValue(null);
  186. if (pref == null)
  187. {
  188. Debug.LogWarning("[UserSettingAttribute] is only valid for types implementing the IUserSetting interface. Skipping \"" + field.Name + "\"");
  189. continue;
  190. }
  191. var category = string.IsNullOrEmpty(attrib.category) ? "Uncategorized" : attrib.category;
  192. var content = listByKey ? new GUIContent(pref.key) : attrib.title;
  193. if (developerModeCategory.Equals(category) && !isDeveloperMode)
  194. continue;
  195. List<PrefEntry> settings;
  196. if (m_Settings.TryGetValue(category, out settings))
  197. settings.Add(new PrefEntry(content, pref));
  198. else
  199. m_Settings.Add(category, new List<PrefEntry>() { new PrefEntry(content, pref) });
  200. }
  201. foreach (var method in methods)
  202. {
  203. var attrib = (UserSettingBlockAttribute)Attribute.GetCustomAttribute(method, typeof(UserSettingBlockAttribute));
  204. var category = string.IsNullOrEmpty(attrib.category) ? "Uncategorized" : attrib.category;
  205. if (developerModeCategory.Equals(category) && !isDeveloperMode)
  206. continue;
  207. List<MethodInfo> blocks;
  208. var parameters = method.GetParameters();
  209. if (!method.IsStatic || parameters.Length < 1 || parameters[0].ParameterType != typeof(string))
  210. {
  211. Debug.LogWarning("[UserSettingBlockAttribute] is only valid for static functions with a single string parameter. Ex, `static void MySettings(string searchContext)`. Skipping \"" + method.Name + "\"");
  212. continue;
  213. }
  214. if (m_SettingBlocks.TryGetValue(category, out blocks))
  215. blocks.Add(method);
  216. else
  217. m_SettingBlocks.Add(category, new List<MethodInfo>() { method });
  218. }
  219. if (showHiddenSettings)
  220. {
  221. var unlisted = new List<PrefEntry>();
  222. m_Settings.Add("Unlisted", unlisted);
  223. foreach (var pref in UserSettings.FindUserSettings(m_Assemblies, SettingVisibility.Unlisted | SettingVisibility.Hidden))
  224. unlisted.Add(new PrefEntry(new GUIContent(pref.key), pref));
  225. }
  226. if (showUnregisteredSettings)
  227. {
  228. var unregistered = new List<PrefEntry>();
  229. m_Settings.Add("Unregistered", unregistered);
  230. foreach (var pref in UserSettings.FindUserSettings(m_Assemblies, SettingVisibility.Unregistered))
  231. unregistered.Add(new PrefEntry(new GUIContent(pref.key), pref));
  232. }
  233. foreach (var cat in m_Settings)
  234. {
  235. foreach (var entry in cat.Value)
  236. {
  237. var content = entry.content;
  238. if (content != null && !string.IsNullOrEmpty(content.text))
  239. {
  240. foreach (var word in content.text.Split(' '))
  241. keywordsHash.Add(word);
  242. }
  243. }
  244. }
  245. keywords = keywordsHash;
  246. m_Categories = m_Settings.Keys.Union(m_SettingBlocks.Keys).ToList();
  247. m_Categories.Sort();
  248. }
  249. #if SETTINGS_PROVIDER_ENABLED
  250. /// <summary>
  251. /// Invoked by the SettingsProvider container when drawing the UI header.
  252. /// </summary>
  253. public override void OnTitleBarGUI()
  254. {
  255. if (GUILayout.Button(GUIContent.none, SettingsGUIStyles.settingsGizmo))
  256. DoContextMenu();
  257. }
  258. #endif
  259. void InitSettingsBlockKeywords()
  260. {
  261. // Have to let the blocks run twice - one for Layout, one for Repaint.
  262. if (m_SettingsBlockKeywordsInitialized == EventType.Repaint)
  263. return;
  264. m_SettingsBlockKeywordsInitialized = Event.current.type;
  265. // Allows SettingsGUILayout.SettingsField to populate keywords
  266. SettingsGUILayout.s_Keywords = new HashSet<string>(keywords);
  267. // Set a dummy value so that GUI blocks with conditional foldouts will behave as though searching.
  268. s_SearchContext[0] = "Search";
  269. foreach (var category in m_SettingBlocks)
  270. {
  271. foreach (var block in category.Value)
  272. block.Invoke(null, s_SearchContext);
  273. }
  274. keywords = SettingsGUILayout.s_Keywords;
  275. SettingsGUILayout.s_Keywords = null;
  276. s_SearchContext[0] = "";
  277. }
  278. void DoContextMenu()
  279. {
  280. var menu = new GenericMenu();
  281. menu.AddItem(new GUIContent("Reset All"), false, () =>
  282. {
  283. if (!UnityEditor.EditorUtility.DisplayDialog("Reset All Settings", "Reset all settings? This is not undo-able.", "Reset", "Cancel"))
  284. return;
  285. // Do not reset SettingVisibility.Unregistered
  286. foreach (var pref in UserSettings.FindUserSettings(m_Assemblies, SettingVisibility.Visible | SettingVisibility.Hidden | SettingVisibility.Unlisted))
  287. pref.Reset();
  288. m_SettingsInstance.Save();
  289. });
  290. if (EditorPrefs.GetBool("DeveloperMode", false))
  291. {
  292. menu.AddSeparator("");
  293. menu.AddItem(new GUIContent("Developer/List Settings By Key"), listByKey, () =>
  294. {
  295. listByKey.SetValue(!listByKey, true);
  296. SearchForUserSettingAttributes();
  297. });
  298. menu.AddSeparator("Developer/");
  299. menu.AddItem(new GUIContent("Developer/Show User Settings"), showUserSettings, () =>
  300. {
  301. showUserSettings.SetValue(!showUserSettings, true);
  302. SearchForUserSettingAttributes();
  303. });
  304. menu.AddItem(new GUIContent("Developer/Show Project Settings"), showProjectSettings, () =>
  305. {
  306. showProjectSettings.SetValue(!showProjectSettings, true);
  307. SearchForUserSettingAttributes();
  308. });
  309. menu.AddSeparator("Developer/");
  310. menu.AddItem(new GUIContent("Developer/Show Unlisted Settings"), showHiddenSettings, () =>
  311. {
  312. showHiddenSettings.SetValue(!showHiddenSettings, true);
  313. SearchForUserSettingAttributes();
  314. });
  315. menu.AddItem(new GUIContent("Developer/Show Unregistered Settings"), showUnregisteredSettings, () =>
  316. {
  317. showUnregisteredSettings.SetValue(!showUnregisteredSettings, true);
  318. SearchForUserSettingAttributes();
  319. });
  320. menu.AddSeparator("Developer/");
  321. menu.AddItem(new GUIContent("Developer/Open Project Settings File"), false, () =>
  322. {
  323. var project = m_SettingsInstance.GetRepository(SettingsScope.Project);
  324. if (project != null)
  325. {
  326. var path = Path.GetFullPath(project.path);
  327. System.Diagnostics.Process.Start(path);
  328. }
  329. });
  330. menu.AddItem(new GUIContent("Developer/Print All Settings"), false, () =>
  331. {
  332. Debug.Log(UserSettings.GetSettingsString(m_Assemblies));
  333. });
  334. #if UNITY_2019_1_OR_NEWER
  335. menu.AddSeparator("Developer/");
  336. #if UNITY_2019_3_OR_NEWER
  337. menu.AddItem(new GUIContent("Developer/Recompile Scripts"), false, EditorUtility.RequestScriptReload);
  338. #else
  339. menu.AddItem(new GUIContent("Developer/Recompile Scripts"), false, UnityEditorInternal.InternalEditorUtility.RequestScriptReload);
  340. #endif
  341. #endif
  342. }
  343. menu.ShowAsContext();
  344. }
  345. #if SETTINGS_PROVIDER_ENABLED
  346. /// <summary>
  347. /// Invoked by the Settings editor.
  348. /// </summary>
  349. /// <param name="searchContext">
  350. /// A string containing the contents of the search bar.
  351. /// </param>
  352. public override void OnGUI(string searchContext)
  353. #else
  354. /// <summary>
  355. /// Invoked by the Settings editor.
  356. /// </summary>
  357. /// <param name="searchContext">
  358. /// A string containing the contents of the search bar.
  359. /// </param>
  360. public void OnGUI(string searchContext)
  361. #endif
  362. {
  363. #if !SETTINGS_PROVIDER_ENABLED
  364. var evt = Event.current;
  365. if (evt.type == EventType.ContextClick)
  366. DoContextMenu();
  367. #endif
  368. InitSettingsBlockKeywords();
  369. EditorGUIUtility.labelWidth = labelWidth;
  370. EditorGUI.BeginChangeCheck();
  371. var maxWidth = defaultLayoutMaxWidth;
  372. if (maxWidth != 0)
  373. GUILayout.BeginVertical(SettingsGUIStyles.settingsArea, GUILayout.MaxWidth(maxWidth));
  374. else
  375. GUILayout.BeginVertical(SettingsGUIStyles.settingsArea);
  376. var hasSearchContext = !string.IsNullOrEmpty(searchContext);
  377. s_SearchContext[0] = searchContext;
  378. if (hasSearchContext)
  379. {
  380. // todo - Improve search comparison
  381. var searchKeywords = searchContext.Split(' ');
  382. foreach (var settingField in m_Settings)
  383. {
  384. foreach (var setting in settingField.Value)
  385. {
  386. if (searchKeywords.Any(x => !string.IsNullOrEmpty(x) && setting.content.text.IndexOf(x, StringComparison.InvariantCultureIgnoreCase) > -1))
  387. DoPreferenceField(setting.content, setting.pref);
  388. }
  389. }
  390. foreach (var settingsBlock in m_SettingBlocks)
  391. {
  392. foreach (var block in settingsBlock.Value)
  393. {
  394. block.Invoke(null, s_SearchContext);
  395. }
  396. }
  397. }
  398. else
  399. {
  400. foreach (var key in m_Categories)
  401. {
  402. GUILayout.Label(key, EditorStyles.boldLabel);
  403. List<PrefEntry> settings;
  404. if (m_Settings.TryGetValue(key, out settings))
  405. foreach (var setting in settings)
  406. DoPreferenceField(setting.content, setting.pref);
  407. List<MethodInfo> blocks;
  408. if (m_SettingBlocks.TryGetValue(key, out blocks))
  409. foreach (var block in blocks)
  410. block.Invoke(null, s_SearchContext);
  411. GUILayout.Space(8);
  412. }
  413. }
  414. EditorGUIUtility.labelWidth = 0;
  415. GUILayout.EndVertical();
  416. if (EditorGUI.EndChangeCheck())
  417. {
  418. m_SettingsInstance.Save();
  419. }
  420. }
  421. void DoPreferenceField(GUIContent title, IUserSetting pref)
  422. {
  423. if (EditorPrefs.GetBool("DeveloperMode", false))
  424. {
  425. if (pref.scope == SettingsScope.Project && !showProjectSettings)
  426. return;
  427. if (pref.scope == SettingsScope.User && !showUserSettings)
  428. return;
  429. }
  430. if (pref is UserSetting<float>)
  431. {
  432. var cast = (UserSetting<float>)pref;
  433. cast.value = EditorGUILayout.FloatField(title, cast.value);
  434. }
  435. else if (pref is UserSetting<int>)
  436. {
  437. var cast = (UserSetting<int>)pref;
  438. cast.value = EditorGUILayout.IntField(title, cast.value);
  439. }
  440. else if (pref is UserSetting<bool>)
  441. {
  442. var cast = (UserSetting<bool>)pref;
  443. cast.value = EditorGUILayout.Toggle(title, cast.value);
  444. }
  445. else if (pref is UserSetting<string>)
  446. {
  447. var cast = (UserSetting<string>)pref;
  448. cast.value = EditorGUILayout.TextField(title, cast.value);
  449. }
  450. else if (pref is UserSetting<Color>)
  451. {
  452. var cast = (UserSetting<Color>)pref;
  453. cast.value = EditorGUILayout.ColorField(title, cast.value);
  454. }
  455. #if UNITY_2018_3_OR_NEWER
  456. else if (pref is UserSetting<Gradient>)
  457. {
  458. var cast = (UserSetting<Gradient>)pref;
  459. cast.value = EditorGUILayout.GradientField(title, cast.value);
  460. }
  461. #endif
  462. else if (pref is UserSetting<Vector2>)
  463. {
  464. var cast = (UserSetting<Vector2>)pref;
  465. cast.value = EditorGUILayout.Vector2Field(title, cast.value);
  466. }
  467. else if (pref is UserSetting<Vector3>)
  468. {
  469. var cast = (UserSetting<Vector3>)pref;
  470. cast.value = EditorGUILayout.Vector3Field(title, cast.value);
  471. }
  472. else if (pref is UserSetting<Vector4>)
  473. {
  474. var cast = (UserSetting<Vector4>)pref;
  475. cast.value = EditorGUILayout.Vector4Field(title, cast.value);
  476. }
  477. else if (typeof(Enum).IsAssignableFrom(pref.type))
  478. {
  479. Enum val = (Enum)pref.GetValue();
  480. EditorGUI.BeginChangeCheck();
  481. if (Attribute.IsDefined(pref.type, typeof(FlagsAttribute)))
  482. val = EditorGUILayout.EnumFlagsField(title, val);
  483. else
  484. val = EditorGUILayout.EnumPopup(title, val);
  485. if (EditorGUI.EndChangeCheck())
  486. pref.SetValue(val);
  487. }
  488. else if (typeof(UnityEngine.Object).IsAssignableFrom(pref.type))
  489. {
  490. var obj = (UnityEngine.Object)pref.GetValue();
  491. EditorGUI.BeginChangeCheck();
  492. obj = EditorGUILayout.ObjectField(title, obj, pref.type, false);
  493. if (EditorGUI.EndChangeCheck())
  494. pref.SetValue(obj);
  495. }
  496. else
  497. {
  498. GUILayout.BeginHorizontal();
  499. GUILayout.Label(title, GUILayout.Width(EditorGUIUtility.labelWidth - EditorStyles.label.margin.right * 2));
  500. var obj = pref.GetValue();
  501. GUILayout.Label(obj == null ? "null" : pref.GetValue().ToString());
  502. GUILayout.EndHorizontal();
  503. }
  504. SettingsGUILayout.DoResetContextMenuForLastRect(pref);
  505. }
  506. }
  507. }