Skip to content

Instantly share code, notes, and snippets.

@JohannesMP
Last active July 17, 2023 11:10
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JohannesMP/342beca2c0bad0b27dba974a2601b5db to your computer and use it in GitHub Desktop.
Save JohannesMP/342beca2c0bad0b27dba974a2601b5db to your computer and use it in GitHub Desktop.
Unity Editor Context Menu Extensions
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();
}
}
}
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;
}
}
}
}
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