Skip to content

Instantly share code, notes, and snippets.

@restush
Last active September 30, 2024 11:37
Show Gist options
  • Save restush/849b5662dd82b7eb68ee78950e1a8033 to your computer and use it in GitHub Desktop.
Save restush/849b5662dd82b7eb68ee78950e1a8033 to your computer and use it in GitHub Desktop.
Scene List & Pins - Scene Dasboard, for quick access your favorite scenes. Made with UI Toolkit.

image

How to open: On Unity menu bar -> Tools -> Scene List and Pins
/// https://gist.github.com/restush/849b5662dd82b7eb68ee78950e1a8033
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UIElements;
public class SceneListEditorWindow : EditorWindow
{
private class UserData
{
public Dictionary<string, System.Action> _callbacks = new();
public void AddCallback(string key, System.Action callback)
{
if (_callbacks.ContainsKey(key))
_callbacks[key] = callback;
else
_callbacks.Add(key, callback);
}
public void RemoveCallback(string key, Button button)
{
if (_callbacks.ContainsKey(key))
{
button.clicked -= _callbacks[key];
_callbacks.Remove(key);
}
}
}
[System.Serializable]
private class HorizontalItemData
{
public string SceneName;
public string GUID;
public bool Pinned;
public int PinnedIndex;
}
private List<HorizontalItemData> dataList = new();
private List<HorizontalItemData> pinnedList = new();
[System.Serializable]
private class StateData
{
public List<HorizontalItemData> pinnedList;
}
[SerializeField] VisualTreeAsset tree;
[SerializeField] VisualTreeAsset treeButton;
private ListView pinnedView;
private ListView unpinnedView;
private string jsonFilePath;
private Label header;
private Label header2;
[MenuItem("Tools/Scene List and Pins")]
static void CreateMenu()
{
var window = GetWindow<SceneListEditorWindow>();
window.titleContent = new GUIContent("Scene List & Pins");
window.Focus();
}
private void CreateGUI()
{
Load();
pinnedList = dataList.FindAll(x => x.Pinned);
dataList.RemoveAll(x => x.Pinned);
pinnedList = pinnedList.OrderBy(x => x.PinnedIndex).ToList();
pinnedView = new ListView()
{
itemsSource = pinnedList,
makeItem = () => CreateItem(),
bindItem = (v, i) => Bind(v, i, true),
showAlternatingRowBackgrounds = AlternatingRowBackground.All,
reorderable = true,
reorderMode = ListViewReorderMode.Animated,
makeNoneElement = () => new VisualElement()
};
pinnedView.style.unityTextAlign = TextAnchor.MiddleCenter;
pinnedView.style.flexGrow = 1;
pinnedView.style.minHeight = PinnedMinHeight();
// callback on reorder
pinnedView.itemIndexChanged += (oldIndex, newIndex) =>
{
Save();
};
pinnedView.selectionType = SelectionType.None;
unpinnedView = new ListView()
{
itemsSource = dataList,
makeItem = () => CreateItem(),
bindItem = (v, i) => Bind(v, i, false)
,
};
unpinnedView.selectionType = SelectionType.None;
unpinnedView.style.flexGrow = 1;
var root = new VisualElement() { name = "Root" };
rootVisualElement.Add(root);
header = new Label("Pinned Scenes") { name = "Header" };
header.style.unityFontStyleAndWeight = FontStyle.Bold;
header.style.fontSize = 18;
header.style.unityTextAlign = TextAnchor.MiddleCenter;
header.style.marginBottom = 2;
root.Add(header);
root.Add(pinnedView);
header2 = new Label("Unpinned Scenes") { name = "Header" };
header2.style.unityFontStyleAndWeight = FontStyle.Bold;
header2.style.fontSize = 18;
header2.style.unityTextAlign = TextAnchor.MiddleCenter;
header2.style.marginBottom = 2;
root.Add(header2);
root.Add(unpinnedView);
if (pinnedList.Count == 0)
{
if (pinnedView != null) HideVisualElement(pinnedView);
if (header != null) HideVisualElement(header);
}
}
void Bind(VisualElement v, int index, bool isPinnedList)
{
var i = index;
var item = (HorizontalItemData)default;
if(isPinnedList)
{
// check valid pinnedList index to prevent out of range
if(pinnedList.Count > i)
{
item = pinnedList[i];
}
}
else
{
if (dataList.Count > i)
{
item = dataList[i];
}
}
if (item == null)
return;
var sceneButton = v.Q<Button>("SceneButton");
sceneButton.text = item.SceneName;
System.Action onClickScene = () => EditorSceneManager.OpenScene(AssetDatabase.GUIDToAssetPath(item.GUID));
SubscribeToButton(sceneButton, onClickScene);
var pinButton = v.Q<Button>("Pin");
item.Pinned = !isPinnedList;
pinButton.text = isPinnedList ? "Unpin" : "Pin";
System.Action clicked = () =>
{
Debug.Log("Clicked " + item.SceneName);
UnsubscribeToButton(pinButton);
item.Pinned = !isPinnedList;
if (!isPinnedList)
{
item.PinnedIndex = pinnedList.Count;
pinnedList.Add(item);
dataList.Remove(item);
}
else
{
pinnedList.Remove(item);
dataList.Insert(0, item);
}
pinnedView.style.minHeight = PinnedMinHeight();
if (!isPinnedList) // if isPinnedList = false, show header and pinned view
{
ShowVisualElement(pinnedView);
ShowVisualElement(header);
}
pinnedView.RefreshItem(i);
unpinnedView.RefreshItem(i);
//pinnedView.Rebuild();
//unpinnedView.Rebuild();
Save();
};
SubscribeToButton(pinButton, clicked);
var findButton = v.Q<Button>("Find");
System.Action clickedFind = () => EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath<SceneAsset>(AssetDatabase.GUIDToAssetPath(item.GUID)));
SubscribeToButton(findButton, clickedFind);
}
private void Load()
{
// clear to prevent duplicate
dataList.Clear();
pinnedList.Clear();
var sceneAssets = FindAssetsOfType<SceneAsset>();
string currentFilePath = AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this));
string currentDirectory = Path.GetDirectoryName(currentFilePath);
jsonFilePath = Path.Combine(currentDirectory, "SceneList-JSON.json");
foreach (var sceneAsset in sceneAssets)
{
string path = AssetDatabase.GetAssetPath(sceneAsset);
dataList.Add(new HorizontalItemData() { SceneName = sceneAsset.name, GUID = AssetDatabase.AssetPathToGUID(path), Pinned = false });
}
if (File.Exists(jsonFilePath))
{
string json = File.ReadAllText(jsonFilePath);
StateData stateData = JsonUtility.FromJson<StateData>(json);
// use except to remove pinned scene from dataList
if (stateData != null && stateData.pinnedList != null && stateData.pinnedList.Count > 0)
{
var count = dataList.Count;
for (int i = 0; i < count; i++)
{
for (int j = 0; j < stateData.pinnedList.Count; j++)
{
if (dataList[i].GUID == stateData.pinnedList[j].GUID)
{
dataList[i].Pinned = true;
dataList[i].PinnedIndex = stateData.pinnedList[j].PinnedIndex;
break;
}
}
}
}
}
}
private VisualElement CreateItem()
{
var horizontalItem = new VisualElement() { name = "HorizontalItem" };
var sceneButton = new Button() { name = "SceneButton" };
sceneButton.style.minWidth = 210;
sceneButton.style.flexGrow = 1;
sceneButton.style.unityTextAlign = TextAnchor.MiddleLeft;
var pinButton = new Button() { name = "Pin", text = "Pin" };
var findButton = new Button() { name = "Find", text = "Find" };
horizontalItem.Add(sceneButton);
horizontalItem.Add(pinButton);
horizontalItem.Add(findButton);
horizontalItem.style.flexDirection = FlexDirection.Row;
pinButton.style.maxWidth = 70;
findButton.style.maxWidth = 70;
return horizontalItem;
}
private int PinnedMinHeight()
{
if (pinnedList.Count > 0)
{
if (pinnedView != null) pinnedView.style.marginBottom = 10;
if (unpinnedView != null) unpinnedView.style.marginTop = 10;
}
if (pinnedList.Count == 0)
{
if (pinnedView != null) HideVisualElement(pinnedView);
if (header != null) HideVisualElement(header);
}
const int maxItem = 10;
int heightIncrease = 22;
return Mathf.Clamp(pinnedList.Count * heightIncrease, 0, maxItem * heightIncrease);
}
private void ShowVisualElement(VisualElement visualElement)
{
visualElement.style.display = DisplayStyle.Flex;
visualElement.style.visibility = Visibility.Visible;
visualElement.pickingMode = PickingMode.Position;
}
private void HideVisualElement(VisualElement visualElement)
{
visualElement.style.display = DisplayStyle.None;
visualElement.style.visibility = Visibility.Hidden;
visualElement.pickingMode = PickingMode.Ignore;
}
private void SubscribeToButton(Button button, System.Action clicked)
{
UnsubscribeToButton(button);
if (button.userData == null) button.userData = new UserData();
(button.userData as UserData).AddCallback(button.text, clicked);
button.clicked += clicked;
}
private void UnsubscribeToButton(Button button)
{
if (button.userData == null) return;
(button.userData as UserData).RemoveCallback(button.text, button);
}
private void Save()
{
StateData stateData = new StateData();
for (int i = 0; i < pinnedList.Count; i++)
{
pinnedList[i].PinnedIndex = i;
}
stateData.pinnedList = pinnedList;
string json = JsonUtility.ToJson(stateData);
File.WriteAllText(jsonFilePath, json);
}
private T[] FindAssetsOfType<T>() where T : UnityEngine.Object
{
string[] guids = AssetDatabase.FindAssets($"t:{typeof(T).Name}");
List<T> list = new List<T>();
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
T asset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
if (asset != null)
{
list.Add(asset);
}
}
return list.ToArray();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment