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) 2019 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. | |
#if UNITY_EDITOR | |
using UnityEngine; | |
using UnityEditor; | |
using UnityEditor.SceneManagement; | |
using System.Collections.Generic; | |
using System.Text; | |
using System.Reflection; | |
namespace Sabresaurus.SabreCSG | |
{ | |
public class PaletteWindow : EditorWindow | |
{ | |
[System.Serializable] | |
protected class Tab | |
{ | |
public string Name = ""; | |
public List<Object> trackedObjects = new List<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 = EditorWindow.GetWindow<PaletteWindow>("Palette");//false, "Palette", true); | |
window.minSize = new Vector2( 120, 120 ); | |
window.Show(); | |
} | |
void OnEnable() | |
{ | |
for (int i = 0; i < 9; i++) | |
{ | |
Load(i); | |
} | |
// Some palette window types respect undo, so make sure we can reload if needed | |
Undo.undoRedoPerformed += OnUndoRedoPerformed; | |
} | |
void OnDisable() | |
{ | |
for (int i = 0; i < 9; i++) | |
{ | |
Save(i); | |
} | |
Undo.undoRedoPerformed -= OnUndoRedoPerformed; | |
} | |
protected virtual void OnUndoRedoPerformed() | |
{ | |
for (int i = 0; i < 9; i++) | |
{ | |
Load(i); | |
} | |
Repaint(); | |
} | |
void OnGUI() | |
{ | |
Event e = Event.current; | |
#if UNITY_5_4_OR_NEWER | |
int columnCount = (int)(Screen.width / EditorGUIUtility.pixelsPerPoint) / (width + SPACING); | |
#else | |
int columnCount = Screen.width / (width + SPACING); | |
#endif | |
List<Object> selectedObjects = tabs[activeTab].trackedObjects; | |
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); | |
GUILayout.Space(8); | |
//GUILayout.Box(new GUIContent(), GUILayout.ExpandWidth(true)); | |
// Make sure there's an empty one at the end to drag into | |
if (selectedObjects.Count == 0 || selectedObjects[selectedObjects.Count-1] != null) | |
{ | |
selectedObjects.Add(null); | |
} | |
if (e.rawType == EventType.MouseUp || e.rawType == EventType.MouseMove || e.rawType == EventType.DragUpdated || e.rawType == EventType.ScrollWheel) | |
{ | |
dropInTargetIndex = -1; | |
dropInType = DropInType.Replace; | |
} | |
List<Object> deferredInsertObjects = null; | |
int deferredInsertIndex = -1; | |
for (int i = 0; i < selectedObjects.Count; i++) | |
{ | |
int columnIndex = i % columnCount; | |
if(UseCells && columnIndex == 0) | |
{ | |
EditorGUILayout.BeginHorizontal(); | |
GUILayout.Space(8); | |
} | |
List<Object> newSelectedObjects = null; | |
bool dropAccepted = false; | |
DrawElement(e, selectedObjects[i], i, out newSelectedObjects, out dropAccepted); | |
if(dropAccepted || newSelectedObjects.Count > 1 || newSelectedObjects[0] != selectedObjects[i]) | |
{ | |
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 | |
{ | |
selectedObjects[i] = newSelectedObjects[0]; | |
if(newSelectedObjects.Count > 1) | |
{ | |
deferredInsertIndex = i + 1; | |
newSelectedObjects.RemoveAt(0); | |
deferredInsertObjects = newSelectedObjects; | |
} | |
Save(activeTab); | |
} | |
} | |
if(UseCells) | |
{ | |
GUILayout.Space(4); | |
} | |
else | |
{ | |
GUILayout.Space(8); | |
} | |
if(UseCells && (columnIndex == columnCount-1 || i == selectedObjects.Count-1)) // If last in row | |
{ | |
GUILayout.FlexibleSpace(); | |
EditorGUILayout.EndHorizontal(); | |
} | |
} | |
if (e.type == EventType.MouseDrag && !isDragging && mouseDownIndex != -1 && selectedObjects[mouseDownIndex] != null) | |
{ | |
isDragging = true; | |
DragAndDrop.PrepareStartDrag(); | |
DragAndDrop.objectReferences = new Object[] { selectedObjects[mouseDownIndex] }; | |
// DragAndDrop.paths = new string[] { selectedObjects[mouseDownIndex] }; | |
DragAndDrop.StartDrag(selectedObjects[mouseDownIndex].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) | |
{ | |
selectedObjects.RemoveAt(deferredIndexToRemove); | |
deferredIndexToRemove = -1; | |
Save(activeTab); | |
} | |
// Insert at the end of the OnGUI so we don't mismatch any UI groups | |
if(deferredInsertObjects != null) | |
{ | |
selectedObjects.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, Object selectedObject, int index, out List<Object> newSelection, out bool dropAccepted) | |
{ | |
newSelection = new List<Object>(); | |
dropAccepted = false; | |
EditorGUILayout.BeginVertical(); | |
Texture2D texture = null; | |
if (selectedObject != null) | |
{ | |
texture = AssetPreview.GetAssetPreview(selectedObject); | |
if (texture == null) | |
{ | |
if (AssetPreview.IsLoadingAssetPreview(selectedObject.GetInstanceID())) | |
{ | |
// Not loaded yet, so tell it to repaint | |
Repaint(); | |
} | |
else | |
{ | |
//texture = AssetPreview.GetMiniTypeThumbnail(selectedObject.GetType()); | |
texture = AssetPreview.GetMiniThumbnail(selectedObject); | |
} | |
} | |
} | |
Rect previewRect; | |
Rect insertAfterRect; | |
if(UseCells) | |
{ | |
GUIStyle style = new GUIStyle(GUI.skin.box); | |
style.padding = new RectOffset(0, 0, 0, 0); | |
style.alignment = TextAnchor.MiddleCenter; | |
if(texture != null) | |
{ | |
GUILayout.Box(texture, style, GUILayout.Width(width), GUILayout.Height(width)); | |
} | |
else | |
{ | |
GUILayout.Box("Drag an object here", style, GUILayout.Width(width), GUILayout.Height(width)); | |
} | |
previewRect = GUILayoutUtility.GetLastRect(); | |
insertAfterRect = new Rect(previewRect.xMax, previewRect.y, 8, previewRect.height); | |
selectedObject = EditorGUILayout.ObjectField(selectedObject, TypeFilter, false, GUILayout.Width(width)); | |
} | |
else | |
{ | |
selectedObject = EditorGUILayout.ObjectField(selectedObject, TypeFilter, false); | |
previewRect = GUILayoutUtility.GetLastRect(); | |
insertAfterRect = new Rect(previewRect.xMin, previewRect.yMax, previewRect.width, 8); | |
} | |
if(dropInTargetIndex == index) | |
{ | |
if(dropInType == DropInType.InsertAfter) | |
{ | |
DrawOutline(insertAfterRect, 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) | |
{ | |
OnItemClick(selectedObject); | |
} | |
else | |
{ | |
if(selectedObject == null) | |
{ | |
deferredIndexToRemove = index; | |
} | |
else | |
{ | |
selectedObject = null; | |
} | |
Repaint(); | |
} | |
} | |
if (mouseInRect && e.type == EventType.MouseDown && !isDragging) | |
{ | |
if (e.button == 0) | |
{ | |
if (e.clickCount == 2 && selectedObject != null) | |
{ | |
if (selectedObject.GetType() == typeof(SceneAsset)) | |
{ | |
if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) | |
{ | |
EditorSceneManager.OpenScene(AssetDatabase.GetAssetPath(selectedObject), OpenSceneMode.Single); | |
} | |
} | |
else if (typeof(RuntimeAnimatorController).IsAssignableFrom(selectedObject.GetType())) | |
{ | |
SetAnimatorWindow(selectedObject); | |
} | |
else if(selectedObject.GetType() == typeof(DefaultAsset)) | |
{ | |
// Something without a strong Unity type, could be a folder | |
if(ProjectWindowUtil.IsFolder(selectedObject.GetInstanceID())) | |
{ | |
SetProjectWindowFolder(AssetDatabase.GetAssetPath(selectedObject)); | |
} | |
} | |
else | |
{ | |
AssetDatabase.OpenAsset(selectedObject); | |
} | |
} | |
} | |
} | |
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(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); | |
} | |
} | |
public static void SetProjectWindowFolder(string folderPath) | |
{ | |
// This code uses reflection to set the project window folder | |
// As a result the internal structure could change, so the entire thing is wrapped in a try-catch | |
try | |
{ | |
EditorApplication.ExecuteMenuItem("Window/Project"); | |
Assembly unityEditorAssembly = Assembly.GetAssembly(typeof(Editor)); | |
if (unityEditorAssembly != null) | |
{ | |
System.Type projectWindowUtilType = unityEditorAssembly.GetType("UnityEditor.ProjectWindowUtil"); | |
MethodInfo getBrowserMethodInfo = projectWindowUtilType.GetMethod("GetProjectBrowserIfExists", BindingFlags.Static | BindingFlags.NonPublic); | |
// Get any active project browser (Project window) | |
object projectBrowser = getBrowserMethodInfo.Invoke(null, new object[] { }); | |
// If there is an active project browser | |
if (projectBrowser != null) | |
{ | |
System.Type projectBrowserType = unityEditorAssembly.GetType("UnityEditor.ProjectBrowser"); | |
System.Type searchFilterType = unityEditorAssembly.GetType("UnityEditor.SearchFilter"); | |
// Create a SearchFilter instance which we will populate then pass to the ProjectBrowser | |
object searchFilterInstance = System.Activator.CreateInstance(searchFilterType); | |
PropertyInfo foldersProperty = searchFilterType.GetProperty("folders"); | |
// Set the active folder on the new SearchFilter | |
foldersProperty.GetSetMethod().Invoke(searchFilterInstance, new object[] { new string[] { folderPath } }); | |
// Supply the new SearchFilter to the project browser so it shows the new folder | |
MethodInfo setSearchMethodInfo = projectBrowserType.GetMethod("SetSearch", BindingFlags.Instance | BindingFlags.Public, null, new System.Type[] { searchFilterType }, null); | |
setSearchMethodInfo.Invoke(projectBrowser, new object[] { searchFilterInstance }); | |
// Folder selection has changed, make sure the tree and top bar properly update | |
MethodInfo selectionChangedMethodInfo = projectBrowserType.GetMethod("FolderTreeSelectionChanged", BindingFlags.Instance | BindingFlags.NonPublic); | |
selectionChangedMethodInfo.Invoke(projectBrowser, new object[] { true }); | |
} | |
} | |
} | |
catch | |
{ | |
// Failed, suppress any errors and just do nothing | |
} | |
} | |
public static void SetAnimatorWindow(Object selectedObject) | |
{ | |
EditorApplication.ExecuteMenuItem("Window/Animator"); | |
} | |
void DrawOutline(Rect lastRect, int lineThickness, Color 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 = Color.white; // Reset GUI color | |
} | |
protected virtual void OnItemClick(Object selectedObject) | |
{ | |
Selection.activeObject = selectedObject; | |
} | |
/// <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()); | |
} | |
List<Object> selectedObjects = tabs[tabIndex].trackedObjects; | |
StringBuilder output = new StringBuilder(); | |
for (int i = 0; i < selectedObjects.Count; i++) | |
{ | |
if(selectedObjects[i] != null) | |
{ | |
string assetPath = AssetDatabase.GetAssetPath(selectedObjects[i]); | |
string guid = AssetDatabase.AssetPathToGUID(assetPath); | |
// Save both the GUID and the path in case the GUID changes | |
output.Append(guid); | |
output.Append(":"); | |
output.Append(assetPath); | |
output.Append(","); | |
} | |
} | |
string outputString = output.ToString().TrimEnd(','); | |
SaveImplementation(tabIndex, tabs[tabIndex].Name, outputString); | |
} | |
void Load(int tabIndex) | |
{ | |
while(tabs.Count <= tabIndex) | |
{ | |
tabs.Add(new Tab()); | |
} | |
tabs[tabIndex].trackedObjects.Clear(); | |
string trackedString, tabName; | |
LoadImplementation(tabIndex, out trackedString, out tabName); | |
string[] trackedObjectStrings = trackedString.Split(','); | |
foreach (string trackedObjectString in trackedObjectStrings) | |
{ | |
// Get the guid, for older versions this is the only thing in the string, for newer versions it's the | |
// left side of a colon. Either way Split()[0] works | |
string guid = trackedObjectString.Split(':')[0]; | |
// Even when we have a path tracked, construct it fresh from the GUID in case the file has moved and | |
// there's a new one with the old path. We should always try to track on the GUID since that's what | |
// Unity does | |
string path = AssetDatabase.GUIDToAssetPath(guid); | |
Object mainAsset = AssetDatabase.LoadMainAssetAtPath(path); | |
if (mainAsset == null && trackedObjectString.Contains(":")) | |
{ | |
// Couldn't find an asset for that GUID, try the path we saved | |
path = trackedObjectString.Split(':')[1]; | |
mainAsset = AssetDatabase.LoadMainAssetAtPath(path); | |
} | |
if(mainAsset != null) | |
{ | |
tabs[tabIndex].trackedObjects.Add(mainAsset); | |
} | |
} | |
tabs[tabIndex].Name = tabName; | |
} | |
} | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment