diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvMenuTreeView.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvMenuTreeView.cs new file mode 100644 index 00000000..e12f8c46 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvMenuTreeView.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEditor; +using UnityEditor.IMGUI.Controls; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; +using VRC.SDKBase; + +namespace net.fushizen.modular_avatar.core.editor +{ + class AvMenuTreeViewWindow : EditorWindow + { + private VRCAvatarDescriptor _avatarDescriptor; + private AvMenuTreeView _treeView; + + public VRCAvatarDescriptor Avatar + { + get => _treeView.Avatar; + set => _treeView.Avatar = value; + } + + public Action OnMenuSelected = (menu) => { }; + + private void Awake() + { + _treeView = new AvMenuTreeView(new TreeViewState()); + _treeView.OnSelect = (menu) => OnMenuSelected.Invoke(menu); + _treeView.OnDoubleclickSelect = Close; + } + + private void OnDisable() + { + OnMenuSelected = (menu) => { }; + } + + private void OnGUI() + { + if (_treeView == null || _treeView.Avatar == null) + { + Close(); + return; + } + + _treeView.OnGUI(new Rect(0, 0, position.width, position.height)); + } + + internal static void Show(VRCAvatarDescriptor Avatar, Action OnSelect) + { + var window = GetWindow(); + window.titleContent = new GUIContent("Select menu"); + + window.Avatar = Avatar; + window.OnMenuSelected = OnSelect; + + window.Show(); + } + } + + class AvMenuTreeView : TreeView + { + private VRCAvatarDescriptor _avatar; + + public VRCAvatarDescriptor Avatar + { + get => _avatar; + set + { + _avatar = value; + Reload(); + } + } + + internal Action OnSelect = (menu) => { }; + internal Action OnDoubleclickSelect = () => { }; + + private List _menuItems = new List(); + private HashSet _visitedMenus = new HashSet(); + + public AvMenuTreeView(TreeViewState state) : base(state) + { + } + + protected override void SelectionChanged(IList selectedIds) + { + OnSelect.Invoke(_menuItems[selectedIds[0]]); + } + + protected override void DoubleClickedItem(int id) + { + OnSelect.Invoke(_menuItems[id]); + OnDoubleclickSelect.Invoke(); + } + + protected override TreeViewItem BuildRoot() + { + _menuItems.Clear(); + _visitedMenus.Clear(); + + if (Avatar.expressionsMenu == null) + { + return new TreeViewItem(0, -1, "No menu"); + } + + _visitedMenus.Add(Avatar.expressionsMenu); + _menuItems.Add(Avatar.expressionsMenu); + var root = new TreeViewItem {id = -1, depth = -1, displayName = ""}; + + var treeItems = new List(); + treeItems.Add(new TreeViewItem + {id = 0, depth = 0, displayName = $"{Avatar.gameObject.name} ({Avatar.expressionsMenu.name})"}); + + TraverseMenu(1, treeItems, Avatar.expressionsMenu); + + SetupParentsAndChildrenFromDepths(root, treeItems); + + return root; + } + + private void TraverseMenu(int depth, List items, VRCExpressionsMenu menu) + { + foreach (var control in menu.controls) + { + if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu + && control.subMenu != null && !_visitedMenus.Contains(control.subMenu)) + { + items.Add(new TreeViewItem + { + id = _menuItems.Count, + depth = depth, + displayName = $"{control.name} ({control.subMenu.name})" + }); + _menuItems.Add(control.subMenu); + _visitedMenus.Add(control.subMenu); + + TraverseMenu(depth + 1, items, control.subMenu); + } + } + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvMenuTreeView.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvMenuTreeView.cs.meta new file mode 100644 index 00000000..f910857a --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvMenuTreeView.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8d6eb9f3b9084abb9453139d5b48ae18 +timeCreated: 1664855743 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarParametersEditor.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarParametersEditor.cs new file mode 100644 index 00000000..16b5973c --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarParametersEditor.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; + +namespace net.fushizen.modular_avatar.core.editor +{ + [CustomEditor(typeof(ModularAvatarParameters))] + public class AvatarParametersEditor : Editor + { + /********************************************************************** + * | Field name | Remap to / config | + * |------------|--------------------| + */ + + private bool _devMode; + private ReorderableList _reorderableList; + private SerializedProperty _parameters; + + private readonly List _selectedIndices = new List(); + + private void OnEnable() + { + SetupList(); + } + + private void SetupList() + { + _parameters = serializedObject.FindProperty(nameof(ModularAvatarParameters.parameters)); + if (_devMode) + { + _reorderableList = new ReorderableList( + serializedObject, + _parameters, + true, true, true, true + ); + _reorderableList.drawHeaderCallback = DrawHeader; + _reorderableList.drawElementCallback = DrawElement; + _reorderableList.onAddCallback = AddElement; + _reorderableList.onRemoveCallback = RemoveElement; + } + else + { + _selectedIndices.Clear(); + for (int i = 0; i < _parameters.arraySize; i++) + { + var isInternal = _parameters.GetArrayElementAtIndex(i) + .FindPropertyRelative(nameof(ParameterConfig.internalParameter)); + if (isInternal.boolValue) continue; + + _selectedIndices.Add(i); + } + + _reorderableList = new ReorderableList( + _selectedIndices, + typeof(int), + false, true, false, false + ); + _reorderableList.drawHeaderCallback = DrawHeader; + _reorderableList.drawElementCallback = DrawElement; + _reorderableList.onAddCallback = AddElement; + _reorderableList.onRemoveCallback = RemoveElement; + } + } + + private void AddElement(ReorderableList list) + { + _parameters.arraySize += 1; + list.index = _parameters.arraySize - 1; + } + + private void RemoveElement(ReorderableList list) + { + if (list.index < 0) return; + _parameters.DeleteArrayElementAtIndex(list.index); + } + + private void DrawElement(Rect rect, int index, bool isactive, bool isfocused) + { + var margin = 20; + var halfMargin = margin / 2; + var leftHalf = new Rect(rect.x, rect.y, rect.width / 2 - halfMargin, rect.height); + var rightHalf = new Rect(rect.x + leftHalf.width + halfMargin, rect.y, leftHalf.width, rect.height); + + if (!_devMode) index = _selectedIndices[index]; + + var elem = _parameters.GetArrayElementAtIndex(index); + + var nameOrPrefix = elem.FindPropertyRelative(nameof(ParameterConfig.nameOrPrefix)); + var remapTo = elem.FindPropertyRelative(nameof(ParameterConfig.remapTo)); + var internalParameter = elem.FindPropertyRelative(nameof(ParameterConfig.internalParameter)); + var isPrefix = elem.FindPropertyRelative(nameof(ParameterConfig.isPrefix)); + + var indentLevel = EditorGUI.indentLevel; + try + { + indentLevel = 0; + if (_devMode) + { + EditorGUI.PropertyField(leftHalf, nameOrPrefix, GUIContent.none); + + var toggleInternalWidth = EditorStyles.toggle.CalcSize(new GUIContent("Internal")).x; + var toggleInternalRect = new Rect(rightHalf.x, rightHalf.y, toggleInternalWidth, rightHalf.height); + + internalParameter.boolValue = + EditorGUI.ToggleLeft(toggleInternalRect, "Internal", internalParameter.boolValue); + + var isPrefixRect = new Rect(rightHalf.x + toggleInternalWidth + halfMargin, rightHalf.y, + rightHalf.width - toggleInternalWidth - halfMargin, rightHalf.height); + isPrefix.boolValue = EditorGUI.ToggleLeft(isPrefixRect, "PhysBones Prefix", isPrefix.boolValue); + } + else + { + EditorGUI.LabelField(leftHalf, + isPrefix.boolValue ? nameOrPrefix.stringValue + "*" : nameOrPrefix.stringValue); + EditorGUI.PropertyField(rightHalf, remapTo, GUIContent.none); + } + } + finally + { + EditorGUI.indentLevel = indentLevel; + } + } + + private void DrawHeader(Rect rect) + { + var leftHalf = new Rect(rect.x, rect.y, rect.width / 2, rect.height); + var rightHalf = new Rect(rect.x + rect.width / 2, rect.y, rect.width / 2, rect.height); + + EditorGUI.LabelField(leftHalf, "Field name"); + if (!_devMode) EditorGUI.LabelField(rightHalf, "Remap to"); + } + + public override void OnInspectorGUI() + { + EditorGUI.BeginChangeCheck(); + _devMode = EditorGUILayout.Toggle("Developer mode", _devMode); + if (EditorGUI.EndChangeCheck()) SetupList(); + _reorderableList.DoLayoutList(); + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarParametersEditor.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarParametersEditor.cs.meta new file mode 100644 index 00000000..aa9165bc --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/AvatarParametersEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 21089a5ec87c4e4fa1616376cbb8fbd7 +timeCreated: 1664847495 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs b/Packages/net.fushizen.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs new file mode 100644 index 00000000..09c5885f --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs @@ -0,0 +1,90 @@ +using System; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace net.fushizen.modular_avatar.core.editor +{ + [CustomEditor(typeof(ModularAvatarMenuInstaller))] + [CanEditMultipleObjects] + public class MenuInstallerEditor : Editor + { + private ModularAvatarMenuInstaller _installer; + private Editor _innerMenuEditor; + private VRCExpressionsMenu _menuToAppend; + + private bool _menuFoldout; + private bool _devFoldout; + + private void OnEnable() + { + _installer = (ModularAvatarMenuInstaller) target; + } + + private void SetupMenuEditor() + { + if (targets.Length != 1) + { + _innerMenuEditor = null; + _menuToAppend = null; + } + else if (_installer.menuToAppend != _menuToAppend) + { + if (_installer.menuToAppend == null) _innerMenuEditor = null; + else + { + _innerMenuEditor = CreateEditor(_installer.menuToAppend); + } + + _menuToAppend = _installer.menuToAppend; + } + } + + public override void OnInspectorGUI() + { + SetupMenuEditor(); + + var installTo = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.installTargetMenu)); + EditorGUILayout.PropertyField(installTo, new GUIContent("Install To")); + + var avatar = RuntimeUtil.FindAvatarInParents(_installer.transform); + if (avatar != null && GUILayout.Button("Select menu")) + { + AvMenuTreeViewWindow.Show(avatar, menu => + { + installTo.objectReferenceValue = menu; + serializedObject.ApplyModifiedProperties(); + }); + } + + if (targets.Length == 1) + { + _menuFoldout = EditorGUILayout.Foldout(_menuFoldout, "Show menu contents"); + if (_menuFoldout) + { + EditorGUI.indentLevel++; + using (var disabled = new EditorGUI.DisabledScope(true)) + { + if (_innerMenuEditor != null) _innerMenuEditor.OnInspectorGUI(); + else EditorGUILayout.HelpBox("No menu selected", MessageType.Info); + } + + EditorGUI.indentLevel--; + } + } + + _devFoldout = EditorGUILayout.Foldout(_devFoldout, "Developer Options"); + if (_devFoldout) + { + EditorGUI.indentLevel++; + EditorGUILayout.PropertyField( + serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.menuToAppend)), + new GUIContent("Menu to install") + ); + EditorGUI.indentLevel--; + } + + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs.meta b/Packages/net.fushizen.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs.meta new file mode 100644 index 00000000..e0709603 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fcf1a7e74b8a4e7e98e7c557ecb8375e +timeCreated: 1664853873 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs new file mode 100644 index 00000000..f1fa8e96 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs @@ -0,0 +1,10 @@ +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace net.fushizen.modular_avatar.core +{ + public class ModularAvatarMenuInstaller : AvatarTagComponent + { + public VRCExpressionsMenu menuToAppend; + public VRCExpressionsMenu installTargetMenu; + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs.meta b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs.meta new file mode 100644 index 00000000..3dd1006b --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7ef83cb0c23d4d7c9d41021e544a1978 +timeCreated: 1664850837 \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs new file mode 100644 index 00000000..ddfcea10 --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace net.fushizen.modular_avatar.core +{ + [Serializable] + public struct ParameterConfig + { + public string nameOrPrefix; + public string remapTo; + public bool internalParameter, isPrefix; + } + + public class ModularAvatarParameters : AvatarTagComponent + { + public List parameters = new List(); + } +} \ No newline at end of file diff --git a/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs.meta b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs.meta new file mode 100644 index 00000000..225c74bb --- /dev/null +++ b/Packages/net.fushizen.modular-avatar/Runtime/ModularAvatarParameters.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 71a96d4ea0c344f39e277d82035bf9bd +timeCreated: 1664847329 \ No newline at end of file