Suggestion : TextMeshPro - Button

Hi

When creating a Button with UGUI it will come with a basic UGUI Text object, yet since TMP is pretty much always the better option i always have to delete that text object and create a new TMP text, change its color (as default is white) and set the anchors to and font size to match the previous UGUI text before actually change its parameters to match my needs.

The code bellow adds an option to the create menu for a TextMeshPro - Button. The code is a modified version of the default create Button code that just replease the Text component with a TextMeshPro UGUI text (and the code must be inside an Editor folder).

I feel like having this option (create/TextMeshPro - Button) would speed up development time a bit, as deleting and creating text objects over and over do adds up.

While making an asset store package would help to have this option available, i believe it would be much better if this could be added to TMP.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEditor;
using UnityEngine.EventSystems;

public class TMPExtensions : UnityEditor.UI.ButtonEditor
{
    private const string kUILayerName = "UI";
    private const float kWidth = 160f;
    private const float kThickHeight = 30f;
    private const string kStandardSpritePath = "UI/Skin/UISprite.psd";

    private static Vector2 s_ThickGUIElementSize = new Vector2(kWidth, kThickHeight);
    private static Color s_DefaultSelectableColor = new Color(1f, 1f, 1f, 1f);
    private static Color s_TextColor = new Color(50f / 255f, 50f / 255f, 50f / 255f, 1f);
    private static float s_TextFontSize = 14;

    private static void SetPositionVisibleinSceneView(RectTransform canvasRTransform, RectTransform itemTransform)
    {
        // Find the best scene view
        SceneView sceneView = SceneView.lastActiveSceneView;
        if (sceneView == null && SceneView.sceneViews.Count > 0)
            sceneView = SceneView.sceneViews[0] as SceneView;

        // Couldn't find a SceneView. Don't set position.
        if (sceneView == null || sceneView.camera == null)
            return;

        // Create world space Plane from canvas position.
        Vector2 localPlanePosition;
        Camera camera = sceneView.camera;
        Vector3 position = Vector3.zero;
        if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRTransform, new Vector2(camera.pixelWidth / 2, camera.pixelHeight / 2), camera, out localPlanePosition))
        {
            // Adjust for canvas pivot
            localPlanePosition.x = localPlanePosition.x + canvasRTransform.sizeDelta.x * canvasRTransform.pivot.x;
            localPlanePosition.y = localPlanePosition.y + canvasRTransform.sizeDelta.y * canvasRTransform.pivot.y;

            localPlanePosition.x = Mathf.Clamp(localPlanePosition.x, 0, canvasRTransform.sizeDelta.x);
            localPlanePosition.y = Mathf.Clamp(localPlanePosition.y, 0, canvasRTransform.sizeDelta.y);

            // Adjust for anchoring
            position.x = localPlanePosition.x - canvasRTransform.sizeDelta.x * itemTransform.anchorMin.x;
            position.y = localPlanePosition.y - canvasRTransform.sizeDelta.y * itemTransform.anchorMin.y;

            Vector3 minLocalPosition;
            minLocalPosition.x = canvasRTransform.sizeDelta.x * (0 - canvasRTransform.pivot.x) + itemTransform.sizeDelta.x * itemTransform.pivot.x;
            minLocalPosition.y = canvasRTransform.sizeDelta.y * (0 - canvasRTransform.pivot.y) + itemTransform.sizeDelta.y * itemTransform.pivot.y;

            Vector3 maxLocalPosition;
            maxLocalPosition.x = canvasRTransform.sizeDelta.x * (1 - canvasRTransform.pivot.x) - itemTransform.sizeDelta.x * itemTransform.pivot.x;
            maxLocalPosition.y = canvasRTransform.sizeDelta.y * (1 - canvasRTransform.pivot.y) - itemTransform.sizeDelta.y * itemTransform.pivot.y;

            position.x = Mathf.Clamp(position.x, minLocalPosition.x, maxLocalPosition.x);
            position.y = Mathf.Clamp(position.y, minLocalPosition.y, maxLocalPosition.y);
        }

        itemTransform.anchoredPosition = position;
        itemTransform.localRotation = Quaternion.identity;
        itemTransform.localScale = Vector3.one;
    }

    private static GameObject CreateUIElementRoot(string name, MenuCommand menuCommand, Vector2 size)
    {
        GameObject parent = menuCommand.context as GameObject;
        if (parent == null || parent.GetComponentInParent<Canvas>() == null)
        {
            parent = GetOrCreateCanvasGameObject();
        }
        GameObject child = new GameObject(name);

        Undo.RegisterCreatedObjectUndo(child, "Create " + name);
        Undo.SetTransformParent(child.transform, parent.transform, "Parent " + child.name);
        GameObjectUtility.SetParentAndAlign(child, parent);

        RectTransform rectTransform = child.AddComponent<RectTransform>();
        rectTransform.sizeDelta = size;
        if (parent != menuCommand.context) // not a context click, so center in sceneview
        {
            SetPositionVisibleinSceneView(parent.GetComponent<RectTransform>(), rectTransform);
        }
        Selection.activeGameObject = child;
        return child;
    }

    [MenuItem("GameObject/UI/TextMeshPro - Button", false, 2012)]
    static public void AddButton(MenuCommand menuCommand)
    {
        GameObject buttonRoot = CreateUIElementRoot("TextMeshPro - Button", menuCommand, s_ThickGUIElementSize);

        GameObject childText = new GameObject("TextMeshPro - Text");
        GameObjectUtility.SetParentAndAlign(childText, buttonRoot);

        Image image = buttonRoot.AddComponent<Image>();
        image.sprite = AssetDatabase.GetBuiltinExtraResource<Sprite>(kStandardSpritePath);
        image.type = Image.Type.Sliced;
        image.color = s_DefaultSelectableColor;

        Button bt = buttonRoot.AddComponent<Button>();
        SetDefaultColorTransitionValues(bt);

        TextMeshProUGUI text = childText.AddComponent<TextMeshProUGUI>();
        text.text = "Button";
        text.alignment = TextAlignmentOptions.Center;
        SetDefaultTextValues(text);

        RectTransform textRectTransform = childText.GetComponent<RectTransform>();
        textRectTransform.anchorMin = Vector2.zero;
        textRectTransform.anchorMax = Vector2.one;
        textRectTransform.sizeDelta = Vector2.zero;
    }

    private static void SetDefaultTextValues(TextMeshProUGUI lbl)
    {
        // Set text values we want across UI elements in default controls.
        // Don't set values which are the same as the default values for the Text component,
        // since there's no point in that, and it's good to keep them as consistent as possible.
        lbl.color = s_TextColor;
        lbl.fontSize = s_TextFontSize;
    }

    static GameObject CreateUIObject(string name, GameObject parent)
    {
        GameObject go = new GameObject(name);
        go.AddComponent<RectTransform>();
        GameObjectUtility.SetParentAndAlign(go, parent);
        return go;
    }

    private static void SetDefaultColorTransitionValues(Selectable slider)
    {
        ColorBlock colors = slider.colors;
        colors.highlightedColor = new Color(0.882f, 0.882f, 0.882f);
        colors.pressedColor = new Color(0.698f, 0.698f, 0.698f);
        colors.disabledColor = new Color(0.521f, 0.521f, 0.521f);
    }

    static public GameObject CreateNewUI()
    {
        // Root for the UI
        var root = new GameObject("Canvas");
        root.layer = LayerMask.NameToLayer(kUILayerName);
        Canvas canvas = root.AddComponent<Canvas>();
        canvas.renderMode = RenderMode.ScreenSpaceOverlay;
        root.AddComponent<CanvasScaler>();
        root.AddComponent<GraphicRaycaster>();
        Undo.RegisterCreatedObjectUndo(root, "Create " + root.name);

        // if there is no event system add one...
        CreateEventSystem(false);
        return root;
    }

    private static void CreateEventSystem(bool select)
    {
        CreateEventSystem(select, null);
    }

    private static void CreateEventSystem(bool select, GameObject parent)
    {
        var esys = Object.FindObjectOfType<EventSystem>();
        if (esys == null)
        {
            var eventSystem = new GameObject("EventSystem");
            GameObjectUtility.SetParentAndAlign(eventSystem, parent);
            esys = eventSystem.AddComponent<EventSystem>();
            eventSystem.AddComponent<StandaloneInputModule>();
            //eventSystem.AddComponent<TouchInputModule>();

            Undo.RegisterCreatedObjectUndo(eventSystem, "Create " + eventSystem.name);
        }

        if (select && esys != null)
        {
            Selection.activeGameObject = esys.gameObject;
        }
    }

    // Helper function that returns a Canvas GameObject; preferably a parent of the selection, or other existing Canvas.
    static public GameObject GetOrCreateCanvasGameObject()
    {
        GameObject selectedGo = Selection.activeGameObject;

        // Try to find a gameobject that is the selected GO or one if its parents.
        Canvas canvas = (selectedGo != null) ? selectedGo.GetComponentInParent<Canvas>() : null;
        if (canvas != null && canvas.gameObject.activeInHierarchy)
            return canvas.gameObject;

        // No canvas in selection or its parents? Then use just any canvas..
        canvas = Object.FindObjectOfType(typeof(Canvas)) as Canvas;
        if (canvas != null && canvas.gameObject.activeInHierarchy)
            return canvas.gameObject;

        // No canvas in the scene at all? Then create a new one.
        return CreateNewUI();
    }
}
1 Like

Thank you for taking the time to provide this solution.

As luck would have it and given this is something I should have offered a long time ago, I actually decided to add this button last week :slight_smile:

P.S. I also renamed the TMP options.

1 Like

Thats great :smile:

Cool script, thanks! Wish I understood it. :stuck_out_tongue:

One thing though, how do you make it so that a text mesh pro text fits nicely on the button, like maxed out size without overflowing?

1 Like

Hi, what type of file (for example *.cs) should this file have with and does it matter where in the Unity editor file hierarchy I put the file in order to make this work?

Its extension should be “cs” and I think you have to put it in a Subfolder called Editor (anywhere in the hierarchy).

@Drabantor
ZoidbergForPresident is right, it should be a .cs script and as long as its in a folder called “Editor” (or in a subfolder of a folder called “Editor”) it should work fine.

Although, its looks like the “navite” create TMP Button was added a few versions ago, so my script isnt that relevant any more (still usefull when using older versions of TMP i guess). As far i can tell the only visible difference is the font size (14 on mine vs 24 on TMP)

Thanks guys. I thought that by upgrading Unity to e.g. version 2018.3.7 that Text Mesh Pro would upgrade to include the Text Mesh button but that was no the case for me. Instead in order to make this work, I had to go to the Package Manager under the Window tab in Unity and then upgrade Text mesh Pro to version 1.4.0. Now I have the Text Mesh button.

With Unity 2018.4.4 LTS, current TMP from Package Manager, TMP essentials imported, the doc PDF is from 2016 and while I have the “Button - TextMeshPro” in the UI menu, it’s no different than the regular button, and has no text components. It just creates a canvas and a UGUI button.

Something must have gone wrong on the package import.

Make sure you are using package version 1.4.1. The Button - TextMeshPro has the button as the parent and the text component as a child.

Which version of TMP has the “Button - TextMeshPro” option? I am on Unity 2018.2.14f1 and my TMP is on 1.3.0 (highest possible from the package manager), but unfortunately I do not see it (only dropdown, text input field and text)… :cry:

I think that was added to version 1.4.x which is already fairly old. The latest for 2018.4 is 1.5.4.

Any reason why you are still running on 2018.2?