forked from Shardstone/trail-into-darkness
388 lines
17 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|