using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; namespace XNodeEditor { /// Contains reflection-related extensions built for xNode public static class NodeEditorReflection { [NonSerialized] private static Dictionary nodeTint; [NonSerialized] private static Dictionary nodeWidth; /// All available node types public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } [NonSerialized] private static Type[] _nodeTypes = null; /// Return a delegate used to determine whether window is docked or not. It is faster to cache this delegate than run the reflection required each time. public static Func GetIsDockedDelegate(this EditorWindow window) { BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); return (Func) Delegate.CreateDelegate(typeof(Func), window, isDockedMethod); } public static Type[] GetNodeTypes() { //Get all classes deriving from Node via reflection return GetDerivedTypes(typeof(XNode.Node)); } /// Custom node tint colors defined with [NodeColor(r, g, b)] public static bool TryGetAttributeTint(this Type nodeType, out Color tint) { if (nodeTint == null) { CacheAttributes(ref nodeTint, x => x.color); } return nodeTint.TryGetValue(nodeType, out tint); } /// Get custom node widths defined with [NodeWidth(width)] public static bool TryGetAttributeWidth(this Type nodeType, out int width) { if (nodeWidth == null) { CacheAttributes(ref nodeWidth, x => x.width); } return nodeWidth.TryGetValue(nodeType, out width); } private static void CacheAttributes(ref Dictionary dict, Func getter) where A : Attribute { dict = new Dictionary(); for (int i = 0; i < nodeTypes.Length; i++) { object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true); if (attribs == null || attribs.Length == 0) continue; A attrib = attribs[0] as A; dict.Add(nodeTypes[i], getter(attrib)); } } /// Get FieldInfo of a field, including those that are private and/or inherited public static FieldInfo GetFieldInfo(this Type type, string fieldName) { // If we can't find field in the first run, it's probably a private field in a base class. FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); // Search base classes for private fields only. Public fields are found above while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); return field; } /// Get all classes deriving from baseType via reflection public static Type[] GetDerivedTypes(this Type baseType) { List types = new List(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); } catch (ReflectionTypeLoadException) { } } return types.ToArray(); } /// Find methods marked with the [ContextMenu] attribute and add them to the context menu public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) { KeyValuePair[] items = GetContextMenuMethods(obj); if (items.Length != 0) { contextMenu.AddSeparator(""); List invalidatedEntries = new List(); foreach (KeyValuePair checkValidate in items) { if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) { invalidatedEntries.Add(checkValidate.Key.menuItem); } } for (int i = 0; i < items.Length; i++) { KeyValuePair kvp = items[i]; if (invalidatedEntries.Contains(kvp.Key.menuItem)) { contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem)); } else { contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); } } } } /// Call OnValidate on target public static void TriggerOnValidate(this UnityEngine.Object target) { System.Reflection.MethodInfo onValidate = null; if (target != null) { onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (onValidate != null) onValidate.Invoke(target, null); } } public static KeyValuePair[] GetContextMenuMethods(object obj) { Type type = obj.GetType(); MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); List> kvp = new List>(); for (int i = 0; i < methods.Length; i++) { ContextMenu[] attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray(); if (attribs == null || attribs.Length == 0) continue; if (methods[i].GetParameters().Length != 0) { Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands."); continue; } if (methods[i].IsStatic) { Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands."); continue; } for (int k = 0; k < attribs.Length; k++) { kvp.Add(new KeyValuePair(attribs[k], methods[i])); } } #if UNITY_5_5_OR_NEWER //Sort menu items kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority)); #endif return kvp.ToArray(); } /// Very crude. Uses a lot of reflection. public static void OpenPreferences() { try { #if UNITY_2018_3_OR_NEWER SettingsService.OpenUserPreferences("Preferences/Node Editor"); #else //Open preferences window Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow)); Type type = assembly.GetType("UnityEditor.PreferencesWindow"); type.GetMethod("ShowPreferencesWindow", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null); //Get the window EditorWindow window = EditorWindow.GetWindow(type); //Make sure custom sections are added (because waiting for it to happen automatically is too slow) FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance); if ((bool) refreshField.GetValue(window)) { type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null); refreshField.SetValue(window, false); } //Get sections FieldInfo sectionsField = type.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic); IList sections = sectionsField.GetValue(window) as IList; //Iterate through sections and check contents Type sectionType = sectionsField.FieldType.GetGenericArguments() [0]; FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public); for (int i = 0; i < sections.Count; i++) { GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent; if (sectionContent.text == "Node Editor") { //Found contents - Set index FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic); sectionIndexField.SetValue(window, i); return; } } #endif } catch (Exception e) { Debug.LogError(e); Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number."); } } } }