Instantly share code, notes, and snippets.
Last active
April 11, 2025 05:59
-
Star
5
(5)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save JohannesMP/342beca2c0bad0b27dba974a2601b5db to your computer and use it in GitHub Desktop.
Unity Editor Context Menu Extensions
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
using UnityEngine; | |
using UnityEditor; | |
namespace UnityEditorExtensions | |
{ | |
public static class EditorContextMenuCopySerialized | |
{ | |
static SerializedObject sourceValues; | |
static System.Type sourceType; | |
[MenuItem("CONTEXT/Component/Copy Serialized Values", priority = 100)] | |
public static void CopySerialized(MenuCommand command) | |
{ | |
sourceType = command.context.GetType(); | |
sourceValues = new SerializedObject(command.context); | |
} | |
[MenuItem("CONTEXT/Component/Paste Serialized Values", isValidateFunction: true)] | |
public static bool CanPasteSerializedSameClass(MenuCommand command) | |
{ | |
if (sourceValues == null || sourceType == null) | |
{ | |
return false; | |
} | |
return sourceType.IsAssignableFrom(command.context.GetType()); | |
} | |
[MenuItem("CONTEXT/Component/Paste Serialized Values", priority = 100)] | |
public static void PasteSerializedSameClass(MenuCommand command) | |
{ | |
if (sourceValues.targetObject.GetType() == command.context.GetType()) | |
{ | |
EditorUtility.CopySerialized(sourceValues.targetObject, command.context); | |
return; | |
} | |
SerializedObject dest = new SerializedObject(command.context); | |
SerializedProperty prop_iterator = sourceValues.GetIterator(); | |
//jump into serialized object, this will skip script type so that we dont override the destination component's type | |
if (prop_iterator.NextVisible(true)) | |
{ | |
while (prop_iterator.NextVisible(true)) //itterate through all serializedProperties | |
{ | |
//try obtaining the property in destination component | |
SerializedProperty prop_element = dest.FindProperty(prop_iterator.name); | |
//validate that the properties are present in both components, and that they're the same type | |
if (prop_element != null && prop_element.propertyType == prop_iterator.propertyType) | |
{ | |
//copy value from source to destination component | |
dest.CopyFromSerializedProperty(prop_iterator); | |
} | |
} | |
} | |
dest.ApplyModifiedProperties(); | |
} | |
} | |
} |
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
using System.Linq; | |
using System.Reflection; | |
using UnityEditor; | |
using UnityEngine; | |
namespace UnityEditorExtensions | |
{ | |
public static class EditorContextMenuReplaceComponent | |
{ | |
[MenuItem("CONTEXT/Component/Replace Component...", isValidateFunction: true)] | |
public static bool CanReplaceWithDerived(MenuCommand command) | |
{ | |
// We can't replace 'Transform' for example | |
return command.context is Behaviour; | |
} | |
[MenuItem("CONTEXT/Component/Replace Component...", priority = 10)] | |
public static void ReplaceWithDerived(MenuCommand command) | |
{ | |
Behaviour toReplace = command.context as Behaviour; | |
ReplaceComponentWindow.Open(toReplace); | |
} | |
private class ReplaceComponentWindow : EditorWindow | |
{ | |
private static bool ShowNamespacesInSelection = true; | |
private System.Type targetType; | |
private Component targetComponent; | |
private GameObject targetObject; | |
private string targetObjectName; | |
private System.Type[] availableTypes; | |
// indeces of types sorted by short name. | |
private int[] availableTypesByShortName; | |
// indeces of types sorted by full name | |
private int[] availableTypesByFullName; | |
public static void Open(UnityEngine.Component toReplace) | |
{ | |
var window = EditorWindow.GetWindow<ReplaceComponentWindow>(true, "Create a new ScriptableObject", true); | |
window.SetTargetComponent(toReplace); | |
if (window.availableTypes == null || window.availableTypes.Length == 0) | |
{ | |
const string noDerivedFormat = ("Unable to replace Component '{0}' on GameOject '{1}'\n\nComponent Type '{2}' has no Usable derived class"); | |
string noDerivedString = string.Format(noDerivedFormat, window.targetType.Name, window.targetObjectName, window.targetType.FullName); | |
window.Close(); | |
EditorUtility.DisplayDialog("Unable to replace Component", noDerivedString, "Understood"); | |
} | |
else | |
{ | |
window.ShowPopup(); | |
} | |
} | |
public void SetTargetComponent(Component newTargetComponent) | |
{ | |
if (newTargetComponent != null) | |
{ | |
targetComponent = newTargetComponent; | |
targetType = newTargetComponent.GetType(); | |
targetObject = newTargetComponent.gameObject; | |
targetObjectName = targetObject.name; | |
RebuildTypeList(targetComponent.GetType()); | |
} | |
} | |
private void RebuildTypeList(System.Type targetType) | |
{ | |
var assembly = GetAssembly(); | |
// Get all classes derived from ScriptableObject | |
availableTypes = (from t in assembly.GetTypes() | |
where ( | |
t.IsSubclassOf(targetType) && | |
!t.IsGenericTypeDefinition // can't instantiate generics | |
) | |
select t).ToArray(); | |
RebuildSortIndeces(); | |
} | |
private void RebuildSortIndeces() | |
{ | |
// Initialize arrays | |
availableTypesByShortName = new int[availableTypes.Length]; | |
availableTypesByFullName = new int[availableTypes.Length]; | |
for (int i = 0; i < availableTypes.Length; ++i) | |
{ | |
availableTypesByShortName[i] = i; | |
availableTypesByFullName[i] = i; | |
} | |
// Set up short name indexes | |
System.Array.Sort(availableTypesByShortName, | |
(int lhs, int rhs) => { return availableTypes[lhs].Name.CompareTo(availableTypes[rhs].Name); } | |
); | |
// Set up full name indexes | |
System.Array.Sort(availableTypesByFullName, | |
(int lhs, int rhs) => { return availableTypes[lhs].FullName.CompareTo(availableTypes[rhs].FullName); } | |
); | |
} | |
//----------------------------------------------------------------------- | |
// GUI-related | |
static GUIStyle headerStyle; | |
static GUIStyle selectableStyle; | |
static GUIStyle submitStyle; | |
static bool guiStylesInit = false; | |
static void InitGUIStyles() | |
{ | |
if (guiStylesInit) | |
{ | |
return; | |
} | |
guiStylesInit = true; | |
headerStyle = new GUIStyle(GUI.skin.label); | |
headerStyle.fontSize = 17; | |
selectableStyle = new GUIStyle(GUI.skin.textField); | |
selectableStyle.wordWrap = true; | |
selectableStyle.clipping = TextClipping.Clip; | |
submitStyle = new GUIStyle(GUI.skin.button); | |
submitStyle.fontSize = 16; | |
} | |
private int selectedIndex = -1; | |
private Vector2 scrollPos; | |
private bool showScrollbar = true; | |
private string filterText = ""; | |
public void OnGUI() | |
{ | |
// Ensure we have all styles (this can only be done in OnGUI()... grumble...) | |
InitGUIStyles(); | |
// If open during recompile just close | |
if (targetType == null) | |
{ | |
Close(); | |
return; | |
} | |
// Handle failures | |
string failText = ""; | |
if (targetObject == null) | |
{ | |
const string objectRemovedFormat = "The GameObject \"{0}\" we wanted to replace a component on has been removed. Replacement Aborted"; | |
failText = string.Format(objectRemovedFormat, targetObjectName); | |
} | |
if (targetComponent == null) | |
{ | |
const string targetRemovedFormat = "The {0} Component of type we wanted to replace on GameObject \"{1}\" has been removed. Replacement Aborted"; | |
failText = string.Format(targetRemovedFormat, targetType.Name, targetObjectName); | |
} | |
if (!string.IsNullOrEmpty(failText)) | |
{ | |
AddActionOnEditorUpdate( | |
() => EditorUtility.DisplayDialog("Component replacement failed", failText, "Understood") | |
); | |
Close(); | |
return; | |
} | |
// Header | |
const string headerFormat = "Select Replacement Type for {0} on {1})"; | |
string headerText = string.Format(headerFormat, targetType.Name, targetObjectName); | |
GUILayout.Label(headerText, headerStyle); | |
// Settings | |
GUILayout.BeginHorizontal(); | |
{ | |
float width = EditorGUIUtility.labelWidth; | |
EditorGUIUtility.labelWidth = 40; | |
// Filter | |
filterText = EditorGUILayout.TextField("Filter:", filterText); | |
EditorGUIUtility.labelWidth = width; | |
// Namespace | |
ShowNamespacesInSelection = GUILayout.Toggle(ShowNamespacesInSelection, "Namespace", GUILayout.ExpandWidth(false)); | |
} | |
GUILayout.EndHorizontal(); | |
// To track if no results exist | |
int elementsShown = 0; | |
// To track if our selection was shown | |
bool sawSelection = false; | |
// Storing how big our scroll content was, so we know if we need to show scrollbars | |
Rect contentRect; | |
scrollPos = GUILayout.BeginScrollView(scrollPos, false, showScrollbar); | |
{ | |
GUILayout.BeginVertical(); | |
// Only bother showing buttons if we have types to show | |
if (availableTypes.Length > 0) | |
{ | |
// how the data is sorted | |
int[] curSort = ShowNamespacesInSelection ? availableTypesByFullName : availableTypesByShortName; | |
// Back up color | |
var color = GUI.color; | |
for (int i = 0; i < availableTypes.Length; ++i) | |
{ | |
// Hold onto the index that this element corresopnds to in the types array | |
int curIndex = curSort[i]; | |
// Update selected buttons | |
GUI.color = curIndex == selectedIndex ? Color.green : Color.white; | |
// Figure out what name to display | |
string name = ShowNamespacesInSelection ? availableTypes[curIndex].FullName : availableTypes[curIndex].Name; | |
bool MatchesFilter = availableTypes[curIndex].FullName.ToLower().Contains(filterText.ToLower()); | |
// Choose to show the element or not | |
if (filterText.Length == 0 || MatchesFilter) | |
{ | |
++elementsShown; | |
if (GUILayout.Button(name, selectableStyle)) | |
{ | |
selectedIndex = curIndex; | |
} | |
if (selectedIndex == curIndex) | |
{ | |
sawSelection = true; | |
} | |
} | |
} | |
// Reset the color | |
GUI.color = color; | |
} | |
// No elements were shown | |
if (elementsShown == 0) | |
{ | |
const string noResultFormat = "No Types found that derive from type {0}"; | |
string noResultText = string.Format(noResultFormat, targetType); | |
GUILayout.Label(noResultText); | |
} | |
GUILayout.EndVertical(); | |
contentRect = GUILayoutUtility.GetLastRect(); | |
} | |
GUILayout.EndScrollView(); | |
showScrollbar = contentRect.height > GUILayoutUtility.GetLastRect().height; | |
// Disable the button if there is nothing to create | |
EditorGUI.BeginDisabledGroup(elementsShown == 0 || selectedIndex < 0 || !sawSelection); | |
if (GUILayout.Button("Replace")) | |
{ | |
EditorReplaceComponent(targetComponent, availableTypes[selectedIndex]); | |
Close(); | |
} | |
EditorGUI.EndDisabledGroup(); | |
// Ensure that the scroll bar width is drawn correctly | |
Repaint(); | |
} | |
//----------------------------------------------------------------------- | |
// Static Utilities | |
// Returns the assembly that contains the script code for this project (currently hard coded) | |
private static Assembly GetAssembly() | |
{ | |
return Assembly.Load(new AssemblyName("Assembly-CSharp")); | |
} | |
private static void EditorReplaceComponent(Component toReplace, System.Type replaceWith) | |
{ | |
string targetObjectName = toReplace.name; | |
string targetTypeName = toReplace.GetType().Name; | |
try | |
{ | |
SerializedObject sourceValues = new SerializedObject(toReplace); | |
GameObject targetObject = toReplace.gameObject; | |
DestroyImmediate(toReplace); | |
AddActionOnEditorUpdate(() => | |
{ | |
targetObject.AddComponent(replaceWith); | |
AddActionOnEditorUpdate(() => | |
{ | |
UnityEngine.Object newComponent = targetObject.GetComponent(replaceWith); | |
CopySerializedValues(sourceValues, new UnityEditor.SerializedObject(newComponent)); | |
}); | |
}); | |
} | |
catch (System.Exception e) | |
{ | |
const string errorFormat = "Error while attempting to replace {0} Behavior on Object {1} with new {2}: {3}"; | |
string errorMsg = string.Format(errorFormat, targetTypeName, targetObjectName, replaceWith.Name); | |
Debug.LogErrorFormat("{0}: {1}", errorMsg, e.Message); | |
} | |
} | |
private static void CopySerializedValues(SerializedObject from, SerializedObject to) | |
{ | |
SerializedProperty prop_iterator = from.GetIterator(); | |
//jump into serialized object, this will skip script type so that we dont override the destination component's type | |
if (prop_iterator.NextVisible(true)) | |
{ | |
while (prop_iterator.NextVisible(true)) //itterate through all serializedProperties | |
{ | |
//try obtaining the property in destination component | |
SerializedProperty prop_element = to.FindProperty(prop_iterator.name); | |
//validate that the properties are present in both components, and that they're the same type | |
if (prop_element != null && prop_element.propertyType == prop_iterator.propertyType) | |
{ | |
//copy value from source to destination component | |
to.CopyFromSerializedProperty(prop_iterator); | |
} | |
} | |
} | |
to.ApplyModifiedProperties(); | |
} | |
private static void AddActionOnEditorUpdate(System.Action action) | |
{ | |
UnityEditor.EditorApplication.CallbackFunction OnEditorUpdate = null; | |
OnEditorUpdate = | |
() => | |
{ | |
UnityEditor.EditorApplication.update -= OnEditorUpdate; | |
try | |
{ | |
action.Invoke(); | |
} | |
catch (System.Exception e) | |
{ | |
Debug.LogErrorFormat("Exception on Queued Editor Update Action: {0}", e.Message); | |
} | |
}; | |
UnityEditor.EditorApplication.update += OnEditorUpdate; | |
} | |
} | |
} | |
} |
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
using UnityEditor; | |
using UnityEngine; | |
namespace UnityEditorExtensions | |
{ | |
public class EditorContextMenuReplaceWithTMProText | |
{ | |
[MenuItem("CONTEXT/Text/Replace with TMPro UI Text", priority = 10)] | |
public static void ReplaceWithTextMeshPro(MenuCommand command) | |
{ | |
UnityEngine.UI.Text textOld = command.context as UnityEngine.UI.Text; | |
if(textOld == null) | |
{ | |
return; | |
} | |
SerializedObject oldTransformValues = new SerializedObject(textOld.transform); | |
SerializedObject oldTextValues = new SerializedObject(textOld); | |
bool isRichText = textOld.supportRichText; | |
string text = textOld.text; | |
Color textColor = textOld.color; | |
int fontSize = textOld.fontSize; | |
float lineSpacing = textOld.lineSpacing; | |
TMPro.FontStyles newStyle = TMPro.FontStyles.Normal; | |
switch (textOld.fontStyle) | |
{ | |
default: | |
case FontStyle.Normal: newStyle = TMPro.FontStyles.Normal; break; | |
case FontStyle.Bold: newStyle = TMPro.FontStyles.Bold; break; | |
case FontStyle.Italic: newStyle = TMPro.FontStyles.Italic; break; | |
case FontStyle.BoldAndItalic: newStyle = TMPro.FontStyles.Bold | TMPro.FontStyles.Italic; break; | |
} | |
TMPro.TextAlignmentOptions newAlignment = TMPro.TextAlignmentOptions.TopLeft; | |
switch (textOld.alignment) | |
{ | |
default: | |
case TextAnchor.UpperLeft: newAlignment = TMPro.TextAlignmentOptions.TopLeft; break; | |
case TextAnchor.UpperCenter: newAlignment = TMPro.TextAlignmentOptions.Top; break; | |
case TextAnchor.UpperRight: newAlignment = TMPro.TextAlignmentOptions.TopRight; break; | |
case TextAnchor.MiddleLeft: newAlignment = TMPro.TextAlignmentOptions.Left; break; | |
case TextAnchor.MiddleCenter: newAlignment = TMPro.TextAlignmentOptions.Center; break; | |
case TextAnchor.MiddleRight: newAlignment = TMPro.TextAlignmentOptions.Right; break; | |
case TextAnchor.LowerLeft: newAlignment = TMPro.TextAlignmentOptions.BottomLeft; break; | |
case TextAnchor.LowerCenter: newAlignment = TMPro.TextAlignmentOptions.Bottom; break; | |
case TextAnchor.LowerRight: newAlignment = TMPro.TextAlignmentOptions.BottomRight; break; | |
} | |
bool isWordWrap = textOld.horizontalOverflow == HorizontalWrapMode.Wrap; | |
AddActionOnEditorUpdate(() => | |
{ | |
// Destroy the Existing Text Object | |
GameObject targetObject = textOld.gameObject; | |
try { Object.DestroyImmediate(textOld); } | |
catch (System.Exception e) | |
{ | |
const string errorFormat = "Unable To Destroy old Text Component before replacement: {0}"; | |
Debug.LogErrorFormat(errorFormat, e.Message); | |
return; | |
} | |
// Double-check that Unity has nulled out the reference | |
if (textOld == null) | |
{ | |
// If that suceeded, add the new Text mesh pro component | |
AddActionOnEditorUpdate(() => | |
{ | |
TMPro.TMP_Text textNew = null; | |
try | |
{ | |
textNew = targetObject.AddComponent<TMPro.TextMeshProUGUI>(); | |
if(textNew == null) | |
{ | |
throw new System.Exception("Unable to add Component"); | |
} | |
} | |
catch (System.Exception e) | |
{ | |
const string errorFormat = "Exception during Add: {0}. Reverting..."; | |
Debug.LogErrorFormat(errorFormat, e.Message); | |
UnityEngine.UI.Text recoveredText = targetObject.AddComponent<UnityEngine.UI.Text>(); | |
CopySerializedValues(oldTextValues, new SerializedObject(recoveredText)); | |
return; | |
} | |
textNew.richText = isRichText; | |
textNew.fontSize = fontSize; | |
textNew.lineSpacing = lineSpacing; | |
textNew.fontStyle = newStyle; | |
textNew.alignment = newAlignment; | |
textNew.enableWordWrapping = isWordWrap; | |
textNew.color = textColor; | |
textNew.text = text; | |
UnityEditor.EditorUtility.SetDirty(textNew); | |
AddActionOnEditorUpdate(() => | |
{ | |
// Fix the RecTransform positioning issues that TMP adds :/ | |
CopySerializedValues(oldTransformValues, new SerializedObject(textNew.transform)); | |
}); | |
}); | |
} | |
}); | |
} | |
private static void CopySerializedValues(SerializedObject from, SerializedObject to) | |
{ | |
SerializedProperty prop_iterator = from.GetIterator(); | |
//jump into serialized object, this will skip script type so that we dont override the destination component's type | |
if (prop_iterator.NextVisible(true)) | |
{ | |
while (prop_iterator.NextVisible(true)) //itterate through all serializedProperties | |
{ | |
//try obtaining the property in destination component | |
SerializedProperty prop_element = to.FindProperty(prop_iterator.name); | |
//validate that the properties are present in both components, and that they're the same type | |
if (prop_element != null && prop_element.propertyType == prop_iterator.propertyType) | |
{ | |
//copy value from source to destination component | |
to.CopyFromSerializedProperty(prop_iterator); | |
} | |
} | |
} | |
to.ApplyModifiedProperties(); | |
} | |
private static void AddActionOnEditorUpdate(System.Action action) | |
{ | |
UnityEditor.EditorApplication.CallbackFunction OnEditorUpdate = null; | |
OnEditorUpdate = | |
() => | |
{ | |
UnityEditor.EditorApplication.update -= OnEditorUpdate; | |
try | |
{ | |
action.Invoke(); | |
} | |
catch(System.Exception e) | |
{ | |
Debug.LogErrorFormat("Exception on Queued Editor Update Action: {0}", e.Message); | |
} | |
}; | |
UnityEditor.EditorApplication.update += OnEditorUpdate; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment