/* * 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. */ #if UNITY_2021_2_OR_NEWER #define OVR_BB_DRAGANDDROP #endif using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace Meta.XR.BuildingBlocks.Editor { public class BuildingBlocksWindow : EditorWindow { private class Styles { internal const int IdealThumbnailWidth = 280; internal const float ThumbnailRatio = 1.8f; internal const int BlockMargin = 8; internal const int Border = 1; internal const int DescriptionHeight = 48; internal const int Padding = 4; internal const int TightPadding = 2; #if OVR_BB_DRAGANDDROP internal const float DragOpacity = 0.5f; internal static readonly Color DragColor = new Color(1.0f, 1.0f, 1.0f, DragOpacity); #endif // OVR_BB_DRAGANDDROP internal const float DisabledTint = 0.6f; internal static readonly Color DisabledColor = new Color(DisabledTint, DisabledTint, DisabledTint, 1.0f); internal readonly GUIStyle GridItemStyle = new GUIStyle() { margin = new RectOffset(BlockMargin, BlockMargin, BlockMargin, BlockMargin), padding = new RectOffset(Border, Border, Border, Border), stretchWidth = false, stretchHeight = false, normal = { background = OVREditorUtils.MakeTexture(1, 1, OVREditorUtils.HexToColor("#1d1d1d")) }, hover = { background = OVREditorUtils.MakeTexture(1, 1, OVREditorUtils.HexToColor("#6d6d6d")) } }; internal readonly GUIStyle GridItemDisabledStyle = new GUIStyle() { margin = new RectOffset(BlockMargin, BlockMargin, BlockMargin, BlockMargin), padding = new RectOffset(Border, Border, Border, Border), stretchWidth = false, stretchHeight = false, normal = { background = OVREditorUtils.MakeTexture(1, 1, OVREditorUtils.HexToColor("#1d1d1d")) } }; internal readonly GUIStyle ThumbnailAreaStyle = new GUIStyle() { stretchHeight = false }; internal readonly GUIStyle SeparatorAreaStyle = new GUIStyle() { fixedHeight = Border, stretchHeight = false, normal = { background = OVREditorUtils.MakeTexture(1, 1, OVREditorUtils.HexToColor("#363636")) } }; internal readonly GUIStyle DescriptionAreaStyle = new GUIStyle() { stretchHeight = false, fixedHeight = Styles.DescriptionHeight, padding = new RectOffset(Padding, Padding, Padding, Padding), margin = new RectOffset(0, 0, 0, Border), normal = { background = OVREditorUtils.MakeTexture(1, 1, OVREditorUtils.HexToColor("#3e3e3e")) } }; internal readonly GUIStyle EmptyAreaStyle = new GUIStyle() { stretchHeight = true, fixedWidth = 0, fixedHeight = Styles.DescriptionHeight, padding = new RectOffset(0, 0, 0, 0), margin = new RectOffset(0, 0, 0, 0), }; internal readonly GUIStyle DescriptionAreaHoverStyle = new GUIStyle() { stretchHeight = false, fixedHeight = Styles.DescriptionHeight, padding = new RectOffset(Padding, Padding, Padding, Padding), margin = new RectOffset(0, 0, 0, Border), normal = { background = OVREditorUtils.MakeTexture(1, 1, OVREditorUtils.HexToColor("#4e4e4e")) } }; internal readonly GUIStyle BoldLabel = new GUIStyle(EditorStyles.boldLabel) { stretchHeight = true, fixedHeight = 32, fontSize = 16, normal = { textColor = Color.white }, hover = { textColor = Color.white }, alignment = TextAnchor.MiddleLeft }; 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 LargeButton = new GUIStyle(EditorStyles.miniButton) { clipping = TextClipping.Overflow, fixedHeight = DescriptionHeight - Padding * 2, fixedWidth = DescriptionHeight - Padding * 2, margin = new RectOffset(2, 2, 0, 2), padding = new RectOffset(Padding, Padding, Padding, Padding) }; 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 Header = new GUIStyle(EditorStyles.miniLabel) { fontSize = 12, fixedHeight = 32 + BlockMargin * 2, padding = new RectOffset(BlockMargin, BlockMargin, BlockMargin, BlockMargin), margin = new RectOffset(0, 0, 0, 0), wordWrap = true, normal = { background = OVREditorUtils.MakeTexture(1, 1, OVREditorUtils.HexToColor("#3e3e3e")) } }; internal readonly GUIStyle HeaderIconStyle = new GUIStyle() { fixedHeight = 32.0f, fixedWidth = 32.0f, stretchWidth = false, alignment = TextAnchor.MiddleCenter }; internal readonly GUIStyle SubtitleHelpText = new GUIStyle(EditorStyles.miniLabel) { fontSize = 12, margin = new RectOffset(BlockMargin, BlockMargin, BlockMargin, BlockMargin), wordWrap = true }; internal readonly GUIStyle InternalHelpBox = new GUIStyle(EditorStyles.helpBox) { margin = new RectOffset(BlockMargin, BlockMargin, BlockMargin, BlockMargin) }; 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 LabelStyle = new GUIStyle(EditorStyles.boldLabel); internal readonly GUIStyle LabelHoverStyle = new GUIStyle(EditorStyles.boldLabel) { normal = { textColor = Color.white } }; internal readonly GUIStyle SubtitleStyle = new GUIStyle(EditorStyles.label) { fontStyle = FontStyle.Italic, normal = { textColor = Color.gray } }; internal readonly GUIStyle InfoStyle = new GUIStyle(EditorStyles.label) { fontSize = 10, normal = { textColor = Color.gray } }; private static Texture2D _infoExperimentalBgTexture; private static Texture2D InfoExperimentalBgTexture { get { if (_infoExperimentalBgTexture != null) { return _infoExperimentalBgTexture; } _infoExperimentalBgTexture = new Texture2D(1, 1); _infoExperimentalBgTexture.SetPixel(0, 0, new Color(0.22f, 0.22f, 0.22f, 0.25f)); _infoExperimentalBgTexture.Apply(); return _infoExperimentalBgTexture; } } internal readonly GUIStyle ExperimentalAreaStyle = new GUIStyle() { margin = new RectOffset(Padding, Padding, Padding, Padding), padding = new RectOffset(Padding, TightPadding, TightPadding, Padding), fixedHeight = 18, normal = { background = OVREditorUtils.MakeTexture(1, 1, OVREditorUtils.HexToColor("#3e3e3eAA")) } }; internal readonly GUIStyle ExperimentalTextStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 10, normal = { textColor = ExperimentalColor }, wordWrap = true }; internal readonly OVRGUIContent AddIcon = OVREditorUtils.CreateContent("ovr_icon_addblock.png", OVRGUIContent.Source.BuildingBlocksIcons, "Add Block to current scene"); internal readonly OVRGUIContent ExperimentalIcon = OVREditorUtils.CreateContent("ovr_icon_experimental.png", OVRGUIContent.Source.BuildingBlocksIcons, "Experimental"); internal readonly OVRGUIContent DownloadIcon = OVREditorUtils.CreateContent("ovr_icon_download.png", OVRGUIContent.Source.BuildingBlocksIcons, "Download Block to your project"); internal readonly OVRGUIContent SelectIcon = OVREditorUtils.CreateContent("ovr_icon_link.png", OVRGUIContent.Source.BuildingBlocksIcons, "Select Block in current scene"); internal readonly OVRGUIContent ConfigIcon = OVREditorUtils.CreateContent("_Popup", OVRGUIContent.Source.BuiltIn, "Additional options"); internal readonly OVRGUIContent DocumentationIcon = OVREditorUtils.CreateContent("ovr_icon_documentation.png", OVRGUIContent.Source.GenericIcons, "Go to Documentation"); internal readonly OVRGUIContent HeaderIcon = OVREditorUtils.CreateContent("ovr_icon_bbw.png", OVRGUIContent.Source.BuildingBlocksIcons, null); internal static readonly Color AccentColor = OVREditorUtils.HexToColor("#a29de5"); internal static readonly Color ExperimentalColor = OVREditorUtils.HexToColor("#eba333"); } private static Styles _styles; private static Styles styles => _styles ??= new Styles(); private const string MenuPath = "Oculus/Tools/Building Blocks"; private const int MenuPriority = 2; private static readonly string WindowName = Utils.BlocksPublicName; private const string AddButtonLabel = "Add"; #if OVR_BB_DRAGANDDROP private const string DragAndDropLabel = "Dragging Block"; private const string DragAndDropBlockDataLabel = "block"; private const string DragAndDropBlockThumbnailLabel = "blockThumbnail"; #endif // OVR_BB_DRAGANDDROP private static readonly GUIContent Title = new GUIContent(Utils.BlocksPublicName); private static readonly GUIContent Description = new GUIContent("Drag and drop blocks onto your scene to add XR features to your project."); private const string DocumentationUrl = "https://developer.oculus.com/documentation/unity/unity-buildingblocks-overview"; private Vector2 _scrollPosition; private OVRAnimatedContent _outline = null; private OVRAnimatedContent _tutorial = null; private static readonly OVRProjectSetupSettingBool _tutorialCompleted = new OVRProjectSetupUserSettingBool("BBTutorialCompleted", false); private static bool _shouldShowTutorial = false; [MenuItem(MenuPath, false, MenuPriority)] private static void ShowWindow() { ShowWindow("MenuItem"); } public static void ShowWindow(string source) { GetWindow(WindowName); OVRTelemetry.Start(OVRTelemetryConstants.BB.MarkerId.OpenWindow) .AddAnnotation(OVRTelemetryConstants.BB.AnnotationType.ActionTrigger, source) .Send(); } private void OnGUI() { OnHeaderGUI(); var windowWidth = position.width - Styles.BlockMargin; windowWidth = Mathf.Max(Styles.IdealThumbnailWidth + Styles.Padding * 3, windowWidth); var scrollableAreaWidth = windowWidth - 18; var blockWidth = Styles.IdealThumbnailWidth; var numberOfColumns = Mathf.FloorToInt(scrollableAreaWidth / blockWidth); if (numberOfColumns < 1) numberOfColumns = 1; var marginToRemove = numberOfColumns * Styles.BlockMargin; var expectedThumbnailWidth = Mathf.FloorToInt((scrollableAreaWidth - marginToRemove) / numberOfColumns); var expectedThumbnailHeight = Mathf.FloorToInt(expectedThumbnailWidth / Styles.ThumbnailRatio); ShowList(_blockList, numberOfColumns, expectedThumbnailWidth, expectedThumbnailHeight, windowWidth); #if OVR_BB_DRAGANDDROP RefreshDragAndDrop(expectedThumbnailWidth, expectedThumbnailHeight); #endif // OVR_BB_DRAGANDDROP if (Event.current.type == EventType.MouseMove) { Repaint(); } } private void OnHeaderGUI() { EditorGUILayout.BeginHorizontal(styles.Header); { using (new OVREditorUtils.OVRGUIColorScope(OVREditorUtils.OVRGUIColorScope.Scope.Content, Styles.AccentColor)) { EditorGUILayout.LabelField(styles.HeaderIcon, styles.HeaderIconStyle, GUILayout.Width(32.0f), GUILayout.ExpandWidth(false)); } EditorGUILayout.LabelField(Title, styles.BoldLabel); EditorGUILayout.Space(0, true); if (GUILayout.Button(styles.ConfigIcon, styles.MiniButton)) { } if (GUILayout.Button(styles.DocumentationIcon, styles.MiniButton)) { Application.OpenURL(DocumentationUrl); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(styles.SubtitleHelpText); GUILayout.Label(Description, styles.LabelStyle); EditorGUILayout.EndHorizontal(); } private void OnEnable() { RefreshBlockList(); #if OVR_BB_DRAGANDDROP DragAndDrop.AddDropHandler(SceneDropHandler); DragAndDrop.AddDropHandler(HierarchyDropHandler); #endif // OVR_BB_DRAGANDDROP wantsMouseMove = true; _shouldShowTutorial = ShouldShowTutorial(); } private void RefreshBlockList() { _blockList = GetList(); } private void OnDisable() { #if OVR_BB_DRAGANDDROP DragAndDrop.RemoveDropHandler(SceneDropHandler); DragAndDrop.RemoveDropHandler(HierarchyDropHandler); #endif // OVR_BB_DRAGANDDROP } private List _blockList; private static List GetList() { var blockGuids = AssetDatabase.FindAssets($"t:{nameof(BlockBaseData)}"); return blockGuids.Select(id => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(id))) .Where(obj => !string.IsNullOrEmpty(obj.name) && obj.DisplayOnContentTab) .OrderBy(block => block.Order) .ThenBy(block => block.BlockName) .ToList(); } private void ShowList(List blocks, int numberOfColumns, int expectedThumbnailWidth, int expectedThumbnailHeight, float expectedScrollWidth) { _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.Width(expectedScrollWidth)); var columnIndex = 0; var showTutorial = _shouldShowTutorial; GUILayout.BeginHorizontal(); foreach (var block in blocks) { var rect = Show(block, expectedThumbnailWidth, expectedThumbnailHeight); if (showTutorial && block.CanBeAdded) { ShowTutorial(rect); showTutorial = false; } columnIndex++; if (columnIndex >= numberOfColumns) { columnIndex = 0; GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); } } GUILayout.EndHorizontal(); EditorGUILayout.EndScrollView(); } private Rect Show(BlockBaseData block, int expectedThumbnailWidth, int expectedThumbnailHeight) { var blockData = block as BlockData; var canBeAdded = block.CanBeAdded; var numberInScene = blockData != null ? blockData.ComputeNumberOfBlocksInScene() : 0; var canBeSelected = numberInScene > 0; var previousColor = GUI.color; var expectedColor = canBeAdded ? Color.white : Styles.DisabledColor; GUI.color = expectedColor; var gridItemStyle = canBeAdded ? styles.GridItemStyle : styles.GridItemDisabledStyle; gridItemStyle.fixedWidth = expectedThumbnailWidth; var dragArea = EditorGUILayout.BeginVertical(gridItemStyle); var hover = canBeAdded && dragArea.Contains(Event.current.mousePosition); var thumbnailAreaStyle = styles.ThumbnailAreaStyle; thumbnailAreaStyle.fixedHeight = expectedThumbnailHeight; var thumbnailArea = EditorGUILayout.BeginVertical(thumbnailAreaStyle); { GUI.DrawTexture(thumbnailArea, block.Thumbnail, ScaleMode.ScaleAndCrop); if (block.Experimental) { const string experimentalStr = "Experimental"; var width = styles.ExperimentalTextStyle.CalcSize(new GUIContent(experimentalStr)).x; var height = styles.ExperimentalAreaStyle.fixedHeight - Styles.TightPadding * 2.0f; EditorGUILayout.BeginHorizontal(styles.ExperimentalAreaStyle, GUILayout.Width(height + width), GUILayout.Height(height)); EditorGUILayout.LabelField(styles.ExperimentalIcon, styles.ExperimentalTextStyle, GUILayout.Width(height), GUILayout.Height(height)); EditorGUILayout.LabelField(experimentalStr, styles.ExperimentalTextStyle, GUILayout.Width(width), GUILayout.Height(height)); EditorGUILayout.EndHorizontal(); } else { // This space fills the area, otherwise the area will have a height of null // despite the fixedHeight set EditorGUILayout.Space(); } } EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(styles.SeparatorAreaStyle); { // This space fills the area, otherwise the area will have a height of null // despite the fixedHeight set EditorGUILayout.Space(); } EditorGUILayout.EndVertical(); var descriptionStyle = hover ? styles.DescriptionAreaHoverStyle : styles.DescriptionAreaStyle; EditorGUILayout.BeginVertical(descriptionStyle); EditorGUILayout.BeginHorizontal(); var numberOfIcons = 0; if (canBeAdded) numberOfIcons++; if (canBeSelected) numberOfIcons++; var iconWidth = styles.LargeButton.fixedWidth + styles.LargeButton.margin.horizontal; var padding = descriptionStyle.padding.horizontal; var style = new GUIStyle(styles.EmptyAreaStyle); style.fixedWidth = expectedThumbnailWidth - padding - numberOfIcons * iconWidth; EditorGUILayout.BeginVertical(style); EditorGUILayout.BeginHorizontal(); var labelStyle = hover ? styles.LabelHoverStyle : styles.LabelStyle; var labelContent = new GUIContent(block.BlockName); EditorGUILayout.LabelField(block.BlockName, labelStyle, GUILayout.Width(labelStyle.CalcSize(labelContent).x)); labelStyle = styles.SubtitleStyle; labelContent = new GUIContent(block.Sdk.ToString()); EditorGUILayout.LabelField(block.Sdk.ToString(), styles.SubtitleStyle, GUILayout.Width(labelStyle.CalcSize(labelContent).x)); EditorGUILayout.EndHorizontal(); var info = numberInScene > 0 ? $"{numberInScene} {OVREditorUtils.ChoosePlural(numberInScene, "instance", "instances")} in current scene" : block.Description; EditorGUILayout.LabelField(info, styles.InfoStyle); EditorGUILayout.EndVertical(); if (canBeAdded) { var addIcon = styles.AddIcon; if (ShowLargeButton(addIcon)) { block.AddToProject(block.RequireListRefreshAfterInstall ? RefreshBlockList : (Action)null); } } if (canBeSelected) { if (ShowLargeButton(styles.SelectIcon)) { blockData.SelectBlockInScene(); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical(); #if OVR_BB_DRAGANDDROP if (canBeAdded) { EditorGUIUtility.AddCursorRect(dragArea, MouseCursor.Pan); if (dragArea.Contains(Event.current.mousePosition) && Event.current.type == EventType.MouseDown) { SetDragAndDrop(block); } } #endif // OVR_BB_DRAGANDDROP GUI.color = previousColor; return dragArea; } private bool ShouldShowTutorial() { _shouldShowTutorial = !_tutorialCompleted.Value; if (_shouldShowTutorial) { // Make sure the scene doesn't have a non block version of the OVRCameraRig _shouldShowTutorial = !BlockData.HasNonBBCameraRig(); } return _shouldShowTutorial; } private void ShowTutorial(Rect dragArea) { if (_outline == null && OVRGUIContent.BuildPath("bb_outline.asset", OVRGUIContent.Source.BuildingBlocksAnimations, out var outlinePath)) { _outline = AssetDatabase.LoadAssetAtPath(outlinePath); } if (_outline != null) { _outline.Update(); GUI.DrawTexture(dragArea, _outline.CurrentFrame); } if (_tutorial == null && OVRGUIContent.BuildPath("bb_tutorial.asset", OVRGUIContent.Source.BuildingBlocksAnimations, out var tutorialPath)) { _tutorial = AssetDatabase.LoadAssetAtPath(tutorialPath); } if (_tutorial != null) { _tutorial.Update(); GUI.DrawTexture(dragArea, _tutorial.CurrentFrame); } Repaint(); } private bool ShowLargeButton(GUIContent icon) { var previousColor = GUI.color; GUI.color = Color.white; var hit = GUILayout.Button(icon, styles.LargeButton); GUI.color = previousColor; EditorGUIUtility.AddCursorRect(GUILayoutUtility.GetLastRect(), MouseCursor.Link); return hit; } private static void OnAdd(BlockBaseData block) { block.AddToProject(); } #if OVR_BB_DRAGANDDROP private void RefreshDragAndDrop(int expectedThumbnailWidth, int expectedThumbnailHeight) { var blockThumbnail = DragAndDrop.GetGenericData(DragAndDropBlockThumbnailLabel) as Texture2D; if (blockThumbnail) { var cursorOffset = new Vector2(expectedThumbnailWidth / 2.0f, expectedThumbnailHeight / 2.0f); var cursorRect = new Rect(Event.current.mousePosition - cursorOffset, new Vector2(expectedThumbnailWidth, expectedThumbnailHeight)); GUI.color = new Color(1, 1, 1, Styles.DragOpacity); GUI.DrawTexture(cursorRect, blockThumbnail, ScaleMode.ScaleAndCrop); GUI.color = Color.white; // Enforce a repaint next frame, as we need to move this thumbnail everyframe Repaint(); } if (Event.current.type == EventType.DragExited) { ResetDragThumbnail(); } if (Event.current.type == EventType.DragUpdated) { DragAndDrop.visualMode = DragAndDropVisualMode.Move; } } private static DragAndDropVisualMode HierarchyDropHandler( int dropTargetInstanceID, HierarchyDropFlags dropMode, Transform parentForDraggedObjects, bool perform) => DropHandler(perform); private static DragAndDropVisualMode SceneDropHandler( UnityEngine.Object dropUpon, Vector3 worldPosition, Vector2 viewportPosition, Transform parentForDraggedObjects, bool perform) => DropHandler(perform); private static DragAndDropVisualMode DropHandler(bool perform) { var block = DragAndDrop.GetGenericData(DragAndDropBlockDataLabel) as BlockBaseData; if (block != null) { if (perform) { block.AddToProject(); ResetDragAndDrop(); _tutorialCompleted.Value = true; _shouldShowTutorial = false; } return DragAndDropVisualMode.Generic; } return DragAndDropVisualMode.None; } private static void SetDragAndDrop(BlockBaseData block) { DragAndDrop.PrepareStartDrag(); DragAndDrop.SetGenericData(DragAndDropBlockDataLabel, block); DragAndDrop.SetGenericData(DragAndDropBlockThumbnailLabel, block.Thumbnail); DragAndDrop.StartDrag(DragAndDropLabel); } private static void ResetDragThumbnail() { DragAndDrop.SetGenericData(DragAndDropBlockThumbnailLabel, null); } private static void ResetDragAndDrop() { DragAndDrop.SetGenericData(DragAndDropBlockDataLabel, null); } #endif // OVR_BB_DRAGANDDROP } }