diff --git a/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs b/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs index 18e10bbf..fd307738 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/AvatarProcessor.cs @@ -138,6 +138,7 @@ namespace nadena.dev.modular_avatar.core.editor } new RenameParametersHook().OnPreprocessAvatar(avatarGameObject); + new MenuFolderCreateHook().OnPreprocessAvatar(avatarGameObject); new MenuInstallHook().OnPreprocessAvatar(avatarGameObject); new MergeArmatureHook().OnPreprocessAvatar(avatarGameObject); new RetargetMeshes().OnPreprocessAvatar(avatarGameObject); diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuFolderCreatorTreeViewWindow.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuFolderCreatorTreeViewWindow.cs new file mode 100644 index 00000000..96e38162 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuFolderCreatorTreeViewWindow.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEditor.IMGUI.Controls; +using UnityEngine; +using VRC.SDK3.Avatars.Components; + +namespace nadena.dev.modular_avatar.core.editor { + public class AvMenuFolderCreatorTreeViewWindow : EditorWindow { + private AvMenuFolderCreatorTreeView _treeView; + + private VRCAvatarDescriptor Avatar { + set => this._treeView.Avatar = value; + } + + private ModularAvatarMenuFolderCreator Creator { + set => this._treeView.Creator = value; + } + + private Action OnMenuSelected = (creator) => { }; + + private void Awake() { + this._treeView = new AvMenuFolderCreatorTreeView(new TreeViewState()) { + OnSelect = (creator) => this.OnMenuSelected.Invoke(creator), + onDoubleClickSelect = this.Close + }; + } + + private void OnLostFocus() { + this.Close(); + } + + private void OnDisable() { + this.OnMenuSelected = (creator) => { }; + } + + private void OnGUI() { + if (this._treeView == null || this._treeView.Avatar == null) { + this.Close(); + return; + } + + this._treeView.OnGUI(new Rect(0, 0, this.position.width, this.position.height)); + } + + internal static void Show(VRCAvatarDescriptor avatar, ModularAvatarMenuFolderCreator creator, Action OnSelect) { + AvMenuFolderCreatorTreeViewWindow window = GetWindow(); + window.titleContent = new GUIContent("Select menu folder creator"); + + window.Avatar = avatar; + window.Creator = creator; + window.OnMenuSelected = OnSelect; + + window.Show(); + } + } + + public class AvMenuFolderCreatorTreeView : TreeView { + private VRCAvatarDescriptor _avatar; + public VRCAvatarDescriptor Avatar { + get => this._avatar; + set { + this._avatar = value; + this.Reload(); + } + } + + private ModularAvatarMenuFolderCreator _creator; + public ModularAvatarMenuFolderCreator Creator { + get => this._creator; + set { + this._creator = value; + this.Reload(); + } + } + + private int _currentCreatorIndex; + private readonly Texture2D _currentBackgroundTexture; + + internal Action OnSelect = (creator) => { }; + internal Action onDoubleClickSelect = () => { }; + + private readonly List _creatorItems = new List(); + private readonly HashSet _visitedCreators = new HashSet(); + + private Dictionary> _childMap; + private List _rootCreators; + + public AvMenuFolderCreatorTreeView(TreeViewState state) : base(state) { + this._currentBackgroundTexture = new Texture2D(1, 1); + this._currentBackgroundTexture.SetPixel(0, 0, new Color(0.0f, 0.3f, 0.0f)); + this._currentBackgroundTexture.Apply(); + } + + protected override void SelectionChanged(IList selectedIds) { + if (selectedIds[0] == this._currentCreatorIndex) return; + this.OnSelect.Invoke(this._creatorItems[selectedIds[0]]); + this.Reload(); + } + + protected override void DoubleClickedItem(int id) { + if (id == this._currentCreatorIndex) return; + this.OnSelect.Invoke(this._creatorItems[id]); + this.onDoubleClickSelect.Invoke(); + } + + protected override TreeViewItem BuildRoot() { + this._creatorItems.Clear(); + this._visitedCreators.Clear(); + this._currentCreatorIndex = -1; + this.MappingFolderCreator(); + + TreeViewItem root = new TreeViewItem(-1, -1, ""); + List treeItems = new List(); + treeItems.Add(new TreeViewItem { + id = treeItems.Count, + depth = 0, + displayName = $"{this._avatar.name} ({(this._avatar.expressionsMenu != null ? this._avatar.expressionsMenu.name : null)})" + }); + this._creatorItems.Add(null); + + foreach (ModularAvatarMenuFolderCreator rootCreator in this._rootCreators) { + bool isCurrent = rootCreator == this.Creator; + if (isCurrent) { + this._currentCreatorIndex = treeItems.Count; + } + treeItems.Add(new TreeViewItem { + id = treeItems.Count, + depth = 1, + displayName = isCurrent ? "This" : $"{rootCreator.name} ({rootCreator.folderName})" + }); + this._creatorItems.Add(rootCreator); + this._visitedCreators.Add(rootCreator); + if (isCurrent) continue; + this.TraverseCreator(2, treeItems, rootCreator); + } + + SetupParentsAndChildrenFromDepths(root, treeItems); + return root; + } + + private void TraverseCreator(int depth, List items, ModularAvatarMenuFolderCreator creator) { + if (!this._childMap.TryGetValue(creator, out List children)) return; + foreach (ModularAvatarMenuFolderCreator child in children.Where(child => !this._visitedCreators.Contains(child))) { + bool isCurrent = child == this.Creator; + if (isCurrent) { + this._currentCreatorIndex = items.Count; + } + + items.Add(new TreeViewItem { + id = items.Count, + depth = depth, + displayName = isCurrent ? "This" : $"{child.name} ({child.folderName})" + }); + + this._creatorItems.Add(child); + this._visitedCreators.Add(child); + if (isCurrent) continue; + this.TraverseCreator(depth + 1, items, child); + } + } + + protected override void RowGUI(RowGUIArgs args) { + if (args.item.id == this._currentCreatorIndex) { + Rect backGroundRect = args.rowRect; + GUI.DrawTexture(backGroundRect, this._currentBackgroundTexture, ScaleMode.StretchToFill, false, 0); + } + + base.RowGUI(args); + } + + + private void MappingFolderCreator() { + this._childMap = new Dictionary>(); + this._rootCreators = new List(); + + foreach (ModularAvatarMenuFolderCreator creator in this.Avatar.gameObject.GetComponentsInChildren()) { + if (!creator.enabled) continue; + if (creator.installTargetType == ModularAvatarMenuFolderCreator.InstallTargetType.VRCExpressionMenu) { + this._rootCreators.Add(creator); + } else { + if (creator.installTargetFolderCreator == null) { + this._rootCreators.Add(creator); + } else { + if (!this._childMap.TryGetValue(creator.installTargetFolderCreator, out List children)) { + children = new List(); + this._childMap[creator.installTargetFolderCreator] = children; + } + + children.Add(creator); + } + } + } + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuFolderCreatorTreeViewWindow.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuFolderCreatorTreeViewWindow.cs.meta new file mode 100644 index 00000000..ed02d29a --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuFolderCreatorTreeViewWindow.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8947dd0003544292b4fa12250f5771c9 +timeCreated: 1669814530 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuFolderCreatorEditor.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuFolderCreatorEditor.cs new file mode 100644 index 00000000..829ea911 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuFolderCreatorEditor.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; +using static nadena.dev.modular_avatar.core.ModularAvatarMenuFolderCreator; +using Object = UnityEngine.Object; + +namespace nadena.dev.modular_avatar.core.editor { + [CustomEditor(typeof(ModularAvatarMenuFolderCreator))] + [CanEditMultipleObjects] + internal class MenuFolderCreatorEditor : MAEditorBase { + private ModularAvatarMenuFolderCreator _creator; + private HashSet _avatarMenus; + private HashSet _menuFolderCreators; + + private void OnEnable() { + this._creator = (ModularAvatarMenuFolderCreator)this.target; + this.FindMenus(); + this.FindMenuFolderCreators(); + } + + protected override void OnInnerInspectorGUI() { + VRCAvatarDescriptor commonAvatar = this.FindCommonAvatar(); + + SerializedProperty installTargetTypeProperty = this.serializedObject.FindProperty(nameof(ModularAvatarMenuFolderCreator.installTargetType)); + EditorGUILayout.PropertyField(installTargetTypeProperty, new GUIContent("Install Target Type")); + InstallTargetType installTargetType = (InstallTargetType)Enum.ToObject(typeof(InstallTargetType), installTargetTypeProperty.enumValueIndex); + + if (!installTargetTypeProperty.hasMultipleDifferentValues) { + string installTargetMenuPropertyName; + Type installTargetObjectType; + if (installTargetType == InstallTargetType.VRCExpressionMenu) { + installTargetMenuPropertyName = nameof(ModularAvatarMenuFolderCreator.installTargetMenu); + installTargetObjectType = typeof(VRCExpressionsMenu); + } else { + installTargetMenuPropertyName = nameof(ModularAvatarMenuFolderCreator.installTargetFolderCreator); + installTargetObjectType = typeof(ModularAvatarMenuFolderCreator); + commonAvatar = null; + } + + SerializedProperty installTargetProperty = this.serializedObject.FindProperty(installTargetMenuPropertyName); + this.ShowMenuFolderCreateHelpBox(installTargetProperty, installTargetType); + this.ShowInstallTargetPropertyField(installTargetProperty, commonAvatar, installTargetObjectType); + + VRCAvatarDescriptor avatar = RuntimeUtil.FindAvatarInParents(this._creator.transform); + if (avatar != null && GUILayout.Button(Localization.G("menuinstall.selectmenu"))) { + if (installTargetType == InstallTargetType.VRCExpressionMenu) { + AvMenuTreeViewWindow.Show(avatar, menu => { + installTargetProperty.objectReferenceValue = menu; + serializedObject.ApplyModifiedProperties(); + }); + } else { + AvMenuFolderCreatorTreeViewWindow.Show(avatar, this._creator, creator => { + installTargetProperty.objectReferenceValue = creator; + serializedObject.ApplyModifiedProperties(); + }); + } + } + } + + SerializedProperty folderNameProperty = this.serializedObject.FindProperty(nameof(ModularAvatarMenuFolderCreator.folderName)); + EditorGUILayout.PropertyField(folderNameProperty, new GUIContent("Folder Name")); + + serializedObject.ApplyModifiedProperties(); + Localization.ShowLanguageUI(); + } + + private void ShowMenuFolderCreateHelpBox(SerializedProperty installTargetProperty, InstallTargetType installTargetType) { + if (installTargetProperty.hasMultipleDifferentValues) return; + bool isEnabled = this.targets.Length != 1 || this._creator.enabled; + + if (installTargetProperty.objectReferenceValue == null) { + if (!isEnabled) return; + EditorGUILayout.HelpBox(Localization.S("menuinstall.help.hint_set_menu"), MessageType.Info); + } else { + VRCAvatarDescriptor avatar = RuntimeUtil.FindAvatarInParents(this._creator.transform); + switch (installTargetType) { + case InstallTargetType.VRCExpressionMenu: + if (!this.IsMenuReachable(avatar, (VRCExpressionsMenu)installTargetProperty.objectReferenceValue)) { + EditorGUILayout.HelpBox(Localization.S("menuinstall.help.hint_bad_menu"), MessageType.Error); + } + + break; + case InstallTargetType.FolderCreator: + if (!this.IsMenuReachable(avatar, (ModularAvatarMenuFolderCreator)installTargetProperty.objectReferenceValue, + new HashSet())) { + EditorGUILayout.HelpBox("選択されたメニューフォルダからアバターまでのパスが見つかりません。", MessageType.Error); + } + + break; + default: + throw new ArgumentOutOfRangeException(nameof(installTargetType), installTargetType, null); + } + } + } + + private void ShowInstallTargetPropertyField(SerializedProperty installTargetProperty, VRCAvatarDescriptor avatar, Type propertyType) { + Object displayValue = installTargetProperty.objectReferenceValue; + if (!installTargetProperty.hasMultipleDifferentValues && avatar != null) { + if (displayValue == null) displayValue = avatar.expressionsMenu; + } + + EditorGUI.BeginChangeCheck(); + Object newValue = EditorGUILayout.ObjectField(Localization.G("menuinstall.installto"), displayValue, propertyType, + propertyType == typeof(ModularAvatarMenuFolderCreator)); + if (newValue == this._creator) newValue = displayValue; + if (EditorGUI.EndChangeCheck()) { + installTargetProperty.objectReferenceValue = newValue; + } + } + + private VRCAvatarDescriptor FindCommonAvatar() { + VRCAvatarDescriptor commonAvatar = null; + foreach (Object targetObject in targets) { + ModularAvatarMenuFolderCreator component = (ModularAvatarMenuFolderCreator)targetObject; + VRCAvatarDescriptor avatar = RuntimeUtil.FindAvatarInParents(component.transform); + if (avatar == null) return null; + + if (commonAvatar == null) { + commonAvatar = avatar; + } else if (commonAvatar != avatar) { + return null; + } + } + + return commonAvatar; + } + + private void FindMenus() { + if (this.targets.Length > 1) { + this._avatarMenus = null; + return; + } + + this._avatarMenus = new HashSet(); + Queue queue = new Queue(); + VRCAvatarDescriptor avatar = RuntimeUtil.FindAvatarInParents(this._creator.transform); + if (avatar == null || avatar.expressionsMenu == null) return; + queue.Enqueue(avatar.expressionsMenu); + + while (queue.Count > 0) { + var menu = queue.Dequeue(); + if (this._avatarMenus.Contains(menu)) continue; + + this._avatarMenus.Add(menu); + IEnumerable subMenus = menu.controls + .Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) + .Select(control => control.subMenu); + + foreach (VRCExpressionsMenu subMenu in subMenus) { + queue.Enqueue(subMenu); + } + } + } + + private void FindMenuFolderCreators() { + if (this.targets.Length > 1) { + this._menuFolderCreators = null; + return; + } + + this._menuFolderCreators = new HashSet(); + VRCAvatarDescriptor avatar = RuntimeUtil.FindAvatarInParents(this._creator.transform); + if (avatar == null) return; + foreach (ModularAvatarMenuFolderCreator creator in avatar.gameObject + .GetComponentsInChildren() + .Where(creator => creator != this._creator)) { + this._menuFolderCreators.Add(creator); + } + } + + private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu) { + return this._avatarMenus == null || this._avatarMenus.Contains(menu); + } + + private bool IsMenuReachable(VRCAvatarDescriptor avatar, ModularAvatarMenuFolderCreator creator, HashSet session) { + if (avatar == null) return true; + if (this._menuFolderCreators == null) return true; + + if (session.Contains(creator)) return false; + if (!this._menuFolderCreators.Contains(creator)) return false; + + if (!creator.enabled) return false; + session.Add(creator); + switch (creator.installTargetType) { + case InstallTargetType.VRCExpressionMenu: + return creator.installTargetMenu == null || this.IsMenuReachable(avatar, creator.installTargetMenu); + case InstallTargetType.FolderCreator: + return creator.installTargetFolderCreator == null || this.IsMenuReachable(avatar, creator.installTargetFolderCreator, session); + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuFolderCreatorEditor.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuFolderCreatorEditor.cs.meta new file mode 100644 index 00000000..6cdc3939 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuFolderCreatorEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 87c99ae6e87240aa9b53fdbf8e962107 +timeCreated: 1669789624 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs index c7dba388..c648f552 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MenuInstallerEditor.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEditor; using UnityEngine; using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.ScriptableObjects; using static nadena.dev.modular_avatar.core.editor.Localization; using static nadena.dev.modular_avatar.core.editor.Util; +using static nadena.dev.modular_avatar.core.ModularAvatarMenuFolderCreator; +using Object = UnityEngine.Object; namespace nadena.dev.modular_avatar.core.editor { @@ -21,12 +24,14 @@ namespace nadena.dev.modular_avatar.core.editor private bool _devFoldout; private HashSet _avatarMenus; + private HashSet _menuFolderCreators; private void OnEnable() { _installer = (ModularAvatarMenuInstaller) target; FindMenus(); + FindMenuFolderCreators(); } private void SetupMenuEditor() @@ -47,59 +52,54 @@ namespace nadena.dev.modular_avatar.core.editor _menuToAppend = _installer.menuToAppend; } } - protected override void OnInnerInspectorGUI() { SetupMenuEditor(); - - var installTo = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.installTargetMenu)); - - var isEnabled = targets.Length != 1 || ((ModularAvatarMenuInstaller) target).enabled; - + VRCAvatarDescriptor commonAvatar = FindCommonAvatar(); - if (!installTo.hasMultipleDifferentValues) + SerializedProperty installTargetTypeProperty = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.InstallTargetType)); + EditorGUILayout.PropertyField(installTargetTypeProperty, new GUIContent("Install Target Type")); + InstallTargetType installTargetType = (InstallTargetType)Enum.ToObject(typeof(InstallTargetType), installTargetTypeProperty.enumValueIndex); + + if (!installTargetTypeProperty.hasMultipleDifferentValues) { - if (installTo.objectReferenceValue == null) + string installTargetMenuPropertyName; + Type installTargetObjectType; + if (installTargetType == InstallTargetType.VRCExpressionMenu) { - if (isEnabled) + installTargetMenuPropertyName = nameof(ModularAvatarMenuFolderCreator.installTargetMenu); + installTargetObjectType = typeof(VRCExpressionsMenu); + } else + { + installTargetMenuPropertyName = nameof(ModularAvatarMenuFolderCreator.installTargetFolderCreator); + installTargetObjectType = typeof(ModularAvatarMenuFolderCreator); + commonAvatar = null; + } + + SerializedProperty installTargetProperty = this.serializedObject.FindProperty(installTargetMenuPropertyName); + this.ShowMenuInstallerHelpBox(installTargetProperty, installTargetType); + this.ShowInstallTargetPropertyField(installTargetProperty, commonAvatar, installTargetObjectType); + + var avatar = RuntimeUtil.FindAvatarInParents(_installer.transform); + if (avatar != null && GUILayout.Button(G("menuinstall.selectmenu"))) + { + if (installTargetType == InstallTargetType.VRCExpressionMenu) { - EditorGUILayout.HelpBox(S("menuinstall.help.hint_set_menu"), MessageType.Info); + AvMenuTreeViewWindow.Show(avatar, menu => + { + installTargetProperty.objectReferenceValue = menu; + serializedObject.ApplyModifiedProperties(); + }); + } else { + AvMenuFolderCreatorTreeViewWindow.Show(avatar, null, creator => + { + installTargetProperty.objectReferenceValue = creator; + serializedObject.ApplyModifiedProperties(); + }); } } - else if (!IsMenuReachable(RuntimeUtil.FindAvatarInParents(((Component) target).transform), - (VRCExpressionsMenu) installTo.objectReferenceValue)) - { - EditorGUILayout.HelpBox(S("menuinstall.help.hint_bad_menu"), MessageType.Error); - } - } - - if (installTo.hasMultipleDifferentValues || commonAvatar == null) - { - EditorGUILayout.PropertyField(installTo, G("menuinstall.installto")); - } - else - { - var displayValue = installTo.objectReferenceValue; - if (displayValue == null) displayValue = commonAvatar.expressionsMenu; - - EditorGUI.BeginChangeCheck(); - var newValue = EditorGUILayout.ObjectField(G("menuinstall.installto"), displayValue, - typeof(VRCExpressionsMenu), false); - if (EditorGUI.EndChangeCheck()) - { - installTo.objectReferenceValue = newValue; - } - } - - var avatar = RuntimeUtil.FindAvatarInParents(_installer.transform); - if (avatar != null && GUILayout.Button(G("menuinstall.selectmenu"))) - { - AvMenuTreeViewWindow.Show(avatar, menu => - { - installTo.objectReferenceValue = menu; - serializedObject.ApplyModifiedProperties(); - }); + } if (targets.Length == 1) @@ -146,6 +146,56 @@ namespace nadena.dev.modular_avatar.core.editor Localization.ShowLanguageUI(); } + + private void ShowMenuInstallerHelpBox(SerializedProperty installTargetProperty, InstallTargetType installTargetType) + { + if (installTargetProperty.hasMultipleDifferentValues) return; + bool isEnabled = targets.Length != 1 || this._installer.enabled; + + if (installTargetProperty.objectReferenceValue == null) + { + if (!isEnabled) return; + EditorGUILayout.HelpBox(S("menuinstall.help.hint_set_menu"), MessageType.Info); + } + else + { + VRCAvatarDescriptor avatar = RuntimeUtil.FindAvatarInParents(this._installer.transform); + switch (installTargetType) + { + case InstallTargetType.VRCExpressionMenu: + if (!this.IsMenuReachable(avatar, (VRCExpressionsMenu)installTargetProperty.objectReferenceValue)) + { + EditorGUILayout.HelpBox(Localization.S("menuinstall.help.hint_bad_menu"), MessageType.Error); + } + break; + case InstallTargetType.FolderCreator: + if (!this.IsMenuReachable(avatar, (ModularAvatarMenuFolderCreator)installTargetProperty.objectReferenceValue, new HashSet())) + { + EditorGUILayout.HelpBox("選択されたメニューフォルダからアバターまでのパスが見つかりません。", MessageType.Error); + } + break; + default: + throw new ArgumentOutOfRangeException(nameof(installTargetType), installTargetType, null); + } + } + } + + private void ShowInstallTargetPropertyField(SerializedProperty installTargetProperty, VRCAvatarDescriptor avatar, Type propertyType) + { + Object displayValue = installTargetProperty.objectReferenceValue; + if (!installTargetProperty.hasMultipleDifferentValues && avatar != null) + { + if (displayValue == null) displayValue = avatar.expressionsMenu; + } + + EditorGUI.BeginChangeCheck(); + Object newValue = EditorGUILayout.ObjectField(G("menuinstall.installto"), displayValue, propertyType, + propertyType == typeof(ModularAvatarMenuFolderCreator)); + if (EditorGUI.EndChangeCheck()) + { + installTargetProperty.objectReferenceValue = newValue; + } + } private VRCAvatarDescriptor FindCommonAvatar() { @@ -200,11 +250,49 @@ namespace nadena.dev.modular_avatar.core.editor } } + private void FindMenuFolderCreators() + { + if (this.targets.Length > 1) + { + this._menuFolderCreators = null; + return; + } + + this._menuFolderCreators = new HashSet(); + VRCAvatarDescriptor avatar = RuntimeUtil.FindAvatarInParents(this._installer.transform); + if (avatar == null) return; + foreach (ModularAvatarMenuFolderCreator creator in avatar.gameObject.GetComponentsInChildren()) + { + this._menuFolderCreators.Add(creator); + } + } + private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu) { return _avatarMenus == null || _avatarMenus.Contains(menu); } + private bool IsMenuReachable(VRCAvatarDescriptor avatar, ModularAvatarMenuFolderCreator creator, HashSet session) + { + if (avatar == null) return true; + if (this._menuFolderCreators == null) return true; + + if (session.Contains(creator)) return false; + if (!this._menuFolderCreators.Contains(creator)) return false; + + if (!creator.enabled) return false; + session.Add(creator); + switch (creator.installTargetType) + { + case InstallTargetType.VRCExpressionMenu: + return creator.installTargetMenu == null || this.IsMenuReachable(avatar, creator.installTargetMenu); + case InstallTargetType.FolderCreator: + return creator.installTargetFolderCreator == null || this.IsMenuReachable(avatar, creator.installTargetFolderCreator, session); + default: + throw new ArgumentOutOfRangeException(); + } + } + private static ValidateExpressionMenuIconResult ValidateExpressionMenuIcon(VRCExpressionsMenu menu) { if (menu == null) return ValidateExpressionMenuIconResult.Success; @@ -229,6 +317,7 @@ namespace nadena.dev.modular_avatar.core.editor } return ValidateExpressionMenuIconResult.Success; - } + + } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuFolderCreateHook.cs b/Packages/nadena.dev.modular-avatar/Editor/MenuFolderCreateHook.cs new file mode 100644 index 00000000..e826e397 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuFolderCreateHook.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; +using static nadena.dev.modular_avatar.core.ModularAvatarMenuFolderCreator; +using Object = UnityEngine.Object; + +namespace nadena.dev.modular_avatar.core.editor { + internal class MenuFolderCreateHook { + private static Texture2D _MORE_ICON = AssetDatabase.LoadAssetAtPath( + "Packages/nadena.dev.modular-avatar/Runtime/Icons/Icon_More_A.png" + ); + + private readonly Dictionary> _childMap; + private readonly List _rootCreators; + + private readonly Dictionary _clonedMenus; + private readonly Dictionary _creatFolders; + private Dictionary _installTargets; + + private VRCExpressionsMenu _rootMenu; + + public MenuFolderCreateHook() { + this._childMap = new Dictionary>(); + this._rootCreators = new List(); + this._clonedMenus = new Dictionary(); + this._creatFolders = new Dictionary(); + } + + public void OnPreprocessAvatar(GameObject avatarRoot) { + this._childMap.Clear(); + this._rootCreators.Clear(); + this._clonedMenus.Clear(); + + this.MappingFolderCreator(avatarRoot); + VRCAvatarDescriptor avatar = avatarRoot.GetComponent(); + if (avatar.expressionsMenu == null) { + avatar.expressionsMenu = CreateMenuAsset(); + } + + this._rootMenu = avatar.expressionsMenu; + avatar.expressionsMenu = this.CloneMenu(avatar.expressionsMenu); + this._installTargets = new Dictionary(this._clonedMenus); + + foreach (ModularAvatarMenuFolderCreator rootCreator in this._rootCreators.Where(rootCreator => rootCreator.enabled)) { + if (rootCreator.installTargetMenu == null) { + rootCreator.installTargetMenu = this._rootMenu; + } + + if (rootCreator.installTargetMenu == null) continue; + if (!this._installTargets.TryGetValue(rootCreator.installTargetMenu, out VRCExpressionsMenu targetMenu)) continue; + + if (!this._creatFolders.TryGetValue(rootCreator, out VRCExpressionsMenu folderMenu)) { + folderMenu = CreateMenuAsset(); + this._creatFolders[rootCreator] = folderMenu; + } + + AddSubMenuElement(targetMenu, rootCreator.folderName, folderMenu); // TODO: Support Custom Icon + if (!this._childMap.TryGetValue(rootCreator, out List children)) continue; + foreach (ModularAvatarMenuFolderCreator child in children) { + this.CreateChildFolder(child); + } + this.SplitMenu(rootCreator); + + this.SplitParentMenu(targetMenu, rootCreator); + } + + ReassignmentMenuInstaller(avatarRoot); + } + + + private void CreateChildFolder(ModularAvatarMenuFolderCreator creator) { + if (!this._creatFolders.TryGetValue(creator.installTargetFolderCreator, out VRCExpressionsMenu targetMenu)) return; + if (!this._creatFolders.TryGetValue(creator, out VRCExpressionsMenu folderMenu)) { + // 子が1つの親を参照する関係なので、同じ要素が複数現れることはありえない。 + // 同様に循環参照等にもたどり付けないので考慮に入れなくてよい。 + folderMenu = CreateMenuAsset(); + this._creatFolders[creator] = folderMenu; + } + + AddSubMenuElement(targetMenu, creator.folderName, folderMenu); // TODO: Support Custom Icon + if (!this._childMap.TryGetValue(creator, out List children)) return; + foreach (ModularAvatarMenuFolderCreator child in children) { + this.CreateChildFolder(child); + } + + this.SplitMenu(creator); + } + + private void SplitMenu(ModularAvatarMenuFolderCreator creator) { + VRCExpressionsMenu targetMenu = this._creatFolders[creator]; + while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS) { + VRCExpressionsMenu newMenu = CreateMenuAsset(); + const int keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1; + newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount)); + targetMenu.controls.RemoveRange(keepCount, targetMenu.controls.Count - keepCount); + AddSubMenuElement(targetMenu, "More", newMenu, _MORE_ICON); + this._creatFolders[creator] = newMenu; + targetMenu = newMenu; + } + } + + private void SplitParentMenu(VRCExpressionsMenu targetMenu, ModularAvatarMenuFolderCreator rootCreator) { + while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS) { + VRCExpressionsMenu newMenu = CreateMenuAsset(); + const int keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1; + newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount)); + targetMenu.controls.RemoveRange(keepCount, targetMenu.controls.Count - keepCount); + AddSubMenuElement(targetMenu, "More", newMenu, _MORE_ICON); + this._installTargets[rootCreator.installTargetMenu] = newMenu; + targetMenu = newMenu; + } + } + + private void ReassignmentMenuInstaller(GameObject avatarRoot) { + ModularAvatarMenuInstaller[] menuInstallers = avatarRoot.GetComponentsInChildren(true) + .Where(installer => installer.enabled) + .ToArray(); + foreach (ModularAvatarMenuInstaller installer in menuInstallers) { + if (installer.installTargetMenu == null) { + installer.installTargetMenu = this._rootMenu; + } + + if (installer.InstallTargetType == InstallTargetType.VRCExpressionMenu || installer.installTargetFolderCreator == null) { + installer.installTargetMenu = this._installTargets[installer.installTargetMenu]; + } else { + installer.installTargetMenu = this._creatFolders[installer.installTargetFolderCreator]; + } + } + } + + private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu) { + if (menu == null) return null; + if (this._clonedMenus.TryGetValue(menu, out VRCExpressionsMenu newMenu)) return newMenu; + newMenu = Object.Instantiate(menu); + AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); + this._clonedMenus[menu] = newMenu; + + foreach (VRCExpressionsMenu.Control control in newMenu.controls.Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)) { + control.subMenu = this.CloneMenu(control.subMenu); + } + + return newMenu; + } + + private void MappingFolderCreator(GameObject avatarRoot) { + foreach (ModularAvatarMenuFolderCreator creator in avatarRoot.GetComponentsInChildren(true)) { + if (!creator.enabled) continue; + if (creator.installTargetType == InstallTargetType.VRCExpressionMenu) { + this._rootCreators.Add(creator); + } else { + if (creator.installTargetFolderCreator == null) { + this._rootCreators.Add(creator); + } else { + if (!this._childMap.TryGetValue(creator.installTargetFolderCreator, out List children)) { + children = new List(); + this._childMap[creator.installTargetFolderCreator] = children; + } + + children.Add(creator); + } + } + } + } + + public static void AddSubMenuElement(VRCExpressionsMenu targetMenu, string elementName, VRCExpressionsMenu subMenu, Texture2D icon = null) { + targetMenu.controls.Add(new VRCExpressionsMenu.Control() { + name = elementName, + type = VRCExpressionsMenu.Control.ControlType.SubMenu, + subMenu = subMenu, + parameter = new VRCExpressionsMenu.Control.Parameter { + name = "" + }, + subParameters = Array.Empty(), + icon = icon, + labels = Array.Empty() + }); + } + + private static VRCExpressionsMenu CreateMenuAsset() { + VRCExpressionsMenu menuFolder = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(menuFolder, Util.GenerateAssetPath()); + return menuFolder; + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuFolderCreateHook.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/MenuFolderCreateHook.cs.meta new file mode 100644 index 00000000..30cd8c67 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuFolderCreateHook.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b732d550fcb1441a91b791c6caecac36 +timeCreated: 1669796064 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuFolderCreator.cs b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuFolderCreator.cs new file mode 100644 index 00000000..ec6ae3bd --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuFolderCreator.cs @@ -0,0 +1,19 @@ +using UnityEngine; +using VRC.SDK3.Avatars.ScriptableObjects; + +namespace nadena.dev.modular_avatar.core { + [AddComponentMenu("Modular Avatar/MA Menu Folder Creator")] + public class ModularAvatarMenuFolderCreator : AvatarTagComponent { + public InstallTargetType installTargetType; + public VRCExpressionsMenu installTargetMenu; + public ModularAvatarMenuFolderCreator installTargetFolderCreator; + public string folderName; + + + public enum InstallTargetType { + VRCExpressionMenu, + FolderCreator, + } + + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuFolderCreator.cs.meta b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuFolderCreator.cs.meta new file mode 100644 index 00000000..329b40f8 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuFolderCreator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: af43171cf0e846c19c34c0420dac976a +timeCreated: 1669789344 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs index 4c38dc65..b9aeb45a 100644 --- a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs +++ b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuInstaller.cs @@ -1,14 +1,16 @@ using UnityEngine; using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.ScriptableObjects; +using static nadena.dev.modular_avatar.core.ModularAvatarMenuFolderCreator; namespace nadena.dev.modular_avatar.core { [AddComponentMenu("Modular Avatar/MA Menu Installer")] - public class ModularAvatarMenuInstaller : AvatarTagComponent - { + public class ModularAvatarMenuInstaller : AvatarTagComponent { + public InstallTargetType InstallTargetType; public VRCExpressionsMenu menuToAppend; public VRCExpressionsMenu installTargetMenu; + public ModularAvatarMenuFolderCreator installTargetFolderCreator; // ReSharper disable once Unity.RedundantEventFunction