using UnityEngine; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEditorInternal; using System; using System.IO; using System.Linq; using System.Collections.Generic; using BgTools.Utils; using BgTools.Dialogs; #if (UNITY_EDITOR_LINUX || UNITY_EDITOR_OSX) using System.Text; using System.Globalization; #endif namespace BgTools.PlayerPrefsEditor { public class PreferencesEditorWindow : EditorWindow { #region ErrorValues private readonly int ERROR_VALUE_INT = int.MinValue; private readonly string ERROR_VALUE_STR = ""; #endregion //ErrorValues private enum PreferencesEntrySortOrder { None = 0, Asscending = 1, Descending = 2 } private static string pathToPrefs = String.Empty; private static string platformPathPrefix = @"~"; private string[] userDef; private string[] unityDef; private bool showSystemGroup = false; private PreferencesEntrySortOrder sortOrder = PreferencesEntrySortOrder.None; private SerializedObject serializedObject; private ReorderableList userDefList; private ReorderableList unityDefList; private SerializedProperty[] userDefListCache = new SerializedProperty[0]; private PreferenceEntryHolder prefEntryHolder; private Vector2 scrollPos; private float relSpliterPos; private bool moveSplitterPos = false; private PreferanceStorageAccessor entryAccessor; private MySearchField searchfield; private string searchTxt; private int loadingSpinnerFrame; private bool updateView = false; private bool monitoring = false; private bool showLoadingIndicatorOverlay = false; private readonly List prefKeyValidatorList = new List() { new TextValidator(TextValidator.ErrorType.Error, @"Invalid character detected. Only letters, numbers, space and ,.;:<>_|!§$%&/()=?*+~#-]+$ are allowed", @"(^$)|(^[a-zA-Z0-9 ,.;:<>_|!§$%&/()=?*+~#-]+$)"), new TextValidator(TextValidator.ErrorType.Warning, @"The given key already exist. The existing entry would be overwritten!", (key) => { return !PlayerPrefs.HasKey(key); }) }; #if UNITY_EDITOR_LINUX private readonly char[] invalidFilenameChars = { '"', '\\', '*', '/', ':', '<', '>', '?', '|' }; #elif UNITY_EDITOR_OSX private readonly char[] invalidFilenameChars = { '$', '%', '&', '\\', '/', ':', '<', '>', '|', '~' }; #endif [MenuItem("Tools/BG Tools/PlayerPrefs Editor", false, 1)] static void ShowWindow() { PreferencesEditorWindow window = EditorWindow.GetWindow(false, "Prefs Editor"); window.minSize = new Vector2(270.0f, 300.0f); window.name = "Prefs Editor"; //window.titleContent = EditorGUIUtility.IconContent("SettingsIcon"); // Icon window.Show(); } private void OnEnable() { #if UNITY_EDITOR_WIN pathToPrefs = @"SOFTWARE\Unity\UnityEditor\" + PlayerSettings.companyName + @"\" + PlayerSettings.productName; platformPathPrefix = @""; entryAccessor = new WindowsPrefStorage(pathToPrefs); #elif UNITY_EDITOR_OSX pathToPrefs = @"Library/Preferences/unity." + MakeValidFileName(PlayerSettings.companyName) + "." + MakeValidFileName(PlayerSettings.productName) + ".plist"; entryAccessor = new MacPrefStorage(pathToPrefs); entryAccessor.StartLoadingDelegate = () => { showLoadingIndicatorOverlay = true; }; entryAccessor.StopLoadingDelegate = () => { showLoadingIndicatorOverlay = false; }; #elif UNITY_EDITOR_LINUX pathToPrefs = @".config/unity3d/" + MakeValidFileName(PlayerSettings.companyName) + "/" + MakeValidFileName(PlayerSettings.productName) + "/prefs"; entryAccessor = new LinuxPrefStorage(pathToPrefs); #endif entryAccessor.PrefEntryChangedDelegate = () => { updateView = true; }; monitoring = EditorPrefs.GetBool("BGTools.PlayerPrefsEditor.WatchingForChanges", true); if(monitoring) entryAccessor.StartMonitoring(); sortOrder = (PreferencesEntrySortOrder) EditorPrefs.GetInt("BGTools.PlayerPrefsEditor.SortOrder", 0); searchfield = new MySearchField(); searchfield.DropdownSelectionDelegate = () => { PrepareData(); }; // Fix for serialisation issue of static fields if (userDefList == null) { InitReorderedList(); PrepareData(); } } // Handel view updates for monitored changes // Necessary to avoid main thread access issue private void Update() { if (showLoadingIndicatorOverlay) { loadingSpinnerFrame = (int)Mathf.Repeat(Time.realtimeSinceStartup * 10, 11.99f); PrepareData(); Repaint(); } if (updateView) { updateView = false; PrepareData(); Repaint(); } } private void OnDisable() { entryAccessor.StopMonitoring(); } private void InitReorderedList() { if (prefEntryHolder == null) { var tmp = Resources.FindObjectsOfTypeAll(); if (tmp.Length > 0) { prefEntryHolder = tmp[0]; } else { prefEntryHolder = ScriptableObject.CreateInstance(); } } if (serializedObject == null) { serializedObject = new SerializedObject(prefEntryHolder); } userDefList = new ReorderableList(serializedObject, serializedObject.FindProperty("userDefList"), false, true, true, true); unityDefList = new ReorderableList(serializedObject, serializedObject.FindProperty("unityDefList"), false, true, false, false); relSpliterPos = EditorPrefs.GetFloat("BGTools.PlayerPrefsEditor.RelativeSpliterPosition", 100 / position.width); userDefList.drawHeaderCallback = (Rect rect) => { EditorGUI.LabelField(rect, "User defined"); }; userDefList.drawElementBackgroundCallback = OnDrawElementBackgroundCallback; userDefList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => { SerializedProperty element = GetUserDefListElementAtIndex(index, userDefList.serializedProperty); SerializedProperty key = element.FindPropertyRelative("m_key"); SerializedProperty type = element.FindPropertyRelative("m_typeSelection"); SerializedProperty value; // Load only necessary type switch ((PreferenceEntry.PrefTypes)type.enumValueIndex) { case PreferenceEntry.PrefTypes.Float: value = element.FindPropertyRelative("m_floatValue"); break; case PreferenceEntry.PrefTypes.Int: value = element.FindPropertyRelative("m_intValue"); break; case PreferenceEntry.PrefTypes.String: value = element.FindPropertyRelative("m_strValue"); break; default: value = element.FindPropertyRelative("This should never happen"); break; } float spliterPos = relSpliterPos * rect.width; rect.y += 2; EditorGUI.BeginChangeCheck(); string prefKeyName = key.stringValue; EditorGUI.LabelField(new Rect(rect.x, rect.y, spliterPos - 1, EditorGUIUtility.singleLineHeight), new GUIContent(prefKeyName, prefKeyName)); GUI.enabled = false; EditorGUI.EnumPopup(new Rect(rect.x + spliterPos + 1, rect.y, 60, EditorGUIUtility.singleLineHeight), (PreferenceEntry.PrefTypes)type.enumValueIndex); GUI.enabled = !showLoadingIndicatorOverlay; switch ((PreferenceEntry.PrefTypes)type.enumValueIndex) { case PreferenceEntry.PrefTypes.Float: EditorGUI.DelayedFloatField(new Rect(rect.x + spliterPos + 62, rect.y, rect.width - spliterPos - 60, EditorGUIUtility.singleLineHeight), value, GUIContent.none); break; case PreferenceEntry.PrefTypes.Int: EditorGUI.DelayedIntField(new Rect(rect.x + spliterPos + 62, rect.y, rect.width - spliterPos - 60, EditorGUIUtility.singleLineHeight), value, GUIContent.none); break; case PreferenceEntry.PrefTypes.String: EditorGUI.DelayedTextField(new Rect(rect.x + spliterPos + 62, rect.y, rect.width - spliterPos - 60, EditorGUIUtility.singleLineHeight), value, GUIContent.none); break; } if (EditorGUI.EndChangeCheck()) { entryAccessor.IgnoreNextChange(); switch ((PreferenceEntry.PrefTypes)type.enumValueIndex) { case PreferenceEntry.PrefTypes.Float: PlayerPrefs.SetFloat(key.stringValue, value.floatValue); break; case PreferenceEntry.PrefTypes.Int: PlayerPrefs.SetInt(key.stringValue, value.intValue); break; case PreferenceEntry.PrefTypes.String: PlayerPrefs.SetString(key.stringValue, value.stringValue); break; } PlayerPrefs.Save(); } }; userDefList.onRemoveCallback = (ReorderableList l) => { userDefList.ReleaseKeyboardFocus(); unityDefList.ReleaseKeyboardFocus(); string prefKey = l.serializedProperty.GetArrayElementAtIndex(l.index).FindPropertyRelative("m_key").stringValue; if (EditorUtility.DisplayDialog("Warning!", $"Are you sure you want to delete this entry from PlayerPrefs?\n\nEntry: {prefKey}", "Yes", "No")) { entryAccessor.IgnoreNextChange(); PlayerPrefs.DeleteKey(prefKey); PlayerPrefs.Save(); ReorderableList.defaultBehaviours.DoRemoveButton(l); PrepareData(); GUIUtility.ExitGUI(); } }; userDefList.onAddDropdownCallback = (Rect buttonRect, ReorderableList l) => { var menu = new GenericMenu(); foreach (PreferenceEntry.PrefTypes type in Enum.GetValues(typeof(PreferenceEntry.PrefTypes))) { menu.AddItem(new GUIContent(type.ToString()), false, () => { TextFieldDialog.OpenDialog("Create new property", "Key for the new property:", prefKeyValidatorList, (key) => { entryAccessor.IgnoreNextChange(); switch (type) { case PreferenceEntry.PrefTypes.Float: PlayerPrefs.SetFloat(key, 0.0f); break; case PreferenceEntry.PrefTypes.Int: PlayerPrefs.SetInt(key, 0); break; case PreferenceEntry.PrefTypes.String: PlayerPrefs.SetString(key, string.Empty); break; } PlayerPrefs.Save(); PrepareData(); Focus(); }, this); }); } menu.ShowAsContext(); }; unityDefList.drawElementBackgroundCallback = OnDrawElementBackgroundCallback; unityDefList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => { var element = unityDefList.serializedProperty.GetArrayElementAtIndex(index); SerializedProperty key = element.FindPropertyRelative("m_key"); SerializedProperty type = element.FindPropertyRelative("m_typeSelection"); SerializedProperty value; // Load only necessary type switch ((PreferenceEntry.PrefTypes)type.enumValueIndex) { case PreferenceEntry.PrefTypes.Float: value = element.FindPropertyRelative("m_floatValue"); break; case PreferenceEntry.PrefTypes.Int: value = element.FindPropertyRelative("m_intValue"); break; case PreferenceEntry.PrefTypes.String: value = element.FindPropertyRelative("m_strValue"); break; default: value = element.FindPropertyRelative("This should never happen"); break; } float spliterPos = relSpliterPos * rect.width; rect.y += 2; GUI.enabled = false; string prefKeyName = key.stringValue; EditorGUI.LabelField(new Rect(rect.x, rect.y, spliterPos - 1, EditorGUIUtility.singleLineHeight), new GUIContent(prefKeyName, prefKeyName)); EditorGUI.EnumPopup(new Rect(rect.x + spliterPos + 1, rect.y, 60, EditorGUIUtility.singleLineHeight), (PreferenceEntry.PrefTypes)type.enumValueIndex); switch ((PreferenceEntry.PrefTypes)type.enumValueIndex) { case PreferenceEntry.PrefTypes.Float: EditorGUI.DelayedFloatField(new Rect(rect.x + spliterPos + 62, rect.y, rect.width - spliterPos - 60, EditorGUIUtility.singleLineHeight), value, GUIContent.none); break; case PreferenceEntry.PrefTypes.Int: EditorGUI.DelayedIntField(new Rect(rect.x + spliterPos + 62, rect.y, rect.width - spliterPos - 60, EditorGUIUtility.singleLineHeight), value, GUIContent.none); break; case PreferenceEntry.PrefTypes.String: EditorGUI.DelayedTextField(new Rect(rect.x + spliterPos + 62, rect.y, rect.width - spliterPos - 60, EditorGUIUtility.singleLineHeight), value, GUIContent.none); break; } GUI.enabled = !showLoadingIndicatorOverlay; }; unityDefList.drawHeaderCallback = (Rect rect) => { EditorGUI.LabelField(rect, "Unity defined"); }; } private void OnDrawElementBackgroundCallback(Rect rect, int index, bool isActive, bool isFocused) { if (Event.current.type == EventType.Repaint) { ReorderableList.defaultBehaviours.elementBackground.Draw(rect, false, isActive, isActive, isFocused); } Rect spliterRect = new Rect(rect.x + relSpliterPos * rect.width, rect.y, 2, rect.height); EditorGUIUtility.AddCursorRect(spliterRect, MouseCursor.ResizeHorizontal); if (Event.current.type == EventType.MouseDown && spliterRect.Contains(Event.current.mousePosition)) { moveSplitterPos = true; } if(moveSplitterPos) { if (Event.current.mousePosition.x > 100 && Event.current.mousePosition.x= Enum.GetValues(typeof(PreferencesEntrySortOrder)).Length) { sortOrder = 0; } EditorPrefs.SetInt("BGTools.PlayerPrefsEditor.SortOrder", (int) sortOrder); PrepareData(false); } GUIContent watcherContent = (entryAccessor.IsMonitoring()) ? new GUIContent(ImageManager.Watching, "Watching changes") : new GUIContent(ImageManager.NotWatching, "Not watching changes"); if (GUILayout.Button(watcherContent, EditorStyles.toolbarButton)) { monitoring = !monitoring; EditorPrefs.SetBool("BGTools.PlayerPrefsEditor.WatchingForChanges", monitoring); if (monitoring) entryAccessor.StartMonitoring(); else entryAccessor.StopMonitoring(); Repaint(); } if (GUILayout.Button(new GUIContent(ImageManager.Refresh, "Refresh"), EditorStyles.toolbarButton)) { PlayerPrefs.Save(); PrepareData(); } if (GUILayout.Button(new GUIContent(ImageManager.Trash, "Delete all"), EditorStyles.toolbarButton)) { if (EditorUtility.DisplayDialog("Warning!", "Are you sure you want to delete ALL entries from PlayerPrefs?\n\nUse with caution! Unity defined keys are affected too.", "Yes", "No")) { PlayerPrefs.DeleteAll(); PrepareData(); GUIUtility.ExitGUI(); } } EditorGUIUtility.SetIconSize(new Vector2(0.0f, 0.0f)); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Box(ImageManager.GetOsIcon(), Styles.icon); GUILayout.TextField(platformPathPrefix + Path.DirectorySeparatorChar + pathToPrefs, GUILayout.MinWidth(200)); GUILayout.EndHorizontal(); scrollPos = GUILayout.BeginScrollView(scrollPos); serializedObject.Update(); userDefList.DoLayoutList(); serializedObject.ApplyModifiedProperties(); GUILayout.FlexibleSpace(); showSystemGroup = EditorGUILayout.Foldout(showSystemGroup, new GUIContent("Show System")); if (showSystemGroup) { unityDefList.DoLayoutList(); } GUILayout.EndScrollView(); GUILayout.EndVertical(); GUI.enabled = true; if (showLoadingIndicatorOverlay) { GUILayout.BeginArea(new Rect(position.size.x * 0.5f - 30, position.size.y * 0.5f - 25, 60, 50), GUI.skin.box); GUILayout.FlexibleSpace(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Box(ImageManager.SpinWheelIcons[loadingSpinnerFrame], Styles.icon); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Label("Loading"); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); GUILayout.FlexibleSpace(); GUILayout.EndArea(); } GUI.contentColor = defaultColor; } catch (InvalidOperationException) { } } private void PrepareData(bool reloadKeys = true) { prefEntryHolder.ClearLists(); LoadKeys(out userDef, out unityDef, reloadKeys); CreatePrefEntries(userDef, ref prefEntryHolder.userDefList); CreatePrefEntries(unityDef, ref prefEntryHolder.unityDefList); // Clear cache userDefListCache = new SerializedProperty[prefEntryHolder.userDefList.Count]; } private void CreatePrefEntries(string[] keySource, ref List listDest) { if (!string.IsNullOrEmpty(searchTxt) && searchfield.SearchMode == MySearchField.SearchModePreferencesEditorWindow.Key) { keySource = keySource.Where((keyEntry) => keyEntry.ToLower().Contains(searchTxt.ToLower())).ToArray(); } foreach (string key in keySource) { var entry = new PreferenceEntry(); entry.m_key = key; string s = PlayerPrefs.GetString(key, ERROR_VALUE_STR); if (s != ERROR_VALUE_STR) { entry.m_strValue = s; entry.m_typeSelection = PreferenceEntry.PrefTypes.String; listDest.Add(entry); continue; } float f = PlayerPrefs.GetFloat(key, float.NaN); if (!float.IsNaN(f)) { entry.m_floatValue = f; entry.m_typeSelection = PreferenceEntry.PrefTypes.Float; listDest.Add(entry); continue; } int i = PlayerPrefs.GetInt(key, ERROR_VALUE_INT); if (i != ERROR_VALUE_INT) { entry.m_intValue = i; entry.m_typeSelection = PreferenceEntry.PrefTypes.Int; listDest.Add(entry); continue; } } if (!string.IsNullOrEmpty(searchTxt) && searchfield.SearchMode == MySearchField.SearchModePreferencesEditorWindow.Value) { listDest = listDest.Where((preferenceEntry) => preferenceEntry.ValueAsString().ToLower().Contains(searchTxt.ToLower())).ToList(); } switch(sortOrder) { case PreferencesEntrySortOrder.Asscending: listDest.Sort((PreferenceEntry x, PreferenceEntry y) => { return x.m_key.CompareTo(y.m_key); }); break; case PreferencesEntrySortOrder.Descending: listDest.Sort((PreferenceEntry x, PreferenceEntry y) => { return y.m_key.CompareTo(x.m_key); }); break; } } private void LoadKeys(out string[] userDef, out string[] unityDef, bool reloadKeys) { string[] keys = entryAccessor.GetKeys(reloadKeys); //keys.ToList().ForEach( e => { Debug.Log(e); } ); // Seperate keys int unity defined and user defined Dictionary> groups = keys .GroupBy( (key) => key.StartsWith("unity.") || key.StartsWith("UnityGraphicsQuality") ) .ToDictionary( (g) => g.Key, (g) => g.ToList() ); unityDef = (groups.ContainsKey(true)) ? groups[true].ToArray() : new string[0]; userDef = (groups.ContainsKey(false)) ? groups[false].ToArray() : new string[0]; } private SerializedProperty GetUserDefListElementAtIndex(int index, SerializedProperty ListProperty) { UnityEngine.Assertions.Assert.IsTrue(ListProperty.isArray, "Given 'ListProperts' is not type of array"); if (userDefListCache[index] == null) { userDefListCache[index] = ListProperty.GetArrayElementAtIndex(index); } return userDefListCache[index]; } #if (UNITY_EDITOR_LINUX || UNITY_EDITOR_OSX) private string MakeValidFileName(string unsafeFileName) { string normalizedFileName = unsafeFileName.Trim().Normalize(NormalizationForm.FormD); StringBuilder stringBuilder = new StringBuilder(); // We need to use a TextElementEmumerator in order to support UTF16 characters that may take up more than one char(case 1169358) TextElementEnumerator charEnum = StringInfo.GetTextElementEnumerator(normalizedFileName); while (charEnum.MoveNext()) { string c = charEnum.GetTextElement(); if (c.Length == 1 && invalidFilenameChars.Contains(c[0])) { stringBuilder.Append('_'); continue; } UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c, 0); if (unicodeCategory != UnicodeCategory.NonSpacingMark) stringBuilder.Append(c); } return stringBuilder.ToString().Normalize(NormalizationForm.FormC); } #endif } } public class MySearchField : SearchField { public enum SearchModePreferencesEditorWindow { Key, Value } public SearchModePreferencesEditorWindow SearchMode { get; private set; } public Action DropdownSelectionDelegate; public new string OnGUI( Rect rect, string text, GUIStyle style, GUIStyle cancelButtonStyle, GUIStyle emptyCancelButtonStyle) { style.padding.left = 17; Rect ContextMenuRect = new Rect(rect.x, rect.y, 10, rect.height); // Add interactive area EditorGUIUtility.AddCursorRect(ContextMenuRect, MouseCursor.Text); if (Event.current.type == EventType.MouseDown && ContextMenuRect.Contains(Event.current.mousePosition)) { void OnDropdownSelection(object parameter) { SearchMode = (SearchModePreferencesEditorWindow) Enum.Parse(typeof(SearchModePreferencesEditorWindow), parameter.ToString()); DropdownSelectionDelegate(); } GenericMenu menu = new GenericMenu(); foreach(SearchModePreferencesEditorWindow EnumIt in Enum.GetValues(typeof(SearchModePreferencesEditorWindow))) { String EnumName = Enum.GetName(typeof(SearchModePreferencesEditorWindow), EnumIt); menu.AddItem(new GUIContent(EnumName), SearchMode == EnumIt, OnDropdownSelection, EnumName); } menu.DropDown(rect); } // Render original search field String result = base.OnGUI(rect, text, style, cancelButtonStyle, emptyCancelButtonStyle); // Render additional images GUIStyle ContexMenuOverlayStyle = GUIStyle.none; ContexMenuOverlayStyle.contentOffset = new Vector2(9, 5); GUI.Box(new Rect(rect.x, rect.y, 5, 5), EditorGUIUtility.IconContent("d_ProfilerTimelineDigDownArrow@2x"), ContexMenuOverlayStyle); if (!HasFocus() && String.IsNullOrEmpty(text)) { GUI.enabled = false; GUI.Label(new Rect(rect.x + 14, rect.y, 40, rect.height), Enum.GetName(typeof(SearchModePreferencesEditorWindow), SearchMode)); GUI.enabled = true; } ContexMenuOverlayStyle.contentOffset = new Vector2(); return result; } public new string OnToolbarGUI(string text, params GUILayoutOption[] options) => this.OnToolbarGUI(GUILayoutUtility.GetRect(29f, 200f, 18f, 18f, EditorStyles.toolbarSearchField, options), text); public new string OnToolbarGUI(Rect rect, string text) => this.OnGUI(rect, text, EditorStyles.toolbarSearchField, EditorStyles.toolbarButton, EditorStyles.toolbarButton); }