using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.Events;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI.CoroutineTween;
|
|
|
|
namespace TMPro
|
|
{
|
|
[AddComponentMenu("UI/TMP Dropdown", 35)]
|
|
[RequireComponent(typeof(RectTransform))]
|
|
public class TMP_Dropdown : Selectable, IPointerClickHandler, ISubmitHandler, ICancelHandler
|
|
{
|
|
protected internal class DropdownItem : MonoBehaviour, IPointerEnterHandler, ICancelHandler
|
|
{
|
|
[SerializeField]
|
|
private TMP_Text m_Text;
|
|
[SerializeField]
|
|
private Image m_Image;
|
|
[SerializeField]
|
|
private RectTransform m_RectTransform;
|
|
[SerializeField]
|
|
private Toggle m_Toggle;
|
|
|
|
public TMP_Text text { get { return m_Text; } set { m_Text = value; } }
|
|
public Image image { get { return m_Image; } set { m_Image = value; } }
|
|
public RectTransform rectTransform { get { return m_RectTransform; } set { m_RectTransform = value; } }
|
|
public Toggle toggle { get { return m_Toggle; } set { m_Toggle = value; } }
|
|
|
|
public virtual void OnPointerEnter(PointerEventData eventData)
|
|
{
|
|
EventSystem.current.SetSelectedGameObject(gameObject);
|
|
}
|
|
|
|
public virtual void OnCancel(BaseEventData eventData)
|
|
{
|
|
TMP_Dropdown dropdown = GetComponentInParent<TMP_Dropdown>();
|
|
if (dropdown)
|
|
dropdown.Hide();
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class OptionData
|
|
{
|
|
[SerializeField]
|
|
private string m_Text;
|
|
[SerializeField]
|
|
private Sprite m_Image;
|
|
|
|
public string text { get { return m_Text; } set { m_Text = value; } }
|
|
public Sprite image { get { return m_Image; } set { m_Image = value; } }
|
|
|
|
public OptionData()
|
|
{
|
|
}
|
|
|
|
public OptionData(string text)
|
|
{
|
|
this.text = text;
|
|
}
|
|
|
|
public OptionData(Sprite image)
|
|
{
|
|
this.image = image;
|
|
}
|
|
|
|
public OptionData(string text, Sprite image)
|
|
{
|
|
this.text = text;
|
|
this.image = image;
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class OptionDataList
|
|
{
|
|
[SerializeField]
|
|
private List<OptionData> m_Options;
|
|
public List<OptionData> options { get { return m_Options; } set { m_Options = value; } }
|
|
|
|
|
|
public OptionDataList()
|
|
{
|
|
options = new List<OptionData>();
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class DropdownEvent : UnityEvent<int> { }
|
|
|
|
// Template used to create the dropdown.
|
|
[SerializeField]
|
|
private RectTransform m_Template;
|
|
public RectTransform template { get { return m_Template; } set { m_Template = value; RefreshShownValue(); } }
|
|
|
|
// Text to be used as a caption for the current value. It's not required, but it's kept here for convenience.
|
|
[SerializeField]
|
|
private TMP_Text m_CaptionText;
|
|
public TMP_Text captionText { get { return m_CaptionText; } set { m_CaptionText = value; RefreshShownValue(); } }
|
|
|
|
[SerializeField]
|
|
private Image m_CaptionImage;
|
|
public Image captionImage { get { return m_CaptionImage; } set { m_CaptionImage = value; RefreshShownValue(); } }
|
|
|
|
[Space]
|
|
|
|
[SerializeField]
|
|
private TMP_Text m_ItemText;
|
|
public TMP_Text itemText { get { return m_ItemText; } set { m_ItemText = value; RefreshShownValue(); } }
|
|
|
|
[SerializeField]
|
|
private Image m_ItemImage;
|
|
public Image itemImage { get { return m_ItemImage; } set { m_ItemImage = value; RefreshShownValue(); } }
|
|
|
|
[Space]
|
|
|
|
[SerializeField]
|
|
private int m_Value;
|
|
|
|
[Space]
|
|
|
|
// Items that will be visible when the dropdown is shown.
|
|
// We box this into its own class so we can use a Property Drawer for it.
|
|
[SerializeField]
|
|
private OptionDataList m_Options = new OptionDataList();
|
|
public List<OptionData> options
|
|
{
|
|
get { return m_Options.options; }
|
|
set { m_Options.options = value; RefreshShownValue(); }
|
|
}
|
|
|
|
[Space]
|
|
|
|
// Notification triggered when the dropdown changes.
|
|
[SerializeField]
|
|
private DropdownEvent m_OnValueChanged = new DropdownEvent();
|
|
public DropdownEvent onValueChanged { get { return m_OnValueChanged; } set { m_OnValueChanged = value; } }
|
|
|
|
private GameObject m_Dropdown;
|
|
private GameObject m_Blocker;
|
|
private List<DropdownItem> m_Items = new List<DropdownItem>();
|
|
private TweenRunner<FloatTween> m_AlphaTweenRunner;
|
|
private bool validTemplate = false;
|
|
|
|
private static OptionData s_NoOptionData = new OptionData();
|
|
|
|
// Current value.
|
|
public int value
|
|
{
|
|
get
|
|
{
|
|
return m_Value;
|
|
}
|
|
set
|
|
{
|
|
if (Application.isPlaying && (value == m_Value || options.Count == 0))
|
|
return;
|
|
|
|
m_Value = Mathf.Clamp(value, 0, options.Count - 1);
|
|
RefreshShownValue();
|
|
|
|
// Notify all listeners
|
|
m_OnValueChanged.Invoke(m_Value);
|
|
}
|
|
}
|
|
|
|
public bool IsExpanded
|
|
{ get { return m_Dropdown != null; } }
|
|
|
|
protected TMP_Dropdown()
|
|
{ }
|
|
|
|
protected override void Awake()
|
|
{
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying)
|
|
return;
|
|
#endif
|
|
|
|
m_AlphaTweenRunner = new TweenRunner<FloatTween>();
|
|
m_AlphaTweenRunner.Init(this);
|
|
|
|
if (m_CaptionImage)
|
|
m_CaptionImage.enabled = (m_CaptionImage.sprite != null);
|
|
|
|
if (m_Template)
|
|
m_Template.gameObject.SetActive(false);
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
protected override void OnValidate()
|
|
{
|
|
base.OnValidate();
|
|
|
|
if (!IsActive())
|
|
return;
|
|
|
|
RefreshShownValue();
|
|
}
|
|
#endif
|
|
|
|
public void RefreshShownValue()
|
|
{
|
|
OptionData data = s_NoOptionData;
|
|
|
|
if (options.Count > 0)
|
|
data = options[Mathf.Clamp(m_Value, 0, options.Count - 1)];
|
|
|
|
if (m_CaptionText)
|
|
{
|
|
if (data != null && data.text != null)
|
|
m_CaptionText.text = data.text;
|
|
else
|
|
m_CaptionText.text = "";
|
|
}
|
|
|
|
if (m_CaptionImage)
|
|
{
|
|
if (data != null)
|
|
m_CaptionImage.sprite = data.image;
|
|
else
|
|
m_CaptionImage.sprite = null;
|
|
m_CaptionImage.enabled = (m_CaptionImage.sprite != null);
|
|
}
|
|
}
|
|
|
|
public void AddOptions(List<OptionData> options)
|
|
{
|
|
this.options.AddRange(options);
|
|
RefreshShownValue();
|
|
}
|
|
|
|
public void AddOptions(List<string> options)
|
|
{
|
|
for (int i = 0; i < options.Count; i++)
|
|
this.options.Add(new OptionData(options[i]));
|
|
RefreshShownValue();
|
|
}
|
|
|
|
public void AddOptions(List<Sprite> options)
|
|
{
|
|
for (int i = 0; i < options.Count; i++)
|
|
this.options.Add(new OptionData(options[i]));
|
|
RefreshShownValue();
|
|
}
|
|
|
|
public void ClearOptions()
|
|
{
|
|
options.Clear();
|
|
RefreshShownValue();
|
|
}
|
|
|
|
private void SetupTemplate()
|
|
{
|
|
validTemplate = false;
|
|
|
|
if (!m_Template)
|
|
{
|
|
Debug.LogError("The dropdown template is not assigned. The template needs to be assigned and must have a child GameObject with a Toggle component serving as the item.", this);
|
|
return;
|
|
}
|
|
|
|
GameObject templateGo = m_Template.gameObject;
|
|
templateGo.SetActive(true);
|
|
Toggle itemToggle = m_Template.GetComponentInChildren<Toggle>();
|
|
|
|
validTemplate = true;
|
|
if (!itemToggle || itemToggle.transform == template)
|
|
{
|
|
validTemplate = false;
|
|
Debug.LogError("The dropdown template is not valid. The template must have a child GameObject with a Toggle component serving as the item.", template);
|
|
}
|
|
else if (!(itemToggle.transform.parent is RectTransform))
|
|
{
|
|
validTemplate = false;
|
|
Debug.LogError("The dropdown template is not valid. The child GameObject with a Toggle component (the item) must have a RectTransform on its parent.", template);
|
|
}
|
|
else if (itemText != null && !itemText.transform.IsChildOf(itemToggle.transform))
|
|
{
|
|
validTemplate = false;
|
|
Debug.LogError("The dropdown template is not valid. The Item Text must be on the item GameObject or children of it.", template);
|
|
}
|
|
else if (itemImage != null && !itemImage.transform.IsChildOf(itemToggle.transform))
|
|
{
|
|
validTemplate = false;
|
|
Debug.LogError("The dropdown template is not valid. The Item Image must be on the item GameObject or children of it.", template);
|
|
}
|
|
|
|
if (!validTemplate)
|
|
{
|
|
templateGo.SetActive(false);
|
|
return;
|
|
}
|
|
|
|
DropdownItem item = itemToggle.gameObject.AddComponent<DropdownItem>();
|
|
item.text = m_ItemText;
|
|
item.image = m_ItemImage;
|
|
item.toggle = itemToggle;
|
|
item.rectTransform = (RectTransform)itemToggle.transform;
|
|
|
|
Canvas popupCanvas = GetOrAddComponent<Canvas>(templateGo);
|
|
popupCanvas.overrideSorting = true;
|
|
popupCanvas.sortingOrder = 30000;
|
|
|
|
GetOrAddComponent<GraphicRaycaster>(templateGo);
|
|
GetOrAddComponent<CanvasGroup>(templateGo);
|
|
templateGo.SetActive(false);
|
|
|
|
validTemplate = true;
|
|
}
|
|
|
|
private static T GetOrAddComponent<T>(GameObject go) where T : Component
|
|
{
|
|
T comp = go.GetComponent<T>();
|
|
if (!comp)
|
|
comp = go.AddComponent<T>();
|
|
return comp;
|
|
}
|
|
|
|
public virtual void OnPointerClick(PointerEventData eventData)
|
|
{
|
|
Show();
|
|
}
|
|
|
|
public virtual void OnSubmit(BaseEventData eventData)
|
|
{
|
|
Show();
|
|
}
|
|
|
|
public virtual void OnCancel(BaseEventData eventData)
|
|
{
|
|
Hide();
|
|
}
|
|
|
|
// Show the dropdown.
|
|
//
|
|
// Plan for dropdown scrolling to ensure dropdown is contained within screen.
|
|
//
|
|
// We assume the Canvas is the screen that the dropdown must be kept inside.
|
|
// This is always valid for screen space canvas modes.
|
|
// For world space canvases we don't know how it's used, but it could be e.g. for an in-game monitor.
|
|
// We consider it a fair constraint that the canvas must be big enough to contains dropdowns.
|
|
public void Show()
|
|
{
|
|
if (!IsActive() || !IsInteractable() || m_Dropdown != null)
|
|
return;
|
|
|
|
if (!validTemplate)
|
|
{
|
|
SetupTemplate();
|
|
if (!validTemplate)
|
|
return;
|
|
}
|
|
|
|
// Get root Canvas.
|
|
var list = TMP_ListPool<Canvas>.Get();
|
|
gameObject.GetComponentsInParent(false, list);
|
|
if (list.Count == 0)
|
|
return;
|
|
Canvas rootCanvas = list[0];
|
|
TMP_ListPool<Canvas>.Release(list);
|
|
|
|
m_Template.gameObject.SetActive(true);
|
|
|
|
// Instantiate the drop-down template
|
|
m_Dropdown = CreateDropdownList(m_Template.gameObject);
|
|
m_Dropdown.name = "Dropdown List";
|
|
m_Dropdown.SetActive(true);
|
|
|
|
// Make drop-down RectTransform have same values as original.
|
|
RectTransform dropdownRectTransform = m_Dropdown.transform as RectTransform;
|
|
dropdownRectTransform.SetParent(m_Template.transform.parent, false);
|
|
|
|
// Instantiate the drop-down list items
|
|
|
|
// Find the dropdown item and disable it.
|
|
DropdownItem itemTemplate = m_Dropdown.GetComponentInChildren<DropdownItem>();
|
|
|
|
GameObject content = itemTemplate.rectTransform.parent.gameObject;
|
|
RectTransform contentRectTransform = content.transform as RectTransform;
|
|
itemTemplate.rectTransform.gameObject.SetActive(true);
|
|
|
|
// Get the rects of the dropdown and item
|
|
Rect dropdownContentRect = contentRectTransform.rect;
|
|
Rect itemTemplateRect = itemTemplate.rectTransform.rect;
|
|
|
|
// Calculate the visual offset between the item's edges and the background's edges
|
|
Vector2 offsetMin = itemTemplateRect.min - dropdownContentRect.min + (Vector2)itemTemplate.rectTransform.localPosition;
|
|
Vector2 offsetMax = itemTemplateRect.max - dropdownContentRect.max + (Vector2)itemTemplate.rectTransform.localPosition;
|
|
Vector2 itemSize = itemTemplateRect.size;
|
|
|
|
m_Items.Clear();
|
|
|
|
Toggle prev = null;
|
|
for (int i = 0; i < options.Count; ++i)
|
|
{
|
|
OptionData data = options[i];
|
|
DropdownItem item = AddItem(data, value == i, itemTemplate, m_Items);
|
|
if (item == null)
|
|
continue;
|
|
|
|
// Automatically set up a toggle state change listener
|
|
item.toggle.isOn = value == i;
|
|
item.toggle.onValueChanged.AddListener(x => OnSelectItem(item.toggle));
|
|
|
|
// Select current option
|
|
if (item.toggle.isOn)
|
|
item.toggle.Select();
|
|
|
|
// Automatically set up explicit navigation
|
|
if (prev != null)
|
|
{
|
|
Navigation prevNav = prev.navigation;
|
|
Navigation toggleNav = item.toggle.navigation;
|
|
prevNav.mode = Navigation.Mode.Explicit;
|
|
toggleNav.mode = Navigation.Mode.Explicit;
|
|
|
|
prevNav.selectOnDown = item.toggle;
|
|
prevNav.selectOnRight = item.toggle;
|
|
toggleNav.selectOnLeft = prev;
|
|
toggleNav.selectOnUp = prev;
|
|
|
|
prev.navigation = prevNav;
|
|
item.toggle.navigation = toggleNav;
|
|
}
|
|
prev = item.toggle;
|
|
}
|
|
|
|
// Reposition all items now that all of them have been added
|
|
Vector2 sizeDelta = contentRectTransform.sizeDelta;
|
|
sizeDelta.y = itemSize.y * m_Items.Count + offsetMin.y - offsetMax.y;
|
|
contentRectTransform.sizeDelta = sizeDelta;
|
|
|
|
float extraSpace = dropdownRectTransform.rect.height - contentRectTransform.rect.height;
|
|
if (extraSpace > 0)
|
|
dropdownRectTransform.sizeDelta = new Vector2(dropdownRectTransform.sizeDelta.x, dropdownRectTransform.sizeDelta.y - extraSpace);
|
|
|
|
// Invert anchoring and position if dropdown is partially or fully outside of canvas rect.
|
|
// Typically this will have the effect of placing the dropdown above the button instead of below,
|
|
// but it works as inversion regardless of initial setup.
|
|
Vector3[] corners = new Vector3[4];
|
|
dropdownRectTransform.GetWorldCorners(corners);
|
|
|
|
RectTransform rootCanvasRectTransform = rootCanvas.transform as RectTransform;
|
|
Rect rootCanvasRect = rootCanvasRectTransform.rect;
|
|
for (int axis = 0; axis < 2; axis++)
|
|
{
|
|
bool outside = false;
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
Vector3 corner = rootCanvasRectTransform.InverseTransformPoint(corners[i]);
|
|
if (corner[axis] < rootCanvasRect.min[axis] || corner[axis] > rootCanvasRect.max[axis])
|
|
{
|
|
outside = true;
|
|
break;
|
|
}
|
|
}
|
|
if (outside)
|
|
RectTransformUtility.FlipLayoutOnAxis(dropdownRectTransform, axis, false, false);
|
|
}
|
|
|
|
for (int i = 0; i < m_Items.Count; i++)
|
|
{
|
|
RectTransform itemRect = m_Items[i].rectTransform;
|
|
itemRect.anchorMin = new Vector2(itemRect.anchorMin.x, 0);
|
|
itemRect.anchorMax = new Vector2(itemRect.anchorMax.x, 0);
|
|
itemRect.anchoredPosition = new Vector2(itemRect.anchoredPosition.x, offsetMin.y + itemSize.y * (m_Items.Count - 1 - i) + itemSize.y * itemRect.pivot.y);
|
|
itemRect.sizeDelta = new Vector2(itemRect.sizeDelta.x, itemSize.y);
|
|
}
|
|
|
|
// Fade in the popup
|
|
AlphaFadeList(0.15f, 0f, 1f);
|
|
|
|
// Make drop-down template and item template inactive
|
|
m_Template.gameObject.SetActive(false);
|
|
itemTemplate.gameObject.SetActive(false);
|
|
|
|
m_Blocker = CreateBlocker(rootCanvas);
|
|
}
|
|
|
|
protected virtual GameObject CreateBlocker(Canvas rootCanvas)
|
|
{
|
|
// Create blocker GameObject.
|
|
GameObject blocker = new GameObject("Blocker");
|
|
|
|
// Setup blocker RectTransform to cover entire root canvas area.
|
|
RectTransform blockerRect = blocker.AddComponent<RectTransform>();
|
|
blockerRect.SetParent(rootCanvas.transform, false);
|
|
blockerRect.anchorMin = Vector3.zero;
|
|
blockerRect.anchorMax = Vector3.one;
|
|
blockerRect.sizeDelta = Vector2.zero;
|
|
|
|
// Make blocker be in separate canvas in same layer as dropdown and in layer just below it.
|
|
Canvas blockerCanvas = blocker.AddComponent<Canvas>();
|
|
blockerCanvas.overrideSorting = true;
|
|
Canvas dropdownCanvas = m_Dropdown.GetComponent<Canvas>();
|
|
blockerCanvas.sortingLayerID = dropdownCanvas.sortingLayerID;
|
|
blockerCanvas.sortingOrder = dropdownCanvas.sortingOrder - 1;
|
|
|
|
// Add raycaster since it's needed to block.
|
|
blocker.AddComponent<GraphicRaycaster>();
|
|
|
|
// Add image since it's needed to block, but make it clear.
|
|
Image blockerImage = blocker.AddComponent<Image>();
|
|
blockerImage.color = Color.clear;
|
|
|
|
// Add button since it's needed to block, and to close the dropdown when blocking area is clicked.
|
|
Button blockerButton = blocker.AddComponent<Button>();
|
|
blockerButton.onClick.AddListener(Hide);
|
|
|
|
return blocker;
|
|
}
|
|
|
|
protected virtual void DestroyBlocker(GameObject blocker)
|
|
{
|
|
Destroy(blocker);
|
|
}
|
|
|
|
protected virtual GameObject CreateDropdownList(GameObject template)
|
|
{
|
|
return (GameObject)Instantiate(template);
|
|
}
|
|
|
|
protected virtual void DestroyDropdownList(GameObject dropdownList)
|
|
{
|
|
Destroy(dropdownList);
|
|
}
|
|
|
|
protected virtual DropdownItem CreateItem(DropdownItem itemTemplate)
|
|
{
|
|
return (DropdownItem)Instantiate(itemTemplate);
|
|
}
|
|
|
|
protected virtual void DestroyItem(DropdownItem item)
|
|
{
|
|
// No action needed since destroying the dropdown list destroys all contained items as well.
|
|
}
|
|
|
|
// Add a new drop-down list item with the specified values.
|
|
private DropdownItem AddItem(OptionData data, bool selected, DropdownItem itemTemplate, List<DropdownItem> items)
|
|
{
|
|
// Add a new item to the dropdown.
|
|
DropdownItem item = CreateItem(itemTemplate);
|
|
item.rectTransform.SetParent(itemTemplate.rectTransform.parent, false);
|
|
|
|
item.gameObject.SetActive(true);
|
|
item.gameObject.name = "Item " + items.Count + (data.text != null ? ": " + data.text : "");
|
|
|
|
if (item.toggle != null)
|
|
{
|
|
item.toggle.isOn = false;
|
|
}
|
|
|
|
// Set the item's data
|
|
if (item.text)
|
|
item.text.text = data.text;
|
|
if (item.image)
|
|
{
|
|
item.image.sprite = data.image;
|
|
item.image.enabled = (item.image.sprite != null);
|
|
}
|
|
|
|
items.Add(item);
|
|
return item;
|
|
}
|
|
|
|
private void AlphaFadeList(float duration, float alpha)
|
|
{
|
|
CanvasGroup group = m_Dropdown.GetComponent<CanvasGroup>();
|
|
AlphaFadeList(duration, group.alpha, alpha);
|
|
}
|
|
|
|
private void AlphaFadeList(float duration, float start, float end)
|
|
{
|
|
if (end.Equals(start))
|
|
return;
|
|
|
|
FloatTween tween = new FloatTween { duration = duration, startValue = start, targetValue = end };
|
|
tween.AddOnChangedCallback(SetAlpha);
|
|
tween.ignoreTimeScale = true;
|
|
m_AlphaTweenRunner.StartTween(tween);
|
|
}
|
|
|
|
private void SetAlpha(float alpha)
|
|
{
|
|
if (!m_Dropdown)
|
|
return;
|
|
CanvasGroup group = m_Dropdown.GetComponent<CanvasGroup>();
|
|
group.alpha = alpha;
|
|
}
|
|
|
|
// Hide the dropdown.
|
|
public void Hide()
|
|
{
|
|
if (m_Dropdown != null)
|
|
{
|
|
AlphaFadeList(0.15f, 0f);
|
|
|
|
// User could have disabled the dropdown during the OnValueChanged call.
|
|
if (IsActive())
|
|
StartCoroutine(DelayedDestroyDropdownList(0.15f));
|
|
}
|
|
if (m_Blocker != null)
|
|
DestroyBlocker(m_Blocker);
|
|
m_Blocker = null;
|
|
Select();
|
|
}
|
|
|
|
private IEnumerator DelayedDestroyDropdownList(float delay)
|
|
{
|
|
|
|
yield return new WaitForSecondsRealtime(delay);
|
|
|
|
for (int i = 0; i < m_Items.Count; i++)
|
|
{
|
|
if (m_Items[i] != null)
|
|
DestroyItem(m_Items[i]);
|
|
m_Items.Clear();
|
|
}
|
|
if (m_Dropdown != null)
|
|
DestroyDropdownList(m_Dropdown);
|
|
m_Dropdown = null;
|
|
}
|
|
|
|
// Change the value and hide the dropdown.
|
|
private void OnSelectItem(Toggle toggle)
|
|
{
|
|
if (!toggle.isOn)
|
|
toggle.isOn = true;
|
|
|
|
int selectedIndex = -1;
|
|
Transform tr = toggle.transform;
|
|
Transform parent = tr.parent;
|
|
for (int i = 0; i < parent.childCount; i++)
|
|
{
|
|
if (parent.GetChild(i) == tr)
|
|
{
|
|
// Subtract one to account for template child.
|
|
selectedIndex = i - 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (selectedIndex < 0)
|
|
return;
|
|
|
|
value = selectedIndex;
|
|
Hide();
|
|
}
|
|
}
|
|
}
|