/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; internal class OVRProjectSetupDrawer { private class Styles { private const float SmallIconSize = 16.0f; private const float FixButtonWidth = 64.0f; private const float FixAllButtonWidth = 80.0f; internal const float GroupSelectionWidth = 244.0f; internal const float LabelWidth = 96f; internal const float TitleLabelWidth = 196f; internal readonly GUIStyle Wrap = new GUIStyle(EditorStyles.label) { wordWrap = true, alignment = TextAnchor.MiddleLeft, padding = new RectOffset(0, 5, 1, 1) }; internal readonly GUIStyle IssuesBackground = new GUIStyle("ScrollViewAlt") { }; internal readonly GUIStyle ListLabel = new GUIStyle("TV Selection") { border = new RectOffset(0, 0, 0, 0), padding = new RectOffset(5, 5, 5, 3), margin = new RectOffset(4, 4, 4, 5) }; internal readonly GUIStyle IssuesTitleLabel = new GUIStyle(EditorStyles.label) { fontSize = 14, wordWrap = false, stretchWidth = false, fontStyle = FontStyle.Bold, padding = new RectOffset(10, 10, 0, 0) }; internal readonly GUIStyle GenerateReportButton = new GUIStyle(EditorStyles.miniButton) { margin = new RectOffset(0, 10, 2, 2), stretchWidth = false, }; internal readonly GUIStyle FixButton = new GUIStyle(EditorStyles.miniButton) { margin = new RectOffset(0, 10, 2, 2), stretchWidth = false, fixedWidth = FixButtonWidth, }; internal readonly GUIStyle FixAllButton = new GUIStyle(EditorStyles.miniButton) { margin = new RectOffset(0, 10, 2, 2), stretchWidth = false, fixedWidth = FixAllButtonWidth, }; internal readonly GUIStyle InlinedIconStyle = new GUIStyle(EditorStyles.label) { margin = new RectOffset(0, 0, 0, 0), padding = new RectOffset(0, 0, 0, 0), fixedWidth = SmallIconSize, fixedHeight = SmallIconSize }; internal readonly GUIStyle IconStyle = new GUIStyle(EditorStyles.label) { margin = new RectOffset(5, 5, 4, 5), padding = new RectOffset(0, 0, 0, 0), fixedWidth = SmallIconSize, fixedHeight = SmallIconSize }; internal readonly GUIStyle SubtitleHelpText = new GUIStyle(EditorStyles.miniLabel) { margin = new RectOffset(10, 0, 0, 0), wordWrap = true }; internal readonly GUIStyle InternalHelpBox = new GUIStyle(EditorStyles.helpBox) { margin = new RectOffset(5, 5, 5, 5) }; internal readonly GUIStyle InternalHelpText = new GUIStyle(EditorStyles.miniLabel) { margin = new RectOffset(10, 0, 0, 0), wordWrap = true, fontStyle = FontStyle.Italic, normal = { textColor = new Color(0.58f, 0.72f, 0.95f) } }; internal readonly GUIStyle NormalStyle = new GUIStyle(EditorStyles.label) { margin = new RectOffset(10, 0, 0, 0), wordWrap = true, stretchWidth = false }; internal readonly GUIStyle BoldStyle = new GUIStyle(EditorStyles.label) { margin = new RectOffset(10, 0, 0, 0), stretchWidth = false, wordWrap = true, fontStyle = FontStyle.Bold }; internal readonly GUIStyle MiniButton = new GUIStyle(EditorStyles.miniButton) { clipping = TextClipping.Overflow, fixedHeight = 18.0f, fixedWidth = 18.0f, margin = new RectOffset(2, 2, 2, 2), padding = new RectOffset(1, 1, 1, 1) }; internal readonly GUIStyle Foldout = new GUIStyle(EditorStyles.foldoutHeader) { margin = new RectOffset(0, 0, 0, 0), padding = new RectOffset(16, 5, 5, 5), fixedHeight = 26.0f }; internal readonly GUIStyle FoldoutHorizontal = new GUIStyle(EditorStyles.label) { fixedHeight = 26.0f }; internal readonly GUIStyle List = new GUIStyle(EditorStyles.helpBox) { margin = new RectOffset(3, 3, 3, 3), padding = new RectOffset(3, 3, 3, 3) }; } private static Styles _styles; private static Styles styles => _styles ??= new Styles(); private readonly OVRProjectSetupSettingBool _showOutstandingItems = new OVRProjectSetupUserSettingBool("ShowOutstandingItems", true); private readonly OVRProjectSetupSettingBool _showRecommendedItems = new OVRProjectSetupUserSettingBool("ShowRecommendedItems", true); private readonly OVRProjectSetupSettingBool _showVerifiedItems = new OVRProjectSetupUserSettingBool("ShowVerifiedItems", false); private readonly OVRProjectSetupSettingBool _showIgnoredItems = new OVRProjectSetupUserSettingBool("ShowIgnoredItems", false); private static readonly GUIContent Title = new GUIContent("Project Setup Tool"); private static readonly GUIContent Description = new GUIContent("This tool maintains a checklist of required setup tasks as well as best practices to " + "ensure your project is ready to go. Follow our suggestions and fixes to quickly setup your project."); private static readonly GUIContent SummaryLabel = new GUIContent("Current project status: "); private static readonly GUIContent ListTitle = new GUIContent("Checklist"); private static readonly GUIContent UnsupportedTitle = new GUIContent("Unsupported Platform"); private static readonly GUIContent Filter = new GUIContent("Filter by Group :", "Filters the task to the selected group."); private static readonly GUIContent FixButtonContent = new GUIContent("Fix", "Fix with recommended settings"); private static readonly GUIContent FixAllButtonContent = new GUIContent("Fix All", "Fix all the issues from this category"); private static readonly GUIContent ApplyButtonContent = new GUIContent("Apply", "Apply the recommended settings"); private static readonly GUIContent ApplyAllButtonContent = new GUIContent("Apply All", "Apply the recommended settings for all the items in this category"); private static readonly GUIContent RefreshTasksButtonContent = new GUIContent("Refresh", "Refresh the items in the list"); private static readonly GUIContent GenerateReportButtonContent = new GUIContent("Generate report", "Generate a report of all the issues"); private static readonly OVRGUIContent WarningIcon = OVREditorUtils.CreateContent("ovr_icon_category_warning.png", OVRGUIContent.Source.ProjectSetupToolIcons); private static readonly OVRGUIContent ErrorIcon = OVREditorUtils.CreateContent("ovr_icon_category_error.png", OVRGUIContent.Source.ProjectSetupToolIcons); private static readonly OVRGUIContent InfoIcon = OVREditorUtils.CreateContent("ovr_icon_category_neutral.png", OVRGUIContent.Source.ProjectSetupToolIcons); private static readonly OVRGUIContent TestPassedIcon = OVREditorUtils.CreateContent("ovr_icon_category_success.png", OVRGUIContent.Source.ProjectSetupToolIcons); private static readonly OVRGUIContent ConfigIcon = OVREditorUtils.CreateContent("_Popup", OVRGUIContent.Source.BuiltIn, "Additional options"); private static readonly OVRGUIContent DocumentationIcon = OVREditorUtils.CreateContent("ovr_icon_documentation.png", OVRGUIContent.Source.GenericIcons, "Go to Documentation"); private const string OutstandingItems = "Outstanding Issues"; private const string RecommendedItems = "Recommended Items"; private const string VerifiedItems = "Verified Items"; private const string IgnoredItems = "Ignored Items"; private const string OutFolderTitle = "Select output folder"; private const string ErrorTitle = "Error"; private const string SuccessTitle = "Success"; private const string ReportGenerationErrorMessage = "Could not generate the project setup report."; private const string ReportGenerationSuccessMessage = "Project setup report generated successfully at:"; private const string TasksRefreshErrorMessage = "Could not refresh the checklist."; private const string TasksRefreshSuccessMessage = "Tasks refreshed successfully."; private const string OkButton = "ok"; private const string DocumentationUrl = "https://developer.oculus.com/documentation/unity/unity-upst-overview"; // Internals private OVRProjectSetup.TaskGroup _selectedTaskGroup; private BuildTargetGroup _selectedBuildTargetGroup = BuildTargetGroup.Unknown; private Vector2 _scrollViewPos = Vector2.zero; private OVRConfigurationTaskUpdaterSummary _lastSummary; internal OVRProjectSetupDrawer() { _selectedTaskGroup = OVRProjectSetup.TaskGroup.All; } private class BuildTargetSelectionScope : GUI.Scope { public BuildTargetGroup BuildTargetGroup { get; protected set; } public BuildTargetSelectionScope() { BuildTargetGroup = EditorGUILayout.BeginBuildTargetSelectionGrouping(); if (BuildTargetGroup == BuildTargetGroup.Unknown) { BuildTargetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget); } } protected override void CloseScope() => EditorGUILayout.EndVertical(); } private TEnumType EnumPopup(GUIContent content, TEnumType currentValue, Action onChanged) where TEnumType : Enum, IComparable { var previousLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = Styles.LabelWidth; TEnumType newValue = (TEnumType)EditorGUILayout.EnumPopup(content, currentValue, GUILayout.Width(Styles.GroupSelectionWidth)); EditorGUIUtility.labelWidth = previousLabelWidth; if (!newValue.Equals(currentValue)) { onChanged(newValue); } return newValue; } private bool FoldoutWithAdditionalAction(OVRProjectSetupSettingBool key, string label, Rect rect, Action inlineAdditionalAction) { var previousLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = rect.width - 8; bool foldout; using (new EditorGUILayout.HorizontalScope(styles.FoldoutHorizontal)) { foldout = Foldout(key, label); inlineAdditionalAction?.Invoke(); } EditorGUIUtility.labelWidth = previousLabelWidth; return foldout; } private bool Foldout(OVRProjectSetupSettingBool key, string label) { var currentValue = key.Value; var newValue = EditorGUILayout.Foldout(currentValue, label, true, styles.Foldout); if (newValue != currentValue) { key.Value = newValue; } return newValue; } private OVRGUIContent GetTaskIcon(OVRConfigurationTask task, BuildTargetGroup buildTargetGroup) { if (task.IsDone(buildTargetGroup)) { return TestPassedIcon; } return task.Level.GetValue(buildTargetGroup) switch { OVRProjectSetup.TaskLevel.Required => ErrorIcon, OVRProjectSetup.TaskLevel.Recommended => WarningIcon, OVRProjectSetup.TaskLevel.Optional => InfoIcon, _ => throw new ArgumentOutOfRangeException() }; } private string GenerateReport(BuildTargetGroup buildTargetGroup, string outputPath) { if (_lastSummary == null) { OVRProjectSetup.UpdateTasks(buildTargetGroup, logMessages: OVRProjectSetup.LogMessages.Disabled, blocking: true, onCompleted: processor => { var updater = processor as OVRConfigurationTaskUpdater; _lastSummary = updater?.Summary; }); return _lastSummary?.GenerateReport(outputPath); } return _lastSummary.GenerateReport(outputPath); } private void UpdateTasks(BuildTargetGroup buildTargetGroup) { OVRProjectSetup.UpdateTasks(buildTargetGroup, logMessages: OVRProjectSetup.LogMessages.Disabled, blocking: false, onCompleted: OnUpdated); } private void OnUpdated(OVRConfigurationTaskProcessor processor) { var updater = processor as OVRConfigurationTaskUpdater; _lastSummary = updater?.Summary; } private void ShowSettingsMenu() { var menu = new GenericMenu(); OVRProjectSetup.Enabled.AppendToMenu(menu); OVRProjectSetupUpdater.Enabled.AppendToMenu(menu); OVRProjectSetup.RequiredThrowErrors.AppendToMenu(menu); OVRProjectSetup.AllowLogs.AppendToMenu(menu); OVRProjectSetup.ShowStatusIcon.AppendToMenu(menu); OVRProjectSetup.ProduceReportOnBuild.AppendToMenu(menu); menu.ShowAsContext(); } private void ShowItemMenu(BuildTargetGroup buildTargetGroup, OVRConfigurationTask task) { var menu = new GenericMenu(); var hasDocumentation = !string.IsNullOrEmpty(task.URL.GetValue(buildTargetGroup)); if (hasDocumentation) { menu.AddItem(new GUIContent("Documentation"), false, OnDocumentation, new object[] { buildTargetGroup, task }); } var hasSourceCode = task.SourceCode.Valid; if (hasSourceCode) { menu.AddItem(new GUIContent("Go to Source Code"), false, OnGoToSourceCode, new object[] { buildTargetGroup, task }); } menu.AddItem(new GUIContent("Ignore"), task.IsIgnored(buildTargetGroup), OnIgnore, new object[] { buildTargetGroup, task }); menu.ShowAsContext(); } internal void OnTitleBarGUI() { if (GUILayout.Button(ConfigIcon, styles.MiniButton)) { ShowSettingsMenu(); } if (GUILayout.Button(DocumentationIcon, styles.MiniButton)) { Application.OpenURL(DocumentationUrl); } } internal void OnGUI() { // Title GUILayout.Label(Title, styles.IssuesTitleLabel); // Short Description GUILayout.Label(Description, styles.SubtitleHelpText); EditorGUILayout.Space(); var enabled = OVRProjectSetup.Enabled.Value; using (new EditorGUI.DisabledScope(!enabled)) { // Summary using (new EditorGUILayout.HorizontalScope()) { GUILayout.Label(SummaryLabel, styles.NormalStyle); if (enabled) { GUILayout.Label(OVRProjectSetupStatusIcon.ComputeIcon(_lastSummary), styles.InlinedIconStyle); GUILayout.Label(_lastSummary?.ComputeNoticeMessage() ?? "", styles.BoldStyle); } else { GUILayout.Label("Setup Tool is disabled", styles.BoldStyle); } } // Checklist using (var buildTargetSelection = new BuildTargetSelectionScope()) { var buildTargetGroup = buildTargetSelection.BuildTargetGroup; if (_selectedBuildTargetGroup != buildTargetGroup) { _selectedBuildTargetGroup = buildTargetGroup; UpdateTasks(buildTargetGroup); } using (new EditorGUILayout.VerticalScope()) { EditorGUILayout.Space(); DrawTasksList(_selectedBuildTargetGroup); } } } } private void DrawTasksList(BuildTargetGroup buildTargetGroup) { var disableTasksList = EditorApplication.isPlaying; using (new EditorGUI.DisabledGroupScope(disableTasksList)) { // Header using (new EditorGUILayout.HorizontalScope()) { // Title GUILayout.Label(ListTitle, styles.IssuesTitleLabel, GUILayout.Width(Styles.TitleLabelWidth)); GUILayout.FlexibleSpace(); // Filter EnumPopup(Filter, _selectedTaskGroup, group => _selectedTaskGroup = group); // More Actions Menu Button DrawMoreActionsMenuList(buildTargetGroup); } // Scroll View _scrollViewPos = EditorGUILayout.BeginScrollView(_scrollViewPos, styles.IssuesBackground, GUILayout.ExpandHeight(true)); DrawCategory(_showOutstandingItems, tasks => tasks .Where(task => (_selectedTaskGroup == OVRProjectSetup.TaskGroup.All || task.Group == _selectedTaskGroup) && !task.IsDone(buildTargetGroup) && !task.IsIgnored(buildTargetGroup) && (task.Level.GetValue(buildTargetGroup) == OVRProjectSetup.TaskLevel.Required)) .OrderByDescending(task => task.FixAction == null) .ToList(), buildTargetGroup, OutstandingItems, true); DrawCategory(_showRecommendedItems, tasks => tasks .Where(task => (_selectedTaskGroup == OVRProjectSetup.TaskGroup.All || task.Group == _selectedTaskGroup) && !task.IsDone(buildTargetGroup) && !task.IsIgnored(buildTargetGroup) && (task.Level.GetValue(buildTargetGroup) != OVRProjectSetup.TaskLevel.Required)) .OrderByDescending(task => task.Level.GetValue(buildTargetGroup)) .ThenBy(task => task.FixAction == null) .ToList(), buildTargetGroup, RecommendedItems, true); DrawCategory(_showVerifiedItems, tasks => tasks .Where(task => (_selectedTaskGroup == OVRProjectSetup.TaskGroup.All || task.Group == _selectedTaskGroup) && task.IsDone(buildTargetGroup) && !task.IsIgnored(buildTargetGroup)) .OrderByDescending(task => task.FixAction == null) .ThenBy(task => task.Level.GetValue(buildTargetGroup)) .ToList(), buildTargetGroup, VerifiedItems, false); DrawCategory(_showIgnoredItems, tasks => tasks .Where(task => (_selectedTaskGroup == OVRProjectSetup.TaskGroup.All || task.Group == _selectedTaskGroup) && task.IsIgnored(buildTargetGroup)) .OrderByDescending(task => task.Level.GetValue(buildTargetGroup)) .ThenBy(task => task.FixAction != null) .ToList(), buildTargetGroup, IgnoredItems, false); EditorGUILayout.EndScrollView(); } } private void DrawCategory(OVRProjectSetupSettingBool key, Func, List> filter, BuildTargetGroup buildTargetGroup, string title, bool fixAllButton) { var tasks = filter(OVRProjectSetup.GetTasks(buildTargetGroup, false)); if (key == null || tasks == null || tasks.Count == 0) { return; } using (var scope = new EditorGUILayout.VerticalScope(styles.List)) { var rect = scope.rect; // Foldout title = $"{title} ({tasks.Count})"; var foldout = FoldoutWithAdditionalAction(key, title, rect, () => { if (fixAllButton) { if (tasks.Any(task => task.FixAction != null)) { var content = tasks[0].Level.GetValue(buildTargetGroup) == OVRProjectSetup.TaskLevel.Required ? FixAllButtonContent : ApplyAllButtonContent; EditorGUI.BeginDisabledGroup( OVRProjectSetup.ProcessorQueue.BusyWith(OVRConfigurationTaskProcessor.ProcessorType.Fixer)); if (GUILayout.Button(content, styles.FixAllButton)) { OVRProjectSetupSettingsProvider.SetNewInteraction(OVRProjectSetupSettingsProvider .Interaction.Fixed); OVRProjectSetup.FixTasks(buildTargetGroup, filter, blocking: false, onCompleted: AfterFixApply); } EditorGUI.EndDisabledGroup(); } } }); if (foldout) { DrawIssues(tasks, buildTargetGroup); } } } private void AfterFixApply(OVRConfigurationTaskProcessor processor) { AssetDatabase.SaveAssets(); UpdateTasks(processor.BuildTargetGroup); } private void DrawIssues(List tasks, BuildTargetGroup buildTargetGroup) { foreach (var task in tasks) { DrawIssue(task, buildTargetGroup); } } private void DrawIssue(OVRConfigurationTask task, BuildTargetGroup buildTargetGroup) { var ignored = task.IsIgnored(buildTargetGroup); var cannotBeFixed = task.IsDone(buildTargetGroup) || OVRProjectSetup.ProcessorQueue.BusyWith(OVRConfigurationTaskProcessor.ProcessorType.Fixer); var disabled = cannotBeFixed || ignored; // Note : We're not using scopes, because in this very case, we've got a cross of scopes EditorGUI.BeginDisabledGroup(disabled); var clickArea = EditorGUILayout.BeginHorizontal(styles.ListLabel); // Icon GUILayout.Label(GetTaskIcon(task, buildTargetGroup), styles.IconStyle); // Message GUILayout.Label(new GUIContent(task.Message.GetValue(buildTargetGroup)), styles.Wrap); EditorGUI.EndDisabledGroup(); if (task.FixAction != null) { EditorGUI.BeginDisabledGroup(cannotBeFixed); var content = task.Level.GetValue(buildTargetGroup) == OVRProjectSetup.TaskLevel.Required ? FixButtonContent : ApplyButtonContent; var fixMessage = task.FixMessage.GetValue(buildTargetGroup); var tooltip = fixMessage != null ? $"{content.tooltip} :\n{fixMessage}" : content.tooltip; content = new GUIContent(content.text, tooltip); if (GUILayout.Button(content, styles.FixButton)) { OVRProjectSetupSettingsProvider.SetNewInteraction(OVRProjectSetupSettingsProvider.Interaction.Fixed); OVRProjectSetup.FixTask(buildTargetGroup, task, blocking: false, onCompleted: AfterFixApply); } EditorGUI.EndDisabledGroup(); } var current = Event.current; if (GUILayout.Button("", EditorStyles.foldoutHeaderIcon, GUILayout.Width(16.0f)) || (clickArea.Contains(current.mousePosition) && current.type == EventType.ContextClick)) { ShowItemMenu(buildTargetGroup, task); if (current.type == EventType.ContextClick) { current.Use(); } } EditorGUILayout.EndHorizontal(); } private void DrawMoreActionsMenuList(BuildTargetGroup buildTargetGroup) { var current = Event.current; if (GUILayout.Button("", EditorStyles.foldoutHeaderIcon, GUILayout.Width(16.0f))) { var menu = new GenericMenu(); menu.AddItem(RefreshTasksButtonContent, false, OnRefresh, new object[] { buildTargetGroup }); menu.AddItem(GenerateReportButtonContent, false, OnGenerateReport, new object[] { buildTargetGroup }); menu.ShowAsContext(); if (current.type == EventType.ContextClick) { current.Use(); } } } private void ReadContextMenuArguments( object arg, out BuildTargetGroup buildTargetGroup, out OVRConfigurationTask task) { var args = arg as object[]; buildTargetGroup = args != null ? (BuildTargetGroup)args[0] : BuildTargetGroup.Unknown; task = args?[1] as OVRConfigurationTask; } private void OnIgnore(object args) { ReadContextMenuArguments(args, out var buildTargetGroup, out var task); var ignore = !task.IsIgnored(buildTargetGroup); if (ignore) { OVRProjectSetupSettingsProvider.SetNewInteraction(OVRProjectSetupSettingsProvider.Interaction.Ignored); } task?.SetIgnored(buildTargetGroup, ignore); } private void OnDocumentation(object args) { OVRProjectSetupSettingsProvider.SetNewInteraction(OVRProjectSetupSettingsProvider.Interaction .WentToDocumentation); ReadContextMenuArguments(args, out var buildTargetGroup, out var task); var url = task?.URL.GetValue(buildTargetGroup); Application.OpenURL(url); } private void OnGoToSourceCode(object args) { OVRProjectSetupSettingsProvider.SetNewInteraction(OVRProjectSetupSettingsProvider.Interaction.WentToSource); ReadContextMenuArguments(args, out var buildTargetGroup, out var task); task?.SourceCode.Open(); } private void OnGenerateReport(object arg) { var buildTargetGroup = arg is object[] args ? (BuildTargetGroup)args[0] : BuildTargetGroup.Unknown; var path = EditorUtility.OpenFolderPanel(OutFolderTitle, "", ""); if (string.IsNullOrEmpty(path)) return; try { var reportFileName = GenerateReport(buildTargetGroup, path); EditorUtility.DisplayDialog(SuccessTitle, $"{ReportGenerationSuccessMessage}\n{reportFileName}", OkButton); } catch (Exception e) { EditorUtility.DisplayDialog(ErrorTitle, $"{ReportGenerationErrorMessage}\n{e.Message}", OkButton); } } private void OnRefresh(object arg) { var buildTargetGroup = arg is object[] args ? (BuildTargetGroup)args[0] : BuildTargetGroup.Unknown; try { OVRProjectSetup.UpdateTasks(buildTargetGroup, logMessages: OVRProjectSetup.LogMessages.Disabled, blocking: true); EditorUtility.DisplayDialog(SuccessTitle, TasksRefreshSuccessMessage, OkButton); } catch (Exception e) { EditorUtility.DisplayDialog(ErrorTitle, $"{TasksRefreshErrorMessage}\n{e.Message}", OkButton); } } }