using System.Collections.Generic; using UnityEngine; using UnityEditor; using System.IO; using UnityEditor.SceneManagement; using UnityEngine.SceneManagement; namespace Jovian.Recents { public class RecentAssets : EditorWindow { [System.Serializable] private class AssetSelection { public bool expanded; public string assetPath; [System.NonSerialized] private Object actualAsset; public Object Asset { get { if(actualAsset == null) { actualAsset = AssetDatabase.LoadMainAssetAtPath(assetPath); } return actualAsset; } set => actualAsset = value; } public List subAssets; public bool isPinned; public string FileName => Path.GetFileName(assetPath); } [System.Serializable] public struct SubAssetSelection { // In the current structure of this tool, to reload this object is quite messy. // The SubAsset doesn't know if the current context, e.g. scene, is the one it was previously selected from. // There might be another asset in this other scene also called "Main Camera" etc [System.NonSerialized] public Object subAsset; public string subAssetPath; } private bool subscribeToSelectionEvents; private bool SubscribeToSelectionEvents { set { if(value) { Selection.selectionChanged -= OnSelectionChanged; Selection.selectionChanged += OnSelectionChanged; } else { Selection.selectionChanged -= OnSelectionChanged; } subscribeToSelectionEvents = value; } } private bool subscribeToPrefabOpenEvents; private bool SubscribeToPrefabOpen { set { PrefabStage.prefabStageOpened -= OnPrefabStageOpened; PrefabStage.prefabStageOpened += OnPrefabStageOpened; PrefabStage.prefabStageClosing -= OnPrefabStageClosing; PrefabStage.prefabStageClosing += OnPrefabStageClosing; subscribeToPrefabOpenEvents = value; } } private bool subscribeToSceneOpenEvents; private bool SubscribeToSceneOpenEvents { set { if(value) { EditorSceneManager.activeSceneChangedInEditMode -= OnActiveSceneChangedInEditMode; EditorSceneManager.activeSceneChangedInEditMode += OnActiveSceneChangedInEditMode; } else { EditorSceneManager.activeSceneChangedInEditMode -= OnActiveSceneChangedInEditMode; } subscribeToSceneOpenEvents = value; } } private string EditorPrefsSettingsSelectionKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_Selections"; private string EditorPrefsSettingsPrefabsKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_Prefabs"; private string EditorPrefsSettingsScenesKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_Scenes"; private string EditorPrefsSettingsCountKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections_MaxHistory"; private string EditorPrefsSettingsHistoryKey => $"{Application.dataPath}Jovian_RecentOpenedAssetsWindow_AssetSelections"; private const string PinnedIcon = "IN LockButton on"; private const string UnPinnedIcon = "IN LockButton"; [System.Serializable] private class JsonWrapper { public JsonWrapper(List data) { this.data = data; } public List data; } private List selectionHistory = new(); private int maxHistory = 10; private Vector2 scrollPos; private PrefabStage prefabStage; private GUIStyle guiStyle; [MenuItem("Jovian/Assets History...", false, 20)] private static void Init() { var window = GetWindow(false, "Assets History"); window.minSize = new Vector2(100f, 100f); window.Focus(); } private void OnEnable() { // In case of Editor reload, perhaps because of recompile, events needs to be resubscribed LoadSettings(); SubscribeToSelectionEvents = subscribeToSelectionEvents; SubscribeToSceneOpenEvents = subscribeToSceneOpenEvents; SubscribeToPrefabOpen = subscribeToPrefabOpenEvents; LoadEntries(); } private void OnDisable() { SaveEntries(); SaveSettings(); } private void OnDestroy() { SubscribeToSelectionEvents = false; SubscribeToSceneOpenEvents = false; SubscribeToPrefabOpen = false; } private void SaveEntries() { var jsonList = JsonUtility.ToJson(new JsonWrapper(selectionHistory)); EditorPrefs.SetString(EditorPrefsSettingsHistoryKey, jsonList); } private void SaveSettings() { EditorPrefs.SetBool(EditorPrefsSettingsSelectionKey, subscribeToSelectionEvents); EditorPrefs.SetBool(EditorPrefsSettingsPrefabsKey, subscribeToPrefabOpenEvents); EditorPrefs.SetBool(EditorPrefsSettingsScenesKey, subscribeToSceneOpenEvents); EditorPrefs.SetInt(EditorPrefsSettingsCountKey, maxHistory); } private void LoadEntries() { var jsonList = EditorPrefs.GetString(EditorPrefsSettingsHistoryKey); var oldList = JsonUtility.FromJson(jsonList)?.data; if(oldList != null) { selectionHistory = oldList; } } private void LoadSettings() { if(EditorPrefs.HasKey(EditorPrefsSettingsSelectionKey)) { subscribeToSelectionEvents = EditorPrefs.GetBool(EditorPrefsSettingsSelectionKey); } if(EditorPrefs.HasKey(EditorPrefsSettingsPrefabsKey)) { subscribeToPrefabOpenEvents = EditorPrefs.GetBool(EditorPrefsSettingsPrefabsKey); } if(EditorPrefs.HasKey(EditorPrefsSettingsScenesKey)) { subscribeToSceneOpenEvents = EditorPrefs.GetBool(EditorPrefsSettingsScenesKey); } if(EditorPrefs.HasKey(EditorPrefsSettingsCountKey)) { maxHistory = EditorPrefs.GetInt(EditorPrefsSettingsCountKey); } } private void OnActiveSceneChangedInEditMode(Scene previousScene, Scene newScene) { var selectedObject = AssetDatabase.LoadMainAssetAtPath(newScene.path); if(selectedObject != null) { OnAssetInteracted(selectedObject, newScene.path); } } private void OnPrefabStageOpened(PrefabStage prefab) { // We actually need to know if we're in prefabStage because of how asset parenting works during prefab editing, e.g. when _subscribeToSelectionEvents==true; prefabStage = prefab; if(subscribeToPrefabOpenEvents) { OnAssetInteracted(prefab.prefabContentsRoot, prefab.assetPath); } } private void OnPrefabStageClosing(PrefabStage prefab) { prefabStage = null; } private void OnSelectionChanged() { var selectedObject = Selection.activeObject; if(!selectedObject) { return; // When you select something in e.g. Project View that isn't an asset or object, perhaps the category header "Packages" } string assetPath; if(prefabStage != null && selectedObject is GameObject selectedGameObject && prefabStage.IsPartOfPrefabContents(selectedGameObject)) { assetPath = prefabStage.assetPath; // If we're in a PrefabStage and selecting a child GO, the assetPath is the actual prefab itself, not this potentially nested entity } else { assetPath = AssetDatabase.GetAssetOrScenePath(selectedObject); } OnAssetInteracted(selectedObject, assetPath); } private void OnAssetInteracted(Object selectedObject, string assetPath) { AssetSelection selection = null; for(var i = 0; i < selectionHistory.Count; i++) { if(selectionHistory[i].assetPath == assetPath) { selection = selectionHistory[i]; selectionHistory.RemoveAt(i); break; } } if(selection == null) { // This is a new asset selection selection = new AssetSelection { expanded = false, assetPath = assetPath, Asset = AssetDatabase.LoadMainAssetAtPath(assetPath), subAssets = new List() }; } selectionHistory.Insert(0, selection); // Selection is now first in list var subAssetSelection = new SubAssetSelection { subAsset = selectedObject, subAssetPath = selectedObject.name }; for(var i = 0; i < selection.subAssets.Count; i++) { if(selection.subAssets[i].subAsset == selectedObject || // In case of unloading and reloading a scene, we try to "reuse" entries to the same object. // If several objects exist by the same name, they're already null and become a new entry "anyway" (selection.subAssets[i].subAsset == null && selection.subAssets[i].subAssetPath == subAssetSelection.subAssetPath)) { selection.subAssets.RemoveAt(i); break; } } selection.subAssets.Insert(0, subAssetSelection); // while instead of if, since you can change the number of tracked assets while(selectionHistory.Count > maxHistory) { selectionHistory.RemoveAt(selectionHistory.Count - 1); } UpdatePinned(); Repaint(); } private void UpdatePinned() { var numberOfAlreadyPinnedEntries = 0; for(var i = 0; i < selectionHistory.Count; i++) { var currSelection = selectionHistory[i]; if(currSelection.isPinned) { if(i == numberOfAlreadyPinnedEntries) { numberOfAlreadyPinnedEntries++; continue; } selectionHistory.RemoveAt(i); selectionHistory.Insert(numberOfAlreadyPinnedEntries, currSelection); numberOfAlreadyPinnedEntries++; } } } private void OnGUI() { var originalIconSize = EditorGUIUtility.GetIconSize(); EditorGUIUtility.SetIconSize(new Vector2(16f, 16f)); if(guiStyle == null) { guiStyle = new GUIStyle(GUI.skin.button) { alignment = TextAnchor.MiddleLeft }; } EditorGUILayout.BeginHorizontal(); TightLabel("History count", "The number of recent assets currently tracked"); TightLabel(selectionHistory.Count.ToString()); if(GUILayout.Button("Clear", GUILayout.ExpandWidth(false))) { selectionHistory.Clear(); } GUILayout.FlexibleSpace(); TightLabel("What to track", "Select for what interactions you want this list to be updated"); var onSelection = GUILayout.Toggle(subscribeToSelectionEvents, "On selection", GUILayout.ExpandWidth(false)); if(onSelection != subscribeToSelectionEvents) { SubscribeToSelectionEvents = onSelection; } EditorGUI.BeginDisabledGroup(subscribeToSelectionEvents); // If we're listening to selection the other two doesn't matter subscribeToPrefabOpenEvents = GUILayout.Toggle(subscribeToPrefabOpenEvents, "Open prefabs", GUILayout.ExpandWidth(false)); SubscribeToSceneOpenEvents = GUILayout.Toggle(subscribeToSceneOpenEvents, "Open scenes", GUILayout.ExpandWidth(false)); EditorGUI.EndDisabledGroup(); GUILayout.FlexibleSpace(); TightLabel("Max history", "The number of recent assets to track"); maxHistory = EditorGUILayout.DelayedIntField(maxHistory, GUILayout.Width(50f)); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); scrollPos = EditorGUILayout.BeginScrollView(scrollPos); var pinStatusChanged = false; for(var i = 0; i < selectionHistory.Count; i++) { var selection = selectionHistory[i]; EditorGUILayout.BeginHorizontal(); // Pinning var pinIcon = selection.isPinned ? PinnedIcon : UnPinnedIcon; if(GUILayout.Button(EditorGUIUtility.IconContent(pinIcon), GUILayout.Width(20f), GUILayout.ExpandWidth(false))) { selection.isPinned = !selection.isPinned; pinStatusChanged = true; } // Open if(GUILayout.Button(EditorGUIUtility.IconContent("d_editicon.sml"), GUILayout.Width(26f), GUILayout.ExpandWidth(false))) { if(Event.current.button == 1) { // On right click we only ping the object. EditorGUIUtility.PingObject(selection.Asset); } else { AssetDatabase.OpenAsset(selection.Asset); } } // Object GUI.enabled = false; if(selection.Asset != null) { EditorGUILayout.ObjectField(selection.Asset, typeof(Object), false); } else { var labelNotFound = new GUIContent($"{selection.FileName} not found.", EditorGUIUtility.ObjectContent(selection.Asset, typeof(Object)).image, selection.assetPath); EditorGUILayout.LabelField(labelNotFound); } GUI.enabled = true; // Sub entries GUILayout.Label(GUIContent.none, GUILayout.MinWidth(80f), GUILayout.MaxWidth(80f)); var rect = GUILayoutUtility.GetLastRect(); selection.expanded = EditorGUI.Foldout(rect, selection.expanded, "Sub entries", true); EditorGUILayout.EndHorizontal(); if(selection.expanded) { EditorGUILayout.BeginVertical(); EditorGUI.indentLevel++; for(var y = 0; y < selection.subAssets.Count; y++) { var subAsset = selection.subAssets[y]; EditorGUILayout.BeginHorizontal(); EditorGUILayout.Space(10f, false); // As an indent var label = new GUIContent(subAsset.subAssetPath, EditorGUIUtility.ObjectContent(subAsset.subAsset, typeof(Object)).image); if(GUILayout.Button(label, guiStyle, GUILayout.MinWidth(50f), GUILayout.MaxWidth(200f), GUILayout.ExpandWidth(false))) { if(Event.current.button == 1) { // On right click we only ping the object. EditorGUIUtility.PingObject(subAsset.subAsset); } else { Selection.activeObject = subAsset.subAsset; } } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); EditorGUI.indentLevel--; } } EditorGUILayout.EndScrollView(); EditorGUIUtility.SetIconSize(originalIconSize); if(pinStatusChanged) { UpdatePinned(); } } public static void TightLabel(string labelStr) { var label = new GUIContent(labelStr); TightLabel(label); } public static void TightLabel(string labelStr, string tooltip) { var label = new GUIContent(labelStr, tooltip); TightLabel(label); } public static void TightLabel(GUIContent label) { //This is the important bit, we set the width to the calculated width of the content in the GUIStyle of the control EditorGUILayout.LabelField(label, GUILayout.Width(GUI.skin.label.CalcSize(label).x)); } } }