diff --git a/Editor/SerializeReferenceSelector.meta b/Editor/SerializeReferenceSelector.meta new file mode 100644 index 0000000..e1def37 --- /dev/null +++ b/Editor/SerializeReferenceSelector.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 070a846b9b875f042b9b4ac4ac4e4cc5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SerializeReferenceSelector/AdvancedTypePopup.cs b/Editor/SerializeReferenceSelector/AdvancedTypePopup.cs new file mode 100644 index 0000000..c509058 --- /dev/null +++ b/Editor/SerializeReferenceSelector/AdvancedTypePopup.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.IMGUI.Controls; + +namespace ubco.ovilab.HPUI.Editor +{ + /// + /// Items used in + /// + public class AdvancedTypePopupItem : AdvancedDropdownItem + { + /// + /// The representing the item. + /// + public Type Type { get; } + + public AdvancedTypePopupItem (Type type,string name) : base(name) + { + Type = type; + } + } + + /// + /// A type popup with a fuzzy finder. + /// + /// This is taken from https://github.com/mackysoft/Unity-SerializeReferenceExtensions + public class AdvancedTypePopup : AdvancedDropdown + { + private string emptyDisplayName; + private Type[] types; + + public event Action OnItemSelected; + + public AdvancedTypePopup (IEnumerable types, int maxLineCount, string emptyDisplayName, AdvancedDropdownState state) : base(state) + { + this.emptyDisplayName = emptyDisplayName; + this.types = types.ToArray(); + minimumSize = new Vector2(minimumSize.x,EditorGUIUtility.singleLineHeight * maxLineCount + EditorGUIUtility.singleLineHeight * 2f); + } + + /// + protected override AdvancedDropdownItem BuildRoot () + { + AdvancedDropdownItem root = new AdvancedDropdownItem("Select Type"); + int itemCount = 0; + + // Add null item. + AdvancedTypePopupItem nullItem = new AdvancedTypePopupItem(null, emptyDisplayName) + { + id = itemCount++ + }; + + root.AddChild(nullItem); + + // Add type items. + foreach (Type type in types) + { + AdvancedDropdownItem parent = root; + + string typeDisplayName = ObjectNames.NicifyVariableName(type.Name); + if (!string.IsNullOrEmpty(type.Namespace)) + { + typeDisplayName += $" ({type.Namespace})"; + } + + // Add type item. + AdvancedTypePopupItem item = new AdvancedTypePopupItem(type, typeDisplayName) + { + id = itemCount++ + }; + parent.AddChild(item); + } + return root; + } + + /// + protected override void ItemSelected (AdvancedDropdownItem item) + { + base.ItemSelected(item); + if (item is AdvancedTypePopupItem typePopupItem) + { + OnItemSelected?.Invoke(typePopupItem); + } + } + } +} diff --git a/Editor/SerializeReferenceSelector/AdvancedTypePopup.cs.meta b/Editor/SerializeReferenceSelector/AdvancedTypePopup.cs.meta new file mode 100644 index 0000000..e55df1e --- /dev/null +++ b/Editor/SerializeReferenceSelector/AdvancedTypePopup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9be93f9edc00426478e676044a88b8f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SerializeReferenceSelector/ManagedReferenceContextualPropertyMenu.cs b/Editor/SerializeReferenceSelector/ManagedReferenceContextualPropertyMenu.cs new file mode 100644 index 0000000..dc04ccf --- /dev/null +++ b/Editor/SerializeReferenceSelector/ManagedReferenceContextualPropertyMenu.cs @@ -0,0 +1,103 @@ +using System; +using UnityEditor; +using UnityEngine; + +namespace ubco.ovilab.HPUI.Editor +{ + public static class ManagedReferenceContextualPropertyMenu + { + const string kCopiedPropertyPathKey = "SerializeReferenceExtensions.CopiedPropertyPath"; + const string kClipboardKey = "SerializeReferenceExtensions.CopyAndPasteProperty"; + + static readonly GUIContent kPasteContent = new GUIContent("Paste Property"); + static readonly GUIContent kNewInstanceContent = new GUIContent("New Instance"); + static readonly GUIContent kResetAndNewInstanceContent = new GUIContent("Reset and New Instance"); + + [InitializeOnLoadMethod] + static void Initialize () + { + EditorApplication.contextualPropertyMenu += OnContextualPropertyMenu; + } + + static void OnContextualPropertyMenu (GenericMenu menu, SerializedProperty property) + { + if (property.propertyType == SerializedPropertyType.ManagedReference) + { + // NOTE: When the callback function is called, the SerializedProperty is rewritten to the property that was being moused over at the time, + // so a new SerializedProperty instance must be created. + SerializedProperty clonedProperty = property.Copy(); + + menu.AddItem(new GUIContent($"Copy \"{property.propertyPath}\" property"), false, Copy, clonedProperty); + + string copiedPropertyPath = SessionState.GetString(kCopiedPropertyPathKey, string.Empty); + if (!string.IsNullOrEmpty(copiedPropertyPath)) + { + menu.AddItem(new GUIContent($"Paste \"{copiedPropertyPath}\" property"), false, Paste, clonedProperty); + } + else + { + menu.AddDisabledItem(kPasteContent); + } + + menu.AddSeparator(""); + + bool hasInstance = clonedProperty.managedReferenceValue != null; + if (hasInstance) + { + menu.AddItem(kNewInstanceContent, false, NewInstance, clonedProperty); + menu.AddItem(kResetAndNewInstanceContent, false, ResetAndNewInstance, clonedProperty); + } + else + { + menu.AddDisabledItem(kNewInstanceContent); + menu.AddDisabledItem(kResetAndNewInstanceContent); + } + } + } + + static void Copy (object customData) + { + SerializedProperty property = (SerializedProperty)customData; + string json = JsonUtility.ToJson(property.managedReferenceValue); + SessionState.SetString(kCopiedPropertyPathKey, property.propertyPath); + SessionState.SetString(kClipboardKey, json); + } + + static void Paste (object customData) + { + SerializedProperty property = (SerializedProperty)customData; + string json = SessionState.GetString(kClipboardKey, string.Empty); + if (string.IsNullOrEmpty(json)) + { + return; + } + + Undo.RecordObject(property.serializedObject.targetObject, "Paste Property"); + JsonUtility.FromJsonOverwrite(json, property.managedReferenceValue); + property.serializedObject.ApplyModifiedProperties(); + } + + static void NewInstance (object customData) + { + SerializedProperty property = (SerializedProperty)customData; + string json = JsonUtility.ToJson(property.managedReferenceValue); + + Undo.RecordObject(property.serializedObject.targetObject, "New Instance"); + property.managedReferenceValue = JsonUtility.FromJson(json, property.managedReferenceValue.GetType()); + property.serializedObject.ApplyModifiedProperties(); + + Debug.Log($"Create new instance of \"{property.propertyPath}\"."); + } + + static void ResetAndNewInstance (object customData) + { + SerializedProperty property = (SerializedProperty)customData; + + Undo.RecordObject(property.serializedObject.targetObject, "Reset and New Instance"); + property.managedReferenceValue = Activator.CreateInstance(property.managedReferenceValue.GetType()); + property.serializedObject.ApplyModifiedProperties(); + + Debug.Log($"Reset property and created new instance of \"{property.propertyPath}\"."); + } + } +} diff --git a/Editor/SerializeReferenceSelector/ManagedReferenceContextualPropertyMenu.cs.meta b/Editor/SerializeReferenceSelector/ManagedReferenceContextualPropertyMenu.cs.meta new file mode 100644 index 0000000..2c16ab8 --- /dev/null +++ b/Editor/SerializeReferenceSelector/ManagedReferenceContextualPropertyMenu.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df77d1f7db91da049be82b46b15cb07c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SerializeReferenceSelector/SubclassSelectorDrawer.cs b/Editor/SerializeReferenceSelector/SubclassSelectorDrawer.cs new file mode 100644 index 0000000..10112f5 --- /dev/null +++ b/Editor/SerializeReferenceSelector/SubclassSelectorDrawer.cs @@ -0,0 +1,350 @@ +using System; +using System.Reflection; +using System.Linq; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.IMGUI.Controls; +using ubco.ovilab.HPUI.utils; + +namespace ubco.ovilab.HPUI.Editor +{ + /// This is taken from https://github.com/mackysoft/Unity-SerializeReferenceExtensions + [CustomPropertyDrawer(typeof(SubclassSelectorAttribute))] + public class SubclassSelectorDrawer : PropertyDrawer + { + static readonly int maxTypePopupLineCount = 13; + static readonly Type unityObjectType = typeof(UnityEngine.Object); + static readonly Dictionary drawerCaches = new Dictionary(); + static readonly string nullDisplayName = ""; + static readonly GUIContent contentNullDisplayName = new GUIContent(""); + static readonly GUIContent contentIsNotManagedReferenceLabel = new GUIContent("The property type is not manage reference."); + + readonly Dictionary typePopups = new Dictionary(); + readonly Dictionary typeNameCaches = new Dictionary(); + + SerializedProperty m_TargetProperty; + + public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + + if (property.propertyType == SerializedPropertyType.ManagedReference) + { + // Render label first to avoid label overlap for lists + Rect foldoutLabelRect = new Rect(position); + foldoutLabelRect.height = EditorGUIUtility.singleLineHeight; + foldoutLabelRect = EditorGUI.IndentedRect(foldoutLabelRect); + Rect popupPosition = EditorGUI.PrefixLabel(foldoutLabelRect, label); + + // Draw the subclass selector popup. + if (EditorGUI.DropdownButton(popupPosition, GetTypeName(property), FocusType.Keyboard)) + { + AdvancedTypePopup popup = GetTypePopup(property); + m_TargetProperty = property; + popup.Show(popupPosition); + } + + // Draw the foldout. + if (!string.IsNullOrEmpty(property.managedReferenceFullTypename)) + { + Rect foldoutRect = new Rect(position); + foldoutRect.height = EditorGUIUtility.singleLineHeight; + + // // NOTE: Position x must be adjusted. + // // FIXME: Is there a more essential solution...? + // foldoutRect.x -= 12; + + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, GUIContent.none, true); + } + + // Draw property if expanded. + if (property.isExpanded) + { + using (new EditorGUI.IndentLevelScope()) + { + // Check if a custom property drawer exists for this type. + PropertyDrawer customDrawer = GetCustomPropertyDrawer(property); + if (customDrawer != null) + { + // Draw the property with custom property drawer. + Rect indentedRect = position; + float foldoutDifference = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + indentedRect.height = customDrawer.GetPropertyHeight(property, label); + indentedRect.y += foldoutDifference; + customDrawer.OnGUI(indentedRect, property, label); + } + else + { + // Draw the properties of the child elements. + // NOTE: In the following code, since the foldout layout isn't working properly, I'll iterate through the properties of the child elements myself. + // EditorGUI.PropertyField(position, property, GUIContent.none, true); + Rect childPosition = position; + childPosition.y += (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing); + + GUIStyle style = new GUIStyle(GUI.skin.label); + style.fontStyle = FontStyle.BoldAndItalic; + GUIContent referenceName = new GUIContent(property.managedReferenceFullTypename); + float height = style.CalcSize(referenceName).y; + childPosition.height = height; + EditorGUI.LabelField(childPosition, referenceName, style); + + childPosition.y += (height + EditorGUIUtility.standardVerticalSpacing); + foreach (SerializedProperty childProperty in GetChildProperties(property)) + { + height = EditorGUI.GetPropertyHeight(childProperty, new GUIContent(childProperty.displayName, childProperty.tooltip), true); + childPosition.height = height; + EditorGUI.PropertyField(childPosition, childProperty, true); + + childPosition.y += height + EditorGUIUtility.standardVerticalSpacing; + } + } + } + } + } + else + { + EditorGUI.LabelField(position, label, contentIsNotManagedReferenceLabel); + } + + EditorGUI.EndProperty(); + } + + /// + /// Tries to get the custom property drawer if one exists. + /// + private PropertyDrawer GetCustomPropertyDrawer (SerializedProperty property) + { + Type propertyType = GetType(property.managedReferenceFullTypename); + PropertyDrawer drawer = null; + + if (propertyType != null && !drawerCaches.TryGetValue(propertyType, out drawer)) + { + Type drawerType = GetCustomPropertyDrawerType(propertyType); + drawer = (drawerType != null) ? (PropertyDrawer)Activator.CreateInstance(drawerType) : null; + + drawerCaches.Add(propertyType, drawer); + } + + if (propertyType != null && drawer != null) + { + return drawer; + } + return null; + } + + /// + /// Gets the searchable popup to set values. + /// + private AdvancedTypePopup GetTypePopup (SerializedProperty property) + { + // Cache this string. This property internally call Assembly.GetName, which result in a large allocation. + string managedReferenceFieldTypename = property.managedReferenceFieldTypename; + + if (!typePopups.TryGetValue(managedReferenceFieldTypename,out AdvancedTypePopup popup)) + { + var state = new AdvancedDropdownState(); + + Type baseType = GetType(managedReferenceFieldTypename); + popup = new AdvancedTypePopup( + TypeCache.GetTypesDerivedFrom(baseType).Append(baseType).Where(p => + (p.IsPublic || p.IsNestedPublic || p.IsNestedPrivate) && + !p.IsAbstract && + !p.IsGenericType && + !unityObjectType.IsAssignableFrom(p) && + Attribute.IsDefined(p,typeof(SerializableAttribute)) + ), + maxTypePopupLineCount, + nullDisplayName, + state + ); + + popup.OnItemSelected += item => + { + Type type = item.Type; + + // Apply changes to individual serialized objects. + foreach (var targetObject in m_TargetProperty.serializedObject.targetObjects) { + SerializedObject individualObject = new SerializedObject(targetObject); + SerializedProperty individualProperty = individualObject.FindProperty(m_TargetProperty.propertyPath); + object obj = SetManagedReference(individualProperty, type); + individualProperty.isExpanded = (obj != null); + + individualObject.ApplyModifiedProperties(); + individualObject.Update(); + } + }; + + typePopups.Add(managedReferenceFieldTypename, popup); + } + return popup; + } + + /// + /// Gets the GUIContent of the SerializedProperty that is decorated. + /// + private GUIContent GetTypeName (SerializedProperty property) + { + // Cache this string. + string managedReferenceFullTypename = property.managedReferenceFullTypename; + + if (string.IsNullOrEmpty(managedReferenceFullTypename)) + { + return contentNullDisplayName; + } + + if (typeNameCaches.TryGetValue(managedReferenceFullTypename,out GUIContent cachedTypeName)) + { + return cachedTypeName; + } + + Type type = GetType(managedReferenceFullTypename); + string typeName = null; + + typeName = ObjectNames.NicifyVariableName(type.Name); + + GUIContent result = new GUIContent(typeName); + typeNameCaches.Add(managedReferenceFullTypename,result); + return result; + } + + /// + public override float GetPropertyHeight (SerializedProperty property,GUIContent label) { + PropertyDrawer customDrawer = GetCustomPropertyDrawer(property); + if (customDrawer != null) + { + return property.isExpanded ? EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing + customDrawer.GetPropertyHeight(property,label):EditorGUIUtility.singleLineHeight; + } + else + { + if (property.isExpanded) + { + return EditorGUI.GetPropertyHeight(property, true) + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + } + else + { + return EditorGUIUtility.singleLineHeight; + } + } + } + + /// + /// Get from typeName + /// + internal static Type GetType (string typeName) + { + if (string.IsNullOrEmpty(typeName)) + { + return null; + } + + int splitIndex = typeName.IndexOf(' '); + var assembly = Assembly.Load(typeName.Substring(0, splitIndex)); + return assembly.GetType(typeName.Substring(splitIndex + 1)); + } + + /// + /// Sets and returns the managed reference. + /// + internal static object SetManagedReference (SerializedProperty property,Type type) + { + object result = null; + + if ((type != null) && (property.managedReferenceValue != null)) + { + // Restore an previous values from json. + string json = JsonUtility.ToJson(property.managedReferenceValue); + result = JsonUtility.FromJson(json, type); + } + + if (result == null) + { + result = (type != null) ? Activator.CreateInstance(type) : null; + } + + property.managedReferenceValue = result; + return result; + } + + /// + /// Enumerator to iterate on the child properties of a SerializedProperty. + /// + internal static IEnumerable GetChildProperties(SerializedProperty parent, int depth = 1) + { + parent = parent.Copy(); + + int depthOfParent = parent.depth; + var enumerator = parent.GetEnumerator(); + + while (enumerator.MoveNext()) + { + if (enumerator.Current is not SerializedProperty childProperty) + { + continue; + } + // if (childProperty.depth > (depthOfParent + depth)) + // { + // continue; + // } + yield return childProperty.Copy(); + } + } + + /// + /// Helper to + /// + private static Type GetCustomPropertyDrawerType (Type type) + { + Type[] interfaceTypes = type.GetInterfaces(); + + var types = TypeCache.GetTypesWithAttribute(); + foreach (Type drawerType in types) + { + var customPropertyDrawerAttributes = drawerType.GetCustomAttributes(typeof(CustomPropertyDrawer), true); + foreach (CustomPropertyDrawer customPropertyDrawer in customPropertyDrawerAttributes) + { + var field = customPropertyDrawer.GetType().GetField("m_Type", BindingFlags.NonPublic | BindingFlags.Instance); + if (field != null) + { + var fieldType = field.GetValue(customPropertyDrawer) as Type; + if (fieldType != null) + { + if (fieldType == type) + { + return drawerType; + } + + // If the property drawer also allows for being applied to child classes, check if they match + var useForChildrenField = customPropertyDrawer.GetType().GetField("m_UseForChildren", BindingFlags.NonPublic | BindingFlags.Instance); + if (useForChildrenField != null) + { + object useForChildrenValue = useForChildrenField.GetValue(customPropertyDrawer); + if (useForChildrenValue is bool && (bool)useForChildrenValue) + { + // Check interfaces + if (Array.Exists(interfaceTypes, interfaceType => interfaceType == fieldType)) + { + return drawerType; + } + + // Check derived types + Type baseType = type.BaseType; + while (baseType != null) + { + if (baseType == fieldType) + { + return drawerType; + } + + baseType = baseType.BaseType; + } + } + } + } + } + } + } + return null; + } + } +} diff --git a/Editor/SerializeReferenceSelector/SubclassSelectorDrawer.cs.meta b/Editor/SerializeReferenceSelector/SubclassSelectorDrawer.cs.meta new file mode 100644 index 0000000..23fe2f4 --- /dev/null +++ b/Editor/SerializeReferenceSelector/SubclassSelectorDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 754575bf6febdd34ba06c3e303f376a8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Interaction/HPUIInteractor.cs b/Runtime/Interaction/HPUIInteractor.cs index ce68fd5..d73122f 100644 --- a/Runtime/Interaction/HPUIInteractor.cs +++ b/Runtime/Interaction/HPUIInteractor.cs @@ -9,6 +9,7 @@ using UnityEngine.XR.Hands; using Unity.XR.CoreUtils; using UnityEngine.Pool; +using ubco.ovilab.HPUI.utils; namespace ubco.ovilab.HPUI.Interaction { @@ -256,10 +257,13 @@ public bool ShowSphereVisual /// public HPUIInteractorFullRangeAngles FullRangeRayAngles { get => fullRangeRayAngles; set => fullRangeRayAngles = value; } + [SerializeReference, SubclassSelector] + private IHPUIGestureLogic gestureLogic; + /// /// The gesture logic used by the interactor /// - public IHPUIGestureLogic GestureLogic { get; set; } + public IHPUIGestureLogic GestureLogic { get => gestureLogic; set => value = gestureLogic; } private Dictionary validTargets = new Dictionary(); private Vector3 lastInteractionPoint; diff --git a/Runtime/Utils.meta b/Runtime/Utils.meta new file mode 100644 index 0000000..cefedb2 --- /dev/null +++ b/Runtime/Utils.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a6556fd3442c7df45961677dc42ad586 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utils/SubclassSelectorAttribute.cs b/Runtime/Utils/SubclassSelectorAttribute.cs new file mode 100644 index 0000000..d8ebcfe --- /dev/null +++ b/Runtime/Utils/SubclassSelectorAttribute.cs @@ -0,0 +1,50 @@ +using System; +using UnityEngine; + +namespace ubco.ovilab.HPUI.utils +{ + /// + /// Attribute to specify the type of the field serialized by the SerializeReference attribute in the inspector. + /// This is taken from https://github.com/mackysoft/Unity-SerializeReferenceExtensions + /// + [AttributeUsage(AttributeTargets.Field,AllowMultiple = false)] + public sealed class SubclassSelectorAttribute : PropertyAttribute {} + + /// + /// An attribute that overrides the name of the type displayed in the SubclassSelector popup. + /// This is taken from https://github.com/mackysoft/Unity-SerializeReferenceExtensions + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Interface,AllowMultiple = false,Inherited = false)] + public sealed class AddTypeMenuAttribute : Attribute + { + public string MenuName { get; } + + public int Order { get; } + + public AddTypeMenuAttribute (string menuName,int order = 0) + { + MenuName = menuName; + Order = order; + } + + static readonly char[] k_Separeters = new char[] { '/' }; + + /// + /// Returns the menu name split by the '/' separator. + /// + public string[] GetSplittedMenuName () + { + return !string.IsNullOrWhiteSpace(MenuName) ? MenuName.Split(k_Separeters,StringSplitOptions.RemoveEmptyEntries) : Array.Empty(); + } + + /// + /// Returns the display name without the path. + /// + public string GetTypeNameWithoutPath () + { + string[] splittedDisplayName = GetSplittedMenuName(); + return (splittedDisplayName.Length != 0) ? splittedDisplayName[splittedDisplayName.Length - 1] : null; + } + + } +} diff --git a/Runtime/Utils/SubclassSelectorAttribute.cs.meta b/Runtime/Utils/SubclassSelectorAttribute.cs.meta new file mode 100644 index 0000000..1cd2b1d --- /dev/null +++ b/Runtime/Utils/SubclassSelectorAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b79973b7280c841428132b1e39da008b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: