// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Utilities; using System; using System.Collections.Generic; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.UI { /// /// Logic for the App Bar. Generates buttons, manages states. /// [AddComponentMenu("Scripts/MRTK/SDK/AppBar")] public class AppBar : MonoBehaviour { private const float backgroundBarMoveSpeed = 5; #region Enum Definitions [Flags] public enum ButtonTypeEnum { Custom = 0, Remove = 1, Adjust = 2, Hide = 4, Show = 8, Done = 16 } public enum AppBarDisplayTypeEnum { Manipulation, Standalone } public enum AppBarStateEnum { Default, Manipulation, Hidden } #endregion #region Private Serialized Fields with Public Properties [Header("Target Bounding Box")] [Tooltip("Object the app bar is controlling - This object must implement the IBoundsTargetProvider.")] [SerializeField] private MonoBehaviour boundingBox = null; /// /// Object the app bar is controlling - This object must implement the IBoundsTargetProvider. /// public MonoBehaviour Target { get { return boundingBox; } set { boundingBox = value; } } [Tooltip("The parent game object for the renderable objects in the app bar")] [SerializeField] private GameObject baseRenderer = null; /// /// The parent game object for the renderable objects in the AppBar /// public GameObject BaseRenderer { get => baseRenderer; set => baseRenderer = value; } [Tooltip("The parent transform for the button collection")] [SerializeField] private Transform buttonParent = null; /// /// The parent transform for the button collection /// public Transform ButtonParent { get => buttonParent; set => buttonParent = value; } [Tooltip("The background gameobject, scales to fill area behind buttons")] [SerializeField] private GameObject backgroundBar = null; /// /// The background gameobject, scales to fill area behind buttons /// public GameObject BackgroundBar { get => backgroundBar; set => backgroundBar = value; } [Header("States")] [Tooltip("The AppBar's display type; default is Manipulation")] [SerializeField] private AppBarDisplayTypeEnum displayType = AppBarDisplayTypeEnum.Manipulation; /// /// The AppBar's display type; default is Manipulation /// public AppBarDisplayTypeEnum DisplayType { get { return displayType; } set { displayType = value; } } [Tooltip("The AppBar's current state")] [SerializeField] private AppBarStateEnum state = AppBarStateEnum.Default; /// /// The AppBar's current state /// public AppBarStateEnum State { get { return state; } set { state = value; } } [Header("Default Button Options")] [Tooltip("Should the AppBar have a remove button")] [SerializeField] private bool useRemove = true; /// /// Should the AppBar have a remove button /// public bool UseRemove { get { return useRemove; } set { useRemove = value; } } [Tooltip("Should the AppBar have an adjust button")] [SerializeField] private bool useAdjust = true; /// /// Should the AppBar have an adjust button /// public bool UseAdjust { get { return useAdjust; } set { useAdjust = value; } } [Tooltip("Should the AppBar have a hide button")] [SerializeField] private bool useHide = true; /// /// Should the AppBar have a hide button /// public bool UseHide { get { return useHide; } set { useHide = value; } } [Header("Default Button Icons")] [Tooltip("The adjust button texture")] [SerializeField] private Texture adjustIcon = null; /// /// The adjust button texture /// public Texture AdjustIcon { get => adjustIcon; set => adjustIcon = value; } [Tooltip("The done button texture")] [SerializeField] private Texture doneIcon = null; /// /// The done button texture /// public Texture DoneIcon { get => doneIcon; set => doneIcon = value; } [Tooltip("The hide button texture")] [SerializeField] private Texture hideIcon = null; /// /// The hide button texture /// public Texture HideIcon { get => hideIcon; set => hideIcon = value; } [Tooltip("The Remove button texture")] [SerializeField] private Texture removeIcon = null; /// /// The remove button texture /// public Texture RemoveIcon { get => removeIcon; set => removeIcon = value; } [Tooltip("The show button texture")] [SerializeField] private Texture showIcon = null; /// /// The show button texture /// public Texture ShowIcon { get => showIcon; set => showIcon = value; } [Header("Scale & Position Options")] [SerializeField] [Tooltip("Uses an alternate follow style that works better for very oblong objects.")] private bool useTightFollow = false; /// /// Uses an alternate follow style that works better for very oblong objects /// public bool UseTightFollow { get { return useTightFollow; } set { useTightFollow = value; } } [SerializeField] [Tooltip("Where to display the app bar on the y axis. This can be set to negative values to force the app bar to appear below the object.")] private float hoverOffsetYScale = 0.25f; /// /// Where to display the app bar on the y axis /// This can be set to negative values /// to force the app bar to appear below the object /// public float HoverOffsetYScale { get { return hoverOffsetYScale; } set { hoverOffsetYScale = value; } } [SerializeField] [Tooltip("Pushes the app bar away from the object.")] private float hoverOffsetZ = 0f; /// /// Pushes the app bar away from the object /// public float HoverOffsetZ { get { return hoverOffsetZ; } set { hoverOffsetZ = value; } } [Tooltip("The button width for each button")] [SerializeField] private float buttonWidth = 0.032f; /// /// The button width for each button /// public float ButtonWidth { get => buttonWidth; set => buttonWidth = value; } [Tooltip("The button depth for each button")] [SerializeField] private float buttonDepth = 0.016f; /// /// The button depth for each button /// public float ButtonDepth { get => buttonDepth; set => buttonDepth = value; } #endregion private List buttons = new List(); private Vector3 targetBarSize = Vector3.one; private float lastTimeTapped = 0f; private float coolDownTime = 0.5f; private BoundingBoxHelper helper = new BoundingBoxHelper(); private List boundsPoints = new List(); #region MonoBehaviour Functions private void OnEnable() { InitializeButtons(); } private void LateUpdate() { UpdateAppBar(); } #endregion public void Reset() { State = AppBarStateEnum.Default; FollowTargetObject(false); lastTimeTapped = Time.time + coolDownTime; } public void OnButtonPressed(AppBarButton button) { if (Time.time < lastTimeTapped + coolDownTime) return; lastTimeTapped = Time.time; switch (button.ButtonType) { case ButtonTypeEnum.Remove: OnClickRemove(); break; case ButtonTypeEnum.Adjust: State = AppBarStateEnum.Manipulation; break; case ButtonTypeEnum.Hide: State = AppBarStateEnum.Hidden; break; case ButtonTypeEnum.Show: State = AppBarStateEnum.Default; break; case ButtonTypeEnum.Done: State = AppBarStateEnum.Default; break; default: break; } } protected virtual void OnClickRemove() { // Set the app bar and bounding box to inactive if (Target is IBoundsTargetProvider boundsProvider && !boundsProvider.IsNull()) { boundsProvider.Target.SetActive(false); } Target.gameObject.SetActive(false); gameObject.SetActive(false); } private void InitializeButtons() { buttons.Clear(); foreach (Transform child in ButtonParent) { AppBarButton appBarButton = child.GetComponent(); if (appBarButton == null) throw new Exception("Found a transform without an AppBarButton component under buttonTransforms!"); appBarButton.InitializeButtonContent(this); // Set to invisible initially if not custom switch (appBarButton.ButtonType) { case ButtonTypeEnum.Custom: break; default: appBarButton.SetVisible(false); break; } buttons.Add(appBarButton); } } private void UpdateAppBar() { UpdateButtons(); UpdateTargetObject(); FollowTargetObject(true); } private void UpdateButtons() { // First just count how many buttons are visible int activeButtonNum = 0; for (int i = 0; i < buttons.Count; i++) { AppBarButton button = buttons[i]; button.SetVisible(GetButtonVisible(button.ButtonType)); if (!buttons[i].Visible) { continue; } activeButtonNum++; } // Sort the buttons by display order buttons.Sort(delegate (AppBarButton b1, AppBarButton b2) { return b2.DisplayOrder.CompareTo(b1.DisplayOrder); }); // Use active button number to determine background size and offset float backgroundBarSize = ButtonWidth * activeButtonNum; Vector3 positionOffset = Vector3.right * ((backgroundBarSize / 2) - (ButtonWidth / 2)); // Go through them again, setting active as activeButtonNum = 0; for (int i = 0; i < buttons.Count; i++) { // Set the sibling index and target position so the button will behave predictably when set visible buttons[i].transform.SetSiblingIndex(i); buttons[i].SetTargetPosition((Vector3.left * ButtonWidth * activeButtonNum) + positionOffset); if (!buttons[i].Visible) continue; activeButtonNum++; } targetBarSize.x = backgroundBarSize; BackgroundBar.transform.localScale = Vector3.Lerp(BackgroundBar.transform.localScale, targetBarSize, Time.deltaTime * backgroundBarMoveSpeed); BackgroundBar.transform.localPosition = Vector3.forward * ButtonDepth / 2; } private void UpdateTargetObject() { if (!(Target is IBoundsTargetProvider boundsProvider) || boundsProvider.IsNull() || boundsProvider.Target == null) { bool isDisplayTypeNotManipulation = DisplayType != AppBarDisplayTypeEnum.Manipulation; if (BaseRenderer.activeSelf != isDisplayTypeNotManipulation) { BaseRenderer.SetActive(isDisplayTypeNotManipulation); } return; } // Target can't be updated in editor mode if (!Application.isPlaying) return; if (boundsProvider == null) return; switch (State) { case AppBarStateEnum.Manipulation: boundsProvider.Active = true; break; default: boundsProvider.Active = false; break; } } private void FollowTargetObject(bool smooth) { if (!(Target is IBoundsTargetProvider boundsProvider) || boundsProvider.IsNull()) { return; } // Calculate the best follow position Vector3 finalPosition = Vector3.zero; Vector3 headPosition = CameraCache.Main.transform.position; boundsPoints.Clear(); helper.UpdateNonAABoundsCornerPositions(boundsProvider.TargetBounds, boundsPoints); int followingFaceIndex = helper.GetIndexOfForwardFace(headPosition); Vector3 faceNormal = helper.GetFaceNormal(followingFaceIndex); // Finalize the new position finalPosition = helper.GetFaceBottomCentroid(followingFaceIndex) + (faceNormal * HoverOffsetZ); // Follow our bounding box transform.position = smooth ? Vector3.Lerp(transform.position, finalPosition, Time.deltaTime * backgroundBarMoveSpeed) : finalPosition; // Rotate on the y axis Vector3 direction = (boundsProvider.TargetBounds.bounds.center - finalPosition).normalized; if (direction != Vector3.zero) { Vector3 eulerAngles = Quaternion.LookRotation(direction, Vector3.up).eulerAngles; eulerAngles.x = 0f; eulerAngles.z = 0f; transform.eulerAngles = eulerAngles; } else { transform.eulerAngles = Vector3.zero; } } private bool GetButtonVisible(ButtonTypeEnum buttonType) { // Set visibility based on button type / options switch (buttonType) { default: break; case ButtonTypeEnum.Remove: if (!UseRemove) return false; break; case ButtonTypeEnum.Hide: if (!UseHide) return false; break; case ButtonTypeEnum.Adjust: if (!UseAdjust) return false; break; } switch (State) { case AppBarStateEnum.Default: default: switch (buttonType) { // Show hide, adjust, remove buttons // The rest are hidden case ButtonTypeEnum.Hide: case ButtonTypeEnum.Remove: case ButtonTypeEnum.Adjust: case ButtonTypeEnum.Custom: return true; default: return false; } case AppBarStateEnum.Hidden: switch (buttonType) { // Show the show button // The rest are hidden case ButtonTypeEnum.Show: return true; default: return false; } case AppBarStateEnum.Manipulation: switch (buttonType) { // Show done button // The rest are hidden case ButtonTypeEnum.Done: return true; default: return false; } } } public void GetButtonTextAndIconFromType(ButtonTypeEnum type, out string buttonText, out Texture buttonIcon, out int displayOrder) { switch (type) { case ButtonTypeEnum.Show: buttonText = "Show"; buttonIcon = ShowIcon; displayOrder = 0; break; case ButtonTypeEnum.Hide: buttonText = "Hide"; buttonIcon = HideIcon; displayOrder = 1; break; case ButtonTypeEnum.Adjust: buttonText = "Adjust"; buttonIcon = AdjustIcon; displayOrder = 2; break; case ButtonTypeEnum.Remove: buttonText = "Remove"; buttonIcon = RemoveIcon; displayOrder = 3; break; case ButtonTypeEnum.Done: buttonText = "Done"; buttonIcon = DoneIcon; displayOrder = 4; break; default: throw new ArgumentOutOfRangeException("type", type, null); } } } }