Files
trail-into-darkness/Packages/com.jovian.assets-history/Editor/RecentAssetsMenu.cs
Sebastian Bularca 36d3f112ef added zlinq
2026-04-02 07:43:33 +02:00

388 lines
17 KiB
C#

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<SubAssetSelection> 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<AssetSelection> data) {
this.data = data;
}
public List<AssetSelection> data;
}
private List<AssetSelection> selectionHistory = new();
private int maxHistory = 10;
private Vector2 scrollPos;
private PrefabStage prefabStage;
private GUIStyle guiStyle;
[MenuItem("Jovian/Utilities/Assets History...", false, 20)]
private static void Init() {
var window = GetWindow<RecentAssets>(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<JsonWrapper>(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<SubAssetSelection>()
};
}
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));
}
}
}