Last active
September 30, 2024 11:37
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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