using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEngine; using UnityEngine.XR.OpenXR; using UnityEngine.XR.OpenXR.Features; using UnityEditorInternal; namespace UnityEditor.XR.OpenXR.Features { enum IssueType { None, Warning, Error } internal class ChildListItem { public GUIContent uiName; public GUIContent documentationIcon; public GUIContent categoryName; public GUIContent version; public GUIContent partner; public string partnerName; public string documentationLink; public bool settingsExpanded; public OpenXRFeature feature; public bool shouldDisplaySettings; public UnityEditor.Editor settingsEditor; public string featureId; public IssueType issueType; } internal class OpenXRFeatureEditor { /// /// Path of the OpenXR settings in the Settings window. Uses "/" as separator. The last token becomes the settings label if none is provided. /// public const string k_FeatureSettingsPathUI = #if XR_MGMT_3_2_0_OR_NEWER "Project/XR Plug-in Management/OpenXR/Features"; #else "Project/XR Plugin Management/OpenXR/Features"; #endif static class Styles { public static float k_IconWidth = 16f; public static float k_DefaultSelectionWidth = 200f; public static float k_DefualtLineMultiplier = 2f; public static GUIStyle s_SelectionStyle = "TV Selection"; public static GUIStyle s_SelectionBackground = "ScrollViewAlt"; public static GUIStyle s_FeatureSetTitleLable; public static GUIStyle s_ListLabel; public static GUIStyle s_ListSelectedLabel; public static GUIStyle s_ListLabelToggle; public static GUIStyle s_Feature; public static GUIStyle s_FeatureSettings; } static class Content { public static readonly GUIContent k_HelpIcon = EditorGUIUtility.IconContent("_Help"); public static readonly GUIContent k_SettingsIcon = EditorGUIUtility.IconContent("Settings"); public static readonly GUIContent k_Settings = new GUIContent("", k_SettingsIcon.image, "Open settings editor for this feature."); public static readonly GUIContent k_InteractionProfilesTitle = new GUIContent("Enabled Interaction Profiles"); } List selectionListItems = new List(); OpenXRFeatureSetManager.FeatureSetInfo selectedItem = null; private List issuesPerFeatureSet = new List(); List filteredListItems = new List(); List allListItems = new List(); Dictionary interactionItems = new Dictionary(); List selectedFeatureIds = new List(); ReorderableList interactionFeaturesList = null; HashSet requiredFeatures = new HashSet(); FeatureHelpersInternal.AllFeatureInfo allFeatureInfos = null; BuildTargetGroup activeBuildTarget = BuildTargetGroup.Unknown; List _issues = new List(); Dictionary lastSelectedItemIndex = new Dictionary(); OpenXRFeatureSettingsEditor featureSetSettingsEditor = null; bool mustInitializeFeatures = false; static readonly string s_AllFeatures = "All Features"; public OpenXRFeatureEditor() { SetupInteractionListUI(); } (Rect, Rect) TakeFromFrontOfRect(Rect rect, float width) { var newRect = new Rect(rect); newRect.x = rect.x + 5; newRect.width = width; rect.x = (newRect.x + newRect.width) + 1; rect.width -= width + 1; return (newRect, rect); } void SetupInteractionListUI() { if (interactionFeaturesList != null) return; interactionFeaturesList = new ReorderableList(selectedFeatureIds, typeof(ChildListItem), false, true, true, true); interactionFeaturesList.drawHeaderCallback = (rect) => { var labelSize = EditorStyles.label.CalcSize(Content.k_InteractionProfilesTitle); var labelRect = new Rect(rect); labelRect.width = labelSize.x; EditorGUI.LabelField(labelRect, Content.k_InteractionProfilesTitle, EditorStyles.label); }; interactionFeaturesList.drawElementCallback = (rect, index, isActive, isFocused) => { Rect fieldRect; string featureId = selectedFeatureIds[index]; var item = interactionItems[featureId]; var labelSize = EditorStyles.label.CalcSize(item.uiName); (fieldRect, rect) = TakeFromFrontOfRect(rect, labelSize.x); EditorGUI.BeginDisabledGroup(requiredFeatures.Contains(item.featureId)); EditorGUI.LabelField(fieldRect, item.uiName, EditorStyles.label); EditorGUI.EndDisabledGroup(); if (!String.IsNullOrEmpty(item.documentationLink)) { var size = EditorStyles.label.CalcSize(item.documentationIcon); (fieldRect, rect) = TakeFromFrontOfRect(rect, size.x); if (GUI.Button(fieldRect, item.documentationIcon, EditorStyles.label)) { UnityEngine.Application.OpenURL(item.documentationLink); } } if (item.issueType != IssueType.None) { GUIContent icon = (item.issueType == IssueType.Error) ? CommonContent.k_ValidationErrorIcon : CommonContent.k_ValidationWarningIcon; var size = EditorStyles.label.CalcSize(icon); (fieldRect, rect) = TakeFromFrontOfRect(rect, size.x); if (GUI.Button(fieldRect, icon, EditorStyles.label)) { OpenXRProjectValidationRulesSetup.ShowWindow(activeBuildTarget); } } }; interactionFeaturesList.onAddDropdownCallback = (rect, list) => { GenericMenu menu = new GenericMenu(); foreach (var kvp in interactionItems) { if (selectedFeatureIds.IndexOf(kvp.Key) == -1) { menu.AddItem(kvp.Value.uiName, false, (object obj) => { string featureId = obj as string; if (!String.IsNullOrEmpty(featureId)) { selectedFeatureIds.Add(featureId); var interactionItem = interactionItems[featureId]; interactionItem.feature.enabled = true; } }, kvp.Key); } } menu.DropDown(rect); }; interactionFeaturesList.onCanRemoveCallback = (list) => { var featureId = selectedFeatureIds[list.index]; return !requiredFeatures.Contains(featureId); }; interactionFeaturesList.onRemoveCallback = (list) => { var featureId = selectedFeatureIds[list.index]; if (requiredFeatures.Contains(featureId)) return; var interactionItem = interactionItems[featureId]; interactionItem.feature.enabled = false; selectedFeatureIds.RemoveAt(list.index); }; } void UpdateValidationIssues(BuildTargetGroup buildTargetGroup) { _issues.Clear(); OpenXRProjectValidation.GetCurrentValidationIssues(_issues, buildTargetGroup); foreach (var item in allListItems) { item.issueType = GetValidationIssueType(item.feature); } foreach (var item in interactionItems.Values) { item.issueType = GetValidationIssueType(item.feature); } issuesPerFeatureSet.Clear(); foreach (var featureSet in selectionListItems) { var featureSetIssue = IssueType.None; foreach (var item in allListItems) { if (featureSet.featureIds == null) break; if (Array.IndexOf(featureSet.featureIds, item.featureId) == -1) continue; if (item.issueType == IssueType.Error) { featureSetIssue = IssueType.Error; break; } if (item.issueType == IssueType.Warning) { featureSetIssue = IssueType.Warning; } } issuesPerFeatureSet.Add(featureSetIssue); } } IssueType GetValidationIssueType(OpenXRFeature feature) { IssueType ret = IssueType.None; foreach (var issue in _issues) { if (feature == issue.feature) { if (issue.error) { ret = IssueType.Error; break; } ret = IssueType.Warning; } } return ret; } void DrawInteractionList() { EditorGUILayout.Space(); if (interactionItems.Count == 0) return; var iconSize = EditorGUIUtility.GetIconSize(); EditorGUILayout.BeginVertical(); EditorGUILayout.Space(); EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); EditorGUIUtility.SetIconSize(new Vector2(30, 30)); GUILayout.Label(EditorGUIUtility.IconContent("console.infoicon"), new GUIStyle(EditorStyles.label)); EditorGUIUtility.SetIconSize(iconSize); GUILayout.Label("Only enable interaction profiles that you actually test, to ensure their input bindings are complete. Otherwise, disable that interaction profile, to allow the OpenXR runtime to remap user input from a profile you do test.", EditorStyles.wordWrappedLabel); EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); interactionFeaturesList.DoLayoutList(); EditorGUILayout.EndVertical(); } void OnSelectItem(OpenXRFeatureSetManager.FeatureSetInfo selectedItem) { this.selectedItem = selectedItem; int selectedItemIndex = selectionListItems.IndexOf(selectedItem); if (lastSelectedItemIndex.ContainsKey(activeBuildTarget)) lastSelectedItemIndex[activeBuildTarget] = selectedItemIndex; else lastSelectedItemIndex.Add(activeBuildTarget, selectedItemIndex); if (this.selectedItem != null) { if (String.IsNullOrEmpty(selectedItem.featureSetId)) { filteredListItems = allListItems. OrderBy((item) => item.uiName.text). ToList(); } else { filteredListItems = allListItems. Where((item) => Array.IndexOf(selectedItem.featureIds, item.featureId) > -1). OrderBy((item) => item.uiName.text). ToList(); } } } void DrawSelectionList() { var skin = EditorGUIUtility.GetBuiltinSkin(EditorSkin.Inspector); var lineHeight = EditorGUIUtility.singleLineHeight * Styles.k_DefualtLineMultiplier; EditorGUILayout.BeginVertical(GUILayout.Width(Styles.k_DefaultSelectionWidth), GUILayout.ExpandWidth(true)); { EditorGUILayout.LabelField("OpenXR Feature Groups", Styles.s_FeatureSetTitleLable); EditorGUILayout.BeginVertical(Styles.s_SelectionBackground, GUILayout.ExpandHeight(true)); { int index = 0; foreach (var item in selectionListItems) { var typeOfIssues = issuesPerFeatureSet[index++]; var selected = (item == this.selectedItem); var style = selected ? Styles.s_ListSelectedLabel : Styles.s_ListLabel; bool disabled = item.uiName.text != "All" && item.featureIds == null; EditorGUILayout.BeginHorizontal(style, GUILayout.ExpandWidth(true)); { EditorGUI.BeginDisabledGroup(disabled); if (String.Compare(item.uiName.text, s_AllFeatures, true) != 0) { var currentToggleState = item.isEnabled; var newToggleState = EditorGUILayout.ToggleLeft("", currentToggleState, GUILayout.ExpandWidth(false), GUILayout.Width(Styles.k_IconWidth), GUILayout.Height(lineHeight)); if (newToggleState != currentToggleState) { item.isEnabled = newToggleState; OpenXRFeatureSetManager.SetFeaturesFromEnabledFeatureSets(activeBuildTarget); } } if (GUILayout.Button(item.uiName, Styles.s_ListLabel, GUILayout.ExpandWidth(true), GUILayout.Height(lineHeight))) { OnSelectItem(item); } EditorGUI.EndDisabledGroup(); if (disabled && item.helpIcon != null) { if (GUILayout.Button(item.helpIcon, EditorStyles.label, GUILayout.Width(Styles.k_IconWidth), GUILayout.Height(lineHeight))) { UnityEngine.Application.OpenURL(item.downloadLink); } } if (typeOfIssues != IssueType.None) { GUIContent icon = (typeOfIssues == IssueType.Error) ? CommonContent.k_ValidationErrorIcon : CommonContent.k_ValidationWarningIcon; if (GUILayout.Button(icon, EditorStyles.label, GUILayout.Width(Styles.k_IconWidth), GUILayout.Height(lineHeight))) { OpenXRProjectValidationRulesSetup.ShowWindow(activeBuildTarget); } } EditorGUILayout.EndHorizontal(); } } EditorGUILayout.EndVertical(); } EditorGUILayout.EndVertical(); } } void DrawFeatureList() { EditorGUILayout.BeginVertical(); { EditorGUILayout.LabelField("", Styles.s_FeatureSetTitleLable); foreach (var filteredListItem in filteredListItems) { EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(false)); { EditorGUILayout.BeginVertical(Styles.s_Feature, GUILayout.ExpandWidth(false)); { EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(false)); { var typeOfIssue = filteredListItem.issueType; var featureNameSize = EditorStyles.toggle.CalcSize(filteredListItem.uiName); var oldEnabledState = filteredListItem.feature.enabled; EditorGUI.BeginDisabledGroup(requiredFeatures.Contains(filteredListItem.featureId)); filteredListItem.feature.enabled = EditorGUILayout.ToggleLeft(filteredListItem.uiName, filteredListItem.feature.enabled, GUILayout.ExpandWidth(false), GUILayout.Width(featureNameSize.x)); EditorGUI.EndDisabledGroup(); if (!String.IsNullOrEmpty(filteredListItem.documentationLink)) { if (GUILayout.Button(filteredListItem.documentationIcon, EditorStyles.label, GUILayout.Width(Styles.k_IconWidth))) { UnityEngine.Application.OpenURL(filteredListItem.documentationLink); } } if (typeOfIssue != IssueType.None) { GUIContent icon = (typeOfIssue == IssueType.Error) ? CommonContent.k_ValidationErrorIcon : CommonContent.k_ValidationWarningIcon; if (GUILayout.Button(icon, EditorStyles.label, GUILayout.Width(Styles.k_IconWidth))) { OpenXRProjectValidationRulesSetup.ShowWindow(activeBuildTarget); } } if (filteredListItem.shouldDisplaySettings) { if (GUILayout.Button(Content.k_Settings, Styles.s_FeatureSettings, GUILayout.ExpandWidth(true))) { if (featureSetSettingsEditor == null) { if (EditorWindow.HasOpenInstances()) { featureSetSettingsEditor = EditorWindow.GetWindow(); } else { featureSetSettingsEditor = ScriptableObject.CreateInstance() as OpenXRFeatureSettingsEditor; } } if (featureSetSettingsEditor != null) { featureSetSettingsEditor.ActiveItem = filteredListItem.featureId; featureSetSettingsEditor.ActiveBuildTarget = activeBuildTarget; featureSetSettingsEditor.ShowUtility(); featureSetSettingsEditor.Focus(); } } } EditorGUILayout.EndHorizontal(); } EditorGUILayout.Space(); EditorGUILayout.EndVertical(); } EditorGUILayout.EndHorizontal(); } } EditorGUILayout.EndVertical(); } } void DrawFeatureSetUI() { EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); DrawSelectionList(); DrawFeatureList(); EditorGUILayout.EndHorizontal(); } public void OnGUI(BuildTargetGroup buildTargetGroup) { InitStyles(); Vector2 iconSize = EditorGUIUtility.GetIconSize(); EditorGUIUtility.SetIconSize(new Vector2(Styles.k_IconWidth, Styles.k_IconWidth)); if (buildTargetGroup != activeBuildTarget || mustInitializeFeatures) { var allFeatureInfo = InitializeFeatures(buildTargetGroup); OpenXRFeatureSetManager.activeBuildTarget = buildTargetGroup; OpenXRFeatureSetManager.SetFeaturesFromEnabledFeatureSets(buildTargetGroup, allFeatureInfo); // This must be done after SetFeaturesFromEnabledFeatureSets to ensure we dont get an infinite update loop mustInitializeFeatures = false; if (EditorWindow.HasOpenInstances()) { featureSetSettingsEditor = EditorWindow.GetWindow(); } if (featureSetSettingsEditor != null) { featureSetSettingsEditor.ActiveBuildTarget = activeBuildTarget; } } if (allFeatureInfos != null) { UpdateValidationIssues(buildTargetGroup); DrawInteractionList(); EditorGUILayout.Space(); DrawFeatureSetUI(); } EditorGUIUtility.SetIconSize(iconSize); } bool HasSettingsToDisplay(OpenXRFeature feature) { FieldInfo[] fieldInfo = feature.GetType().GetFields(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance); foreach (var field in fieldInfo) { var nonSerializedAttrs = field.GetCustomAttributes(typeof(NonSerializedAttribute)); if (nonSerializedAttrs.Count() == 0) return true; } fieldInfo = feature.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Instance); foreach (var field in fieldInfo) { var serializedAttrs = field.GetCustomAttributes(typeof(SerializeField)); if (serializedAttrs.Count() > 0) return true; } return false; } FeatureHelpersInternal.AllFeatureInfo InitializeFeatures(BuildTargetGroup group) { selectionListItems.Clear(); filteredListItems.Clear(); allListItems.Clear(); interactionItems.Clear(); selectedFeatureIds.Clear(); requiredFeatures.Clear(); allFeatureInfos = FeatureHelpersInternal.GetAllFeatureInfo(group); activeBuildTarget = group; var featureSets = OpenXRFeatureSetManager.FeatureSetInfosForBuildTarget(group). OrderBy((fs) => fs.uiName.text); foreach (var featureSet in featureSets) { bool isKnownUninstalledFeatureSet = featureSet.featureIds == null && OpenXRFeatureSetManager.IsKnownFeatureSet(activeBuildTarget, featureSet.featureSetId); var featureSetFeatures = allFeatureInfos.Features. Where((f) => featureSet.featureIds != null && Array.IndexOf(featureSet.featureIds, f.Attribute.FeatureId) > -1); if (isKnownUninstalledFeatureSet || featureSetFeatures.Any()) { selectionListItems.Add(featureSet); if (featureSet.isEnabled && featureSet.requiredFeatureIds != null) requiredFeatures.UnionWith(featureSet.requiredFeatureIds); } } foreach (var _ext in allFeatureInfos.Features) { if (_ext.Attribute.Hidden) continue; var listItem = new ChildListItem() { uiName = new GUIContent(_ext.Attribute.UiName), documentationIcon = new GUIContent("", Content.k_HelpIcon.image, "Click for documentation"), categoryName = new GUIContent($"Category: {_ext.Category.ToString()}"), partner = new GUIContent($"Author: {_ext.Attribute.Company}"), version = new GUIContent($"Version: {_ext.Attribute.Version}"), partnerName = _ext.Attribute.Company, documentationLink = _ext.Attribute.InternalDocumentationLink, shouldDisplaySettings = HasSettingsToDisplay(_ext.Feature), feature = _ext.Feature, featureId = _ext.Attribute.FeatureId }; if (_ext.Attribute.Category == UnityEditor.XR.OpenXR.Features.FeatureCategory.Interaction) { interactionItems.Add(listItem.featureId, listItem); if (listItem.feature.enabled) { selectedFeatureIds.Add(listItem.featureId); } } else { allListItems.Add(listItem); } } selectionListItems.Add(new OpenXRFeatureSetManager.FeatureSetInfo() { uiName = new GUIContent(s_AllFeatures), featureSetId = string.Empty, featureIds = allFeatureInfos.Features.Select((e) => e.Attribute.FeatureId).ToArray(), }); var initialSelectedItem = selectionListItems[selectionListItems.Count - 1]; if (lastSelectedItemIndex.ContainsKey(activeBuildTarget)) { initialSelectedItem = selectionListItems[lastSelectedItemIndex[activeBuildTarget]]; } OnSelectItem(initialSelectedItem); return allFeatureInfos; } void InitStyles() { if (Styles.s_ListLabel == null) { Styles.s_FeatureSetTitleLable = new GUIStyle(EditorStyles.label); Styles.s_FeatureSetTitleLable.fontSize = 14; Styles.s_FeatureSetTitleLable.fontStyle = FontStyle.Bold; Styles.s_ListLabel = new GUIStyle(EditorStyles.label); Styles.s_ListLabel.border = new RectOffset(0, 0, 0, 0); Styles.s_ListLabel.padding = new RectOffset(5, 0, 0, 0); Styles.s_ListLabel.margin = new RectOffset(2, 2, 2, 2); Styles.s_ListSelectedLabel = new GUIStyle(Styles.s_SelectionStyle); Styles.s_ListSelectedLabel.border = Styles.s_ListLabel.border; Styles.s_ListSelectedLabel.padding = Styles.s_ListLabel.padding; Styles.s_ListSelectedLabel.margin = Styles.s_ListLabel.margin; Styles.s_ListLabelToggle = new GUIStyle(EditorStyles.toggle); Styles.s_ListLabelToggle.border = Styles.s_ListLabel.border; Styles.s_ListLabelToggle.padding = Styles.s_ListLabel.padding; Styles.s_ListLabelToggle.margin = Styles.s_ListLabel.margin; Styles.s_FeatureSettings = new GUIStyle(Styles.s_SelectionStyle); Styles.s_FeatureSettings.alignment = TextAnchor.MiddleRight; Styles.s_FeatureSettings.border = new RectOffset(2, 2, 0, 0); Styles.s_FeatureSettings.padding = new RectOffset(0, 2, 5, 0); Styles.s_Feature = new GUIStyle(Styles.s_SelectionStyle); Styles.s_Feature.border = new RectOffset(0, 0, 0, 0); Styles.s_Feature.padding = new RectOffset(5, 0, 0, 0); Styles.s_Feature.margin = new RectOffset(2, 2, 2, 2); } } public static OpenXRFeatureEditor CreateFeatureEditor() { if (OpenXRSettings.Instance == null) return null; if (TypeCache.GetTypesWithAttribute().Count > 0) return new OpenXRFeatureEditor(); return null; } internal void OnFeatureSetStateChanged(BuildTargetGroup buildTargetGroup) { if (activeBuildTarget != buildTargetGroup) return; mustInitializeFeatures = true; } } }