Last active
September 27, 2023 19:50
-
-
Save sabresaurus/2ea2df6cb22ad878833d07f1aac2c695 to your computer and use it in GitHub Desktop.
Palette window with drag-drop boxes that you can drag common objects onto such as scenes, materials, prefabs to easily access them later (From SabreCSG originally)
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
// MIT License | |
// | |
// Copyright (c) 2021 Sabresaurus | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
using UnityEngine; | |
using UnityEditor; | |
using UnityEditor.SceneManagement; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Text; | |
using UnityEngine.SceneManagement; | |
namespace Sabresaurus.Core | |
{ | |
public class PaletteWindow : EditorWindow | |
{ | |
protected class Tab | |
{ | |
public string Name = ""; | |
public List<(GlobalObjectId key, Object resolvedValue)> TrackedObjects = new List<(GlobalObjectId, Object)>(); | |
} | |
enum DropInType | |
{ | |
Replace, | |
InsertAfter | |
}; | |
const int SPACING = 11; | |
Vector2 scrollPosition = Vector2.zero; | |
int width = 100; // Width of a palette cell | |
bool editingTabName = false; | |
int? hotControl = null; | |
List<Tab> tabs = new List<Tab>(); | |
int activeTab = 0; | |
int mouseDownIndex = -1; | |
int dropInTargetIndex = -1; // Current slot a DragDrop is being considered as a destination | |
DropInType dropInType = DropInType.Replace; | |
bool isDragging = false; // If a DragDrop from a slot is in progress | |
int deferredIndexToRemove = -1; | |
protected virtual string PlayerPrefKeyPrefix | |
{ | |
get { return "PaletteSelection"; } | |
} | |
protected virtual System.Type TypeFilter | |
{ | |
get { return typeof(Object); } | |
} | |
bool UseCells | |
{ | |
get { return width >= 50; } | |
} | |
[MenuItem("Window/Palette")] | |
static void CreateAndShow() | |
{ | |
EditorWindow window = GetWindow<PaletteWindow>("Palette"); | |
window.minSize = new Vector2(120, 120); | |
window.Show(); | |
} | |
void OnEnable() | |
{ | |
LoadAndRepaint(); | |
// Some palette window types respect undo, so make sure we can reload if needed | |
Undo.undoRedoPerformed += OnUndoRedoPerformed; | |
EditorSceneManager.sceneOpened += OnSceneOpened; | |
} | |
void OnDisable() | |
{ | |
for (int i = 0; i < 9; i++) | |
{ | |
Save(i); | |
} | |
Undo.undoRedoPerformed -= OnUndoRedoPerformed; | |
} | |
private void LoadAndRepaint() | |
{ | |
for (int i = 0; i < 9; i++) | |
{ | |
Load(i); | |
} | |
Repaint(); | |
} | |
protected virtual void OnUndoRedoPerformed() | |
{ | |
LoadAndRepaint(); | |
} | |
private void OnSceneOpened(Scene scene, OpenSceneMode mode) | |
{ | |
LoadAndRepaint(); | |
} | |
void OnGUI() | |
{ | |
Event e = Event.current; | |
int columnCount = (int) (Screen.width / EditorGUIUtility.pixelsPerPoint) / (width + SPACING); | |
var trackedObjects = tabs[activeTab].TrackedObjects; | |
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); | |
GUILayout.Space(8); | |
// Make sure there's an empty one at the end to drag into | |
if (trackedObjects.Count == 0 || trackedObjects[trackedObjects.Count - 1].key.identifierType != 0) | |
{ | |
trackedObjects.Add((new GlobalObjectId(), null)); | |
} | |
if (e.rawType == EventType.MouseUp || e.rawType == EventType.MouseMove || e.rawType == EventType.DragUpdated || e.rawType == EventType.ScrollWheel) | |
{ | |
dropInTargetIndex = -1; | |
dropInType = DropInType.Replace; | |
} | |
List<(GlobalObjectId, Object)> deferredInsertObjects = null; | |
int deferredInsertIndex = -1; | |
for (int i = 0; i < trackedObjects.Count; i++) | |
{ | |
int columnIndex = i % columnCount; | |
if (UseCells && columnIndex == 0) | |
{ | |
EditorGUILayout.BeginHorizontal(); | |
GUILayout.Space(8); | |
} | |
List<(GlobalObjectId key, Object resolvedValue)> newSelectedObjects = null; | |
bool dropAccepted = false; | |
DrawElement(e, trackedObjects[i], i, out newSelectedObjects, out dropAccepted); | |
if (dropAccepted || newSelectedObjects.Count > 1 || newSelectedObjects[0].resolvedValue != trackedObjects[i].resolvedValue) | |
{ | |
if (dropInType == DropInType.InsertAfter) | |
{ | |
// Defer the insert until after we've drawn the UI so we don't mismatch UI mid-draw | |
deferredInsertIndex = i + 1; | |
deferredInsertObjects = newSelectedObjects; | |
} | |
else | |
{ | |
trackedObjects[i] = newSelectedObjects[0]; | |
if (newSelectedObjects.Count > 1) | |
{ | |
deferredInsertIndex = i + 1; | |
newSelectedObjects.RemoveAt(0); | |
deferredInsertObjects = newSelectedObjects; | |
} | |
Save(activeTab); | |
} | |
} | |
GUILayout.Space(UseCells ? 4 : 8); | |
if (UseCells && (columnIndex == columnCount - 1 || i == trackedObjects.Count - 1)) // If last in row | |
{ | |
GUILayout.FlexibleSpace(); | |
EditorGUILayout.EndHorizontal(); | |
} | |
} | |
if (e.type == EventType.MouseDrag && !isDragging && mouseDownIndex != -1 && trackedObjects[mouseDownIndex].resolvedValue != null) | |
{ | |
isDragging = true; | |
DragAndDrop.PrepareStartDrag(); | |
DragAndDrop.objectReferences = new[] {trackedObjects[mouseDownIndex].resolvedValue}; | |
DragAndDrop.StartDrag(trackedObjects[mouseDownIndex].resolvedValue.name); | |
} | |
if (e.rawType == EventType.MouseUp || e.rawType == EventType.MouseMove || e.rawType == EventType.DragPerform || e.rawType == EventType.DragExited) | |
{ | |
isDragging = false; | |
mouseDownIndex = -1; | |
} | |
EditorGUILayout.EndScrollView(); | |
GUILayout.FlexibleSpace(); | |
GUIStyle boxStyle = new GUIStyle(GUI.skin.box); | |
boxStyle.margin = new RectOffset(0, 0, 0, 0); | |
RectOffset padding = boxStyle.padding; | |
padding.top += 1; | |
boxStyle.padding = padding; | |
GUILayout.Box(new GUIContent(), boxStyle, GUILayout.ExpandWidth(true)); | |
Rect lastRect = GUILayoutUtility.GetLastRect(); | |
Rect buttonRect = lastRect; | |
buttonRect.y += 1; | |
buttonRect.width = (buttonRect.width - 90) / 9f; | |
GUIStyle activeStyle = EditorStyles.toolbarButton; | |
buttonRect.height = activeStyle.CalcHeight(new GUIContent(), 20); | |
for (int i = 0; i < 9; i++) | |
{ | |
string tabName = (i + 1).ToString(); | |
if (!string.IsNullOrEmpty(tabs[i].Name)) | |
{ | |
tabName = tabs[i].Name; | |
} | |
bool oldValue = (activeTab == i); | |
if (oldValue == true && editingTabName) | |
{ | |
GUI.SetNextControlName("PaletteTabName"); | |
tabs[activeTab].Name = GUI.TextField(buttonRect, tabs[activeTab].Name); | |
if (!hotControl.HasValue) | |
{ | |
hotControl = GUIUtility.hotControl; | |
GUI.FocusControl("PaletteTabName"); | |
} | |
else | |
{ | |
if (GUIUtility.hotControl != hotControl.Value // Clicked off it | |
|| Event.current.type == EventType.KeyDown && Event.current.character == (char) 10) // Return pressed | |
{ | |
editingTabName = false; | |
hotControl = null; | |
Save(activeTab); | |
} | |
} | |
} | |
else | |
{ | |
bool newValue = GUI.Toggle(buttonRect, oldValue, tabName, activeStyle); | |
if (newValue != oldValue) | |
{ | |
if (newValue == true) | |
{ | |
activeTab = i; | |
Repaint(); | |
editingTabName = false; | |
} | |
else if (newValue == false) | |
{ | |
editingTabName = true; | |
hotControl = null; | |
} | |
} | |
} | |
buttonRect.x += buttonRect.width; | |
} | |
// Debug.Log(GUI.GetNameOfFocusedControl()); | |
// if(GUI.GetNameOfFocusedControl() != "PaletteTabName") | |
// { | |
// editingTabName = false; | |
// } | |
Rect sliderRect = lastRect; | |
sliderRect.xMax -= 10; | |
sliderRect.xMin = sliderRect.xMax - 60; | |
// User configurable tile size | |
width = (int) GUI.HorizontalSlider(sliderRect, width, 49, 100); | |
// Delete at the end of the OnGUI so we don't mismatch any UI groups | |
if (deferredIndexToRemove != -1) | |
{ | |
trackedObjects.RemoveAt(deferredIndexToRemove); | |
deferredIndexToRemove = -1; | |
Save(activeTab); | |
} | |
// Insert at the end of the OnGUI so we don't mismatch any UI groups | |
if (deferredInsertObjects != null) | |
{ | |
trackedObjects.InsertRange(deferredInsertIndex, deferredInsertObjects); | |
Save(activeTab); | |
} | |
// Carried out a DragPerform, so reset drop in states | |
if (e.rawType == EventType.DragPerform) | |
{ | |
dropInTargetIndex = -1; | |
dropInType = DropInType.Replace; | |
Repaint(); | |
} | |
} | |
public void DrawElement(Event e, (GlobalObjectId key, Object resolvedValue) selectedObject, int index, out List<(GlobalObjectId key, Object resolvedValue)> newSelection, out bool dropAccepted) | |
{ | |
newSelection = new List<(GlobalObjectId, Object)>(); | |
dropAccepted = false; | |
EditorGUILayout.BeginVertical(); | |
Texture2D previewTexture = null; | |
Object resolvedValue = selectedObject.resolvedValue; | |
if (resolvedValue != null) | |
{ | |
previewTexture = AssetPreview.GetAssetPreview(resolvedValue); | |
if (previewTexture == null) | |
{ | |
if (AssetPreview.IsLoadingAssetPreview(resolvedValue.GetInstanceID())) | |
{ | |
// Not loaded yet, so tell it to repaint | |
Repaint(); | |
} | |
else | |
{ | |
previewTexture = AssetPreview.GetMiniThumbnail(resolvedValue); | |
} | |
} | |
} | |
Rect previewRect; | |
Rect insertAfterRect; | |
if (UseCells) | |
{ | |
GUIStyle style = new GUIStyle(GUI.skin.box) | |
{ | |
normal = EditorStyles.wordWrappedLabel.normal, | |
alignment = TextAnchor.MiddleCenter | |
}; | |
GUIContent guiContent = new GUIContent(); | |
if (selectedObject.key.identifierType != 0) | |
{ | |
// Construct a tooltip that has more information about what is tracked | |
string trackedObjectType = selectedObject.key.identifierType == 2 ? "Scene Object" : "Asset"; | |
if (selectedObject.resolvedValue != null) | |
{ | |
guiContent.tooltip = $"{selectedObject.resolvedValue.name} ({selectedObject.resolvedValue.GetType().Name} {trackedObjectType})\n{selectedObject.key}"; | |
} | |
else | |
{ | |
guiContent.tooltip = $"Unresolved {trackedObjectType}\n{selectedObject.key}"; | |
} | |
if (selectedObject.key.identifierType == 2) | |
{ | |
string sceneAssetPath = AssetDatabase.GUIDToAssetPath(selectedObject.key.assetGUID.ToString()); | |
if (string.IsNullOrEmpty(sceneAssetPath)) | |
{ | |
guiContent.tooltip += "\nIn unresolved scene"; | |
} | |
else | |
{ | |
guiContent.tooltip += "\nIn scene " + sceneAssetPath; | |
} | |
} | |
} | |
if (previewTexture != null) | |
{ | |
guiContent.image = previewTexture; | |
style.padding = new RectOffset(0, 0, 0, 0); | |
} | |
else | |
{ | |
style.padding = new RectOffset(5, 5, 5, 5); | |
if (selectedObject.key.identifierType == 0) | |
{ | |
guiContent.text = "Drag an object here"; | |
} | |
else if (selectedObject.key.identifierType == 2) // Scene Object - from Docs is the identifier type represented by an integer (0 = Null, 1 = Imported Asset, 2 = Scene Object, 3 = Source Asset). | |
{ | |
var sceneName = Path.GetFileNameWithoutExtension(AssetDatabase.GUIDToAssetPath(selectedObject.key.assetGUID.ToString())); | |
if (string.IsNullOrEmpty(sceneName)) | |
guiContent.text = $"Scene (not resolvable) not loaded"; | |
else | |
guiContent.text = $"Scene {sceneName} not loaded"; | |
} | |
else | |
{ | |
guiContent.text = "Asset object not resolved"; | |
} | |
} | |
GUILayout.Box(guiContent, style, GUILayout.Width(width), GUILayout.Height(width)); | |
previewRect = GUILayoutUtility.GetLastRect(); | |
insertAfterRect = new Rect(previewRect.xMax, previewRect.y, 8, previewRect.height); | |
// Colour code the outline based on type - from Docs is the identifier type represented by an integer (0 = Null, 1 = Imported Asset, 2 = Scene Object, 3 = Source Asset). | |
if (selectedObject.key.identifierType == 2) // 2 = Scene Object | |
{ | |
DrawOutline(previewRect, 1, new Color(0f, 0.36f, 0f)); | |
} | |
else if (selectedObject.key.identifierType == 1 || selectedObject.key.identifierType == 3) // 1 = Imported Asset, 3 = Source Asset | |
{ | |
DrawOutline(previewRect, 1, new Color(0.21f, 0.26f, 0.36f)); | |
} | |
EditorGUI.BeginChangeCheck(); | |
resolvedValue = EditorGUILayout.ObjectField(resolvedValue, TypeFilter, false, GUILayout.Width(width)); | |
if (EditorGUI.EndChangeCheck()) | |
{ | |
selectedObject.key = GlobalObjectId.GetGlobalObjectIdSlow(resolvedValue); | |
} | |
} | |
else | |
{ | |
EditorGUI.BeginChangeCheck(); | |
resolvedValue = EditorGUILayout.ObjectField(resolvedValue, TypeFilter, false); | |
if (EditorGUI.EndChangeCheck()) | |
{ | |
selectedObject.key = GlobalObjectId.GetGlobalObjectIdSlow(resolvedValue); | |
} | |
previewRect = GUILayoutUtility.GetLastRect(); | |
insertAfterRect = new Rect(previewRect.xMin, previewRect.yMax, previewRect.width, 8); | |
} | |
if (dropInTargetIndex == index) | |
{ | |
if (dropInType == DropInType.InsertAfter) | |
{ | |
Rect insertAfterRectDisplay = insertAfterRect; | |
insertAfterRectDisplay.xMin += 2; | |
insertAfterRectDisplay.xMax -= 2; | |
DrawOutline(insertAfterRectDisplay, 2, Color.blue); | |
} | |
else | |
{ | |
DrawOutline(previewRect, 2, Color.blue); | |
} | |
} | |
bool mouseInRect = previewRect.Contains(e.mousePosition); | |
bool mouseInInsertAfterRect = insertAfterRect.Contains(e.mousePosition); | |
if (mouseInRect && e.type == EventType.MouseDown) | |
{ | |
mouseDownIndex = index; | |
} | |
if (mouseInRect && e.type == EventType.MouseUp && !isDragging) | |
{ | |
if (e.button == 0) | |
{ | |
Selection.activeObject = resolvedValue; | |
} | |
else | |
{ | |
if (resolvedValue == null) | |
{ | |
deferredIndexToRemove = index; | |
} | |
else | |
{ | |
selectedObject = (new GlobalObjectId(), null); | |
} | |
Repaint(); | |
} | |
} | |
if (mouseInRect && e.type == EventType.MouseDown && !isDragging) | |
{ | |
if (e.button == 0) | |
{ | |
if (e.clickCount == 2 && resolvedValue != null) | |
{ | |
AssetDatabase.OpenAsset(resolvedValue); | |
} | |
} | |
} | |
if (e.type == EventType.DragUpdated || e.type == EventType.DragPerform) | |
{ | |
foreach (Object objectReference in DragAndDrop.objectReferences) | |
{ | |
if (objectReference.GetType() == TypeFilter || objectReference.GetType().IsSubclassOf(TypeFilter)) | |
{ | |
if (mouseInRect || mouseInInsertAfterRect) | |
{ | |
DragAndDrop.visualMode = DragAndDropVisualMode.Copy; | |
if (e.type == EventType.DragPerform) | |
{ | |
DragAndDrop.AcceptDrag(); | |
newSelection.Add((GlobalObjectId.GetGlobalObjectIdSlow(objectReference), objectReference)); | |
dropAccepted = true; | |
} | |
else | |
{ | |
dropInTargetIndex = index; | |
dropInType = mouseInInsertAfterRect ? DropInType.InsertAfter : DropInType.Replace; | |
} | |
Repaint(); | |
} | |
} | |
} | |
} | |
EditorGUILayout.EndVertical(); | |
// We didn't change selection, so just fill it in with what is occupying the cell | |
if (newSelection.Count == 0) | |
{ | |
newSelection.Add(selectedObject); | |
} | |
} | |
void DrawOutline(Rect lastRect, int lineThickness, Color color) | |
{ | |
var oldColor = GUI.color; | |
GUI.color = color; | |
GUI.DrawTexture(new Rect(lastRect.xMin, lastRect.yMin, lineThickness, lastRect.height), EditorGUIUtility.whiteTexture); | |
GUI.DrawTexture(new Rect(lastRect.xMax - lineThickness, lastRect.yMin, lineThickness, lastRect.height), EditorGUIUtility.whiteTexture); | |
GUI.DrawTexture(new Rect(lastRect.xMin + lineThickness, lastRect.yMin, lastRect.width - lineThickness * 2, lineThickness), EditorGUIUtility.whiteTexture); | |
GUI.DrawTexture(new Rect(lastRect.xMin + lineThickness, lastRect.yMax - lineThickness, lastRect.width - lineThickness * 2, lineThickness), EditorGUIUtility.whiteTexture); | |
GUI.color = oldColor; // Reset GUI color | |
} | |
/// <summary> | |
/// By default use PlayerPrefs to persist palette objects, but allow derived classes to implement alternative | |
/// persistence methods | |
/// </summary> | |
protected virtual void SaveImplementation(int tabIndex, string tabName, string outputString) | |
{ | |
string key = PlayerPrefKeyPrefix; | |
if (tabIndex > 0) | |
{ | |
key += tabIndex; | |
} | |
PlayerPrefs.SetString(key, outputString); | |
PlayerPrefs.SetString(key + "-Tab", tabName); | |
PlayerPrefs.Save(); | |
} | |
/// <summary> | |
/// By default use PlayerPrefs to persist palette objects, but allow derived classes to implement alternative | |
/// persistence methods | |
/// </summary> | |
protected virtual void LoadImplementation(int tabIndex, out string trackedString, out string tabName) | |
{ | |
string key = PlayerPrefKeyPrefix; | |
if (tabIndex > 0) | |
{ | |
key += tabIndex; | |
} | |
trackedString = PlayerPrefs.GetString(key); | |
tabName = PlayerPrefs.GetString(key + "-Tab"); | |
} | |
void Save(int tabIndex) | |
{ | |
while (tabs.Count <= tabIndex) | |
{ | |
tabs.Add(new Tab()); | |
} | |
var trackedObjects = tabs[tabIndex].TrackedObjects; | |
StringBuilder output = new StringBuilder(); | |
for (var index = 0; index < trackedObjects.Count; index++) | |
{ | |
var trackedObject = trackedObjects[index]; | |
output.Append(trackedObject.key.ToString()); | |
if (index != trackedObjects.Count - 1) | |
{ | |
output.Append(","); | |
} | |
} | |
SaveImplementation(tabIndex, tabs[tabIndex].Name, output.ToString()); | |
} | |
void Load(int tabIndex) | |
{ | |
while (tabs.Count <= tabIndex) | |
{ | |
tabs.Add(new Tab()); | |
} | |
tabs[tabIndex].TrackedObjects.Clear(); | |
LoadImplementation(tabIndex, out var trackedString, out var tabName); | |
string[] trackedObjectStrings = trackedString.Split(','); | |
foreach (string trackedObjectString in trackedObjectStrings) | |
{ | |
if (GlobalObjectId.TryParse(trackedObjectString, out GlobalObjectId globalObjectId)) | |
{ | |
// Resolve | |
var resolvedObject = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(globalObjectId); | |
// Track | |
tabs[tabIndex].TrackedObjects.Add((globalObjectId, resolvedObject)); | |
} | |
} | |
tabs[tabIndex].Name = tabName; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment