// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; using Microsoft.MixedReality.Toolkit.Utilities.Editor; #endif namespace Microsoft.MixedReality.Toolkit.UI { /// /// Helper component that gathers the most commonly modified button elements in one place. /// [ExecuteAlways] [HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/ux-building-blocks/button#how-to-change-the-icon-and-text")] public partial class ButtonConfigHelper : MonoBehaviour { /// /// Modifies the main label text. /// public string MainLabelText { get { return mainLabelText != null ? mainLabelText.text : null; } set { if (mainLabelText == null) { Debug.LogWarning("No main label set in " + name + " - not setting main label text."); return; } mainLabelText.text = value; } } /// /// Modifies the 'See it / Say it' label text. /// public string SeeItSayItLabelText { get { return seeItSayItLabelText != null ? seeItSayItLabelText.text : null; } set { if (seeItSayItLabelText == null) { Debug.LogWarning("No see it / say it label set in " + name + " - not setting see it / say it label text."); return; } seeItSayItLabelText.text = value; } } /// /// Turns the see it / say it label object on / off. /// public bool SeeItSayItLabelEnabled { get { if (seeItSayItLabel == null) { return false; } return seeItSayItLabel.activeSelf; } set { if (seeItSayItLabel == null) { Debug.LogWarning("No see it / say it label set in " + name + " - not setting see it / say it label enabled."); return; } seeItSayItLabel.SetActive(value); } } /// /// Returns the Interactable's OnClick event. /// public UnityEvent OnClick { get { if (interactable == null) { Debug.LogWarning("No interactable set in " + name + " - returning an empty OnClick event."); return emptyOnClickEvent; } return interactable.OnClick; } } /// /// Modifies the button's icon rendering style. /// public ButtonIconStyle IconStyle { get { return iconStyle; } set { if (iconStyle != value) { SetIconStyle(value); } } } /// /// The button's icon set. Note that setting this will not automatically assign an icon from the new set. /// public ButtonIconSet IconSet { get => iconSet; set => iconSet = value; } private readonly static UnityEvent emptyOnClickEvent = new UnityEvent(); private readonly static uint defaultIconChar = ButtonIconSet.ConvertCharStringToUInt32("\uEBD2"); private const string defaultIconTextureNameID = "_MainTex"; [SerializeField, Tooltip("Optional main label used by the button.")] private TextMeshPro mainLabelText = null; [SerializeField, Tooltip("Optional interactable component used by the button. Used for its OnClick event.")] private Interactable interactable = null; [SerializeField, Tooltip("Optional see it / say it object.")] private GameObject seeItSayItLabel = null; [SerializeField, Tooltip("Optional see it / say it label used by the button. Should be subsumed under the seeItSayItLabel object."), FormerlySerializedAs("seeItSatItLabelText")] private TextMeshPro seeItSayItLabelText = null; [SerializeField, Tooltip("How the button icon should be rendered.")] private ButtonIconStyle iconStyle = ButtonIconStyle.Quad; [Header("Font Icon")] [SerializeField, Tooltip("Optional label used for font icon.")] private TextMeshPro iconCharLabel = null; [SerializeField, Tooltip("Optional font used for font icon. This will be set by configuration actions using the icon set.")] private TMP_FontAsset iconCharFont = null; [SerializeField, Tooltip("Optional unicode code for font icon. See Text Mesh Pro font asset for available unicode characters. This will be set by configuration actions.")] private uint iconChar = 0; [Header("Sprite Icon")] [SerializeField, Tooltip("Optional sprite renderer used for sprite icon.")] private SpriteRenderer iconSpriteRenderer = null; [SerializeField, Tooltip("Optional sprite used for sprite icon. This will be set by configuration actions.")] private Sprite iconSprite = null; [Header("Quad Icon")] [SerializeField, Tooltip("Optional quad renderer used for texture icon.")] private MeshRenderer iconQuadRenderer = null; [SerializeField, Tooltip("The texture name ID. Set to " + defaultIconTextureNameID + " by default.")] private string iconQuadTextureNameID = defaultIconTextureNameID; [SerializeField, Tooltip("Optional texture used for texture icon. This will be set by configuration actions.")] private Texture iconQuadTexture = null; // Disable 'assigned but never used' errors to avoid errors related to editor-only fields. #pragma warning disable CS0414 [SerializeField, Tooltip("The default material used by quad button icons. Used to detect legacy custom buttons.")] private Material defaultButtonQuadMaterial = null; [Header("Icon Set")] [SerializeField, Tooltip("Optional icon set used to configure icon objects.")] private ButtonIconSet iconSet = null; [SerializeField, Tooltip("The default icon set.")] private ButtonIconSet defaultIconSet = null; #pragma warning restore CS0414 private MaterialPropertyBlock iconTexturePropertyBlock; /// /// Searches the icon set for a character matching newIconCharName. /// If no icon set is available, or if no texture with that name is found, no action is taken. /// /// Name of the new icon character as defined in the IconSet. public void SetCharIconByName(string newIconCharName) { if (string.IsNullOrEmpty(newIconCharName)) { Debug.LogError("Icon character name cannot be null."); return; } if (iconSet == null) { Debug.LogWarning("No icon set in " + name + " - taking no action.."); return; } uint charIcon = 0; if (!iconSet.TryGetCharIcon(newIconCharName, out charIcon)) { Debug.LogWarning("Couldn't find icon character with name " + newIconCharName + " in " + name + " - taking no action.."); return; } SetCharIcon(charIcon); } /// /// Searches the icon set for a texture matching newIconTextureName. /// If no icon set is available, or if no texture with that name is found, no action is taken. /// /// Name of the new icon texture asset. public void SetQuadIconByName(string newIconTextureName) { if (string.IsNullOrEmpty(newIconTextureName)) { Debug.LogError("Icon texture name cannot be null."); return; } if (iconSet == null) { Debug.LogWarning("No icon set in " + name + " - taking no action.."); return; } Texture2D quadIcon = null; if (!iconSet.TryGetQuadIcon(newIconTextureName, out quadIcon)) { Debug.LogWarning("Couldn't find icon texture with name " + newIconTextureName + " in " + name + " - taking no action.."); return; } SetQuadIcon(quadIcon); } /// /// Searches the icon set for a texture matching newIconSpriteName. /// If no icon set is available, or if no texture with that name is found, no action is taken. /// /// Name of the new icon texture asset. public void SetSpriteIconByName(string newIconSpriteName) { if (string.IsNullOrEmpty(newIconSpriteName)) { Debug.LogError("Icon sprite name cannot be null."); return; } if (iconSet == null) { Debug.LogWarning("No icon set in " + name + " - taking no action.."); return; } Sprite spriteIcon = null; if (!iconSet.TryGetSpriteIcon(newIconSpriteName, out spriteIcon)) { Debug.LogWarning("Couldn't find icon sprite with name " + newIconSpriteName + " in " + name + " - taking no action.."); return; } SetSpriteIcon(spriteIcon); } /// /// Sets the character for the button. This automatically sets the button icon style to Char. /// /// Unicode string for new icon character. /// Optional TMPro font asset. If null, the existing font asset will be used. public void SetCharIcon(uint newIconChar, UnityEngine.Object newIconCharFont = null) { if (newIconChar <= 0) { return; } if (newIconCharFont != null && newIconCharFont != iconCharFont) { iconCharFont = (TMP_FontAsset)newIconCharFont; } if (iconCharLabel == null) { Debug.LogWarning("No icon char label in " + name + " - not setting custom icon char."); return; } iconChar = newIconChar; if (iconCharFont != null) { iconCharLabel.font = iconCharFont; } uint labelChar = ButtonIconSet.ConvertCharStringToUInt32(iconCharLabel.text); if (labelChar != iconChar || iconCharLabel.font != iconCharFont) { iconCharLabel.text = ButtonIconSet.ConvertUInt32ToUnicodeCharString(newIconChar); } SetIconStyle(ButtonIconStyle.Char); } /// /// Sets the sprite for the button. This automatically sets the button icon style to Sprite. /// public void SetSpriteIcon(Sprite newIconSprite) { if (newIconSprite == null) { return; } if (iconSpriteRenderer == null) { Debug.LogWarning("No icon sprite renderer in " + name + " - not setting custom icon sprite."); return; } iconSprite = newIconSprite; if (iconSpriteRenderer.sprite != iconSprite) { iconSpriteRenderer.sprite = newIconSprite; } SetIconStyle(ButtonIconStyle.Sprite); } /// /// Sets the quad texture for the button. This automatically sets the button icon style to Quad. /// public void SetQuadIcon(Texture newIconTexture) { if (newIconTexture == null) { return; } if (iconQuadRenderer == null) { Debug.LogWarning("No icon quad renderer in " + name + " - not setting custom icon texture."); return; } iconQuadTexture = newIconTexture; if (iconTexturePropertyBlock == null) { iconTexturePropertyBlock = new MaterialPropertyBlock(); } iconQuadRenderer.GetPropertyBlock(iconTexturePropertyBlock); iconTexturePropertyBlock.SetTexture(iconQuadTextureNameID, newIconTexture); iconQuadRenderer.SetPropertyBlock(iconTexturePropertyBlock); SetIconStyle(ButtonIconStyle.Quad); } /// /// Sets the icon style for the button. Relevant components will be turned on / off based on style. /// private void SetIconStyle(ButtonIconStyle newStyle) { iconStyle = newStyle; switch (iconStyle) { case ButtonIconStyle.Char: if (iconCharLabel != null) { iconCharLabel.gameObject.SetActive(true); } if (iconSpriteRenderer != null) { iconSpriteRenderer.gameObject.SetActive(false); } if (iconQuadRenderer != null) { iconQuadRenderer.gameObject.SetActive(false); } break; case ButtonIconStyle.Sprite: if (iconCharLabel != null) { iconCharLabel.gameObject.SetActive(false); } if (iconSpriteRenderer != null) { iconSpriteRenderer.gameObject.SetActive(true); } if (iconQuadRenderer != null) { iconQuadRenderer.gameObject.SetActive(false); } break; case ButtonIconStyle.Quad: if (iconCharLabel != null) { iconCharLabel.gameObject.SetActive(false); } if (iconSpriteRenderer != null) { iconSpriteRenderer.gameObject.SetActive(false); } if (iconQuadRenderer != null) { iconQuadRenderer.gameObject.SetActive(true); } break; case ButtonIconStyle.None: if (iconCharLabel != null) { iconCharLabel.gameObject.SetActive(false); } if (iconSpriteRenderer != null) { iconSpriteRenderer.gameObject.SetActive(false); } if (iconQuadRenderer != null) { iconQuadRenderer.gameObject.SetActive(false); } break; } } /// /// Forces the config helper to apply its internal settings. /// public void ForceRefresh() { switch (iconStyle) { case ButtonIconStyle.Quad: SetQuadIcon(iconQuadTexture); break; case ButtonIconStyle.Char: SetCharIcon(iconChar, iconCharFont); break; case ButtonIconStyle.Sprite: SetSpriteIcon(iconSprite); break; case ButtonIconStyle.None: SetIconStyle(ButtonIconStyle.None); break; } } private void OnEnable() { #if UNITY_EDITOR if (EditorCheckForCustomIcon()) { // If we're using a custom icon, preserve it so it doesn't vanish EditorPreserveCustomIcon(); } #else // Set these to null to avoid build errors. defaultIconSet = null; defaultButtonQuadMaterial = null; #endif ForceRefresh(); } #if UNITY_EDITOR private static readonly string generatedIconSetName = "CustomIconSet"; private static readonly string customIconSetsFolderName = "CustomIconSets"; private static readonly string customIconSetCreatedMessage = "A new icon set has been created to hold your button's custom icons. This icon set will be used by your button's ButtonConfigHelper component. It has been saved to:\n\n{0}"; /// /// Returns true if the button is using a custom icon material. /// public bool EditorCheckForCustomIcon() { if (iconSet == null || iconQuadRenderer == null || iconStyle != ButtonIconStyle.Quad) { // Nothing to do here. return false; } if (iconSet != defaultIconSet) { // This button is using a custom icon set, so we can't assume material differences mean it needs an upgrade. return false; } if (iconQuadRenderer.sharedMaterial == defaultButtonQuadMaterial) { // This button is using the default material, so it's not a customized button. return false; } string assetPath = AssetDatabase.GetAssetPath(iconQuadRenderer.sharedMaterial); if (string.IsNullOrEmpty(assetPath)) { // If the asset path is null, this material instance exists only in memory. return false; } return true; } /// /// Upgrades a button using a custom icon material. /// public void EditorUpgradeCustomIcon(ButtonIconSet defaultIconSet = null, string customIconsFolder = null, bool hideAlert = false) { if (string.IsNullOrEmpty(customIconsFolder)) { customIconsFolder = MixedRealityToolkitFiles.GetGeneratedFolder; } if (defaultIconSet == null) { defaultIconSet = this.defaultIconSet; } SerializedObject configObject = new SerializedObject(this); SerializedProperty iconStyleProp = configObject.FindProperty("iconStyle"); SerializedProperty iconSetProp = configObject.FindProperty("iconSet"); SerializedProperty iconQuadTextureProp = configObject.FindProperty("iconQuadTexture"); if (iconQuadRenderer.gameObject.activeSelf && !iconQuadRenderer.enabled) { // If the quad renderer is disabled, enable it and disable the quad renderer game object instead. // Disabling the quad renderer used to be the preferred way to disable icons but it's no longer consistent with our icon style. iconQuadRenderer.enabled = true; iconQuadRenderer.gameObject.SetActive(false); EditorUtility.SetDirty(gameObject); } if (!iconQuadRenderer.gameObject.activeSelf && !iconSpriteRenderer.gameObject.activeSelf && !iconCharLabel.gameObject.activeSelf) { // If all the icon objects are disabled, set the icon style to none and do nothing else. iconStyleProp.intValue = (int)ButtonIconStyle.None; configObject.ApplyModifiedProperties(); EditorUtility.SetDirty(gameObject); return; } string assetPath = AssetDatabase.GetAssetPath(iconQuadRenderer.sharedMaterial); if (string.IsNullOrEmpty(assetPath)) { // If the asset path is null, this material instance exists only in memory. return; } Material targetQuadMaterial = iconQuadRenderer.sharedMaterial; Texture targetQuadIcon = targetQuadMaterial.mainTexture; if (targetQuadIcon == null) { // There is no icon to copy. return; } ButtonIconSet targetIconSet = iconSet; bool createdIconSet = false; string generatedIconSetFolder = System.IO.Path.Combine(customIconsFolder, customIconSetsFolderName); // If this icon set doesn't have our icon in it, we need to either add it or create a new icon set if (!iconSet.TryGetQuadIcon(targetQuadIcon.name, out Texture2D quadIcon) && iconSet == defaultIconSet) { // If we're using the default icon set, we have to create a new set to add the icon if (!AssetDatabase.IsValidFolder(generatedIconSetFolder)) { // Create the folder if it doesn't exist AssetDatabase.CreateFolder(customIconsFolder, customIconSetsFolderName); } string generatedIconSetPath = System.IO.Path.Combine(generatedIconSetFolder, generatedIconSetName + ".asset"); targetIconSet = (ButtonIconSet)AssetDatabase.LoadAssetAtPath(generatedIconSetPath, typeof(ButtonIconSet)); if (targetIconSet == null) { // If the icon set doesn't already exist, duplicate the default ScriptableObject duplicateIconSet = Instantiate(defaultIconSet); duplicateIconSet.name = generatedIconSetName; AssetDatabase.CreateAsset(duplicateIconSet, generatedIconSetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); targetIconSet = (ButtonIconSet)AssetDatabase.LoadAssetAtPath(generatedIconSetPath, typeof(ButtonIconSet)); createdIconSet = true; } } bool selectIconSet = false; if (createdIconSet && !hideAlert) { selectIconSet = EditorUtility.DisplayDialog("Custom Icon Set Created", string.Format(customIconSetCreatedMessage, generatedIconSetFolder), "View Asset", "OK"); } // Set the icon set to the custom generated icon set iconSetProp.objectReferenceValue = targetIconSet; // Add the custom icon to the custom set targetIconSet.EditorAddCustomQuadIcon(targetQuadIcon); // Reset changes to the quad renderer iconQuadTextureProp.objectReferenceValue = targetQuadIcon; configObject.ApplyModifiedProperties(); // If the custom material shader is different from the default material, don't alter the material if (targetQuadMaterial.shader.name == defaultButtonQuadMaterial.shader.name && PrefabUtility.IsPartOfPrefabInstance(iconQuadRenderer)) { // If the custom material shader is the same, revert any prefab overrides SerializedObject iconQuadRendererObject = new SerializedObject(iconQuadRenderer); SerializedProperty materialsProp = iconQuadRendererObject.FindProperty("m_Materials"); PrefabUtility.RevertPropertyOverride(materialsProp, InteractionMode.AutomatedAction); } EditorUtility.SetDirty(gameObject); ForceRefresh(); if (selectIconSet) { Selection.activeObject = targetIconSet; EditorGUIUtility.PingObject(targetIconSet); } } private void EditorPreserveCustomIcon() { SerializedObject configObject = new SerializedObject(this); SerializedProperty iconQuadTextureProp = configObject.FindProperty("iconQuadTexture"); iconQuadTextureProp.objectReferenceValue = iconQuadRenderer.sharedMaterial.mainTexture; configObject.ApplyModifiedProperties(); } public void EditorSetDefaultIconSet(ButtonIconSet iconSet) { defaultIconSet = iconSet; } public void EditorSetDefaultQuadMaterial(Material mat) { defaultButtonQuadMaterial = mat; } public void EditorSetIconQuadRenderer(MeshRenderer renderer) { iconQuadRenderer = renderer; } #endif } }