UnityGame/Library/PackageCache/com.unity.ugui/Runtime/TMP/TMP_Dropdown.cs
2024-10-27 10:53:47 +03:00

1326 lines
51 KiB
C#

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/Dropdown - TextMeshPro", 35)]
[RequireComponent(typeof(RectTransform))]
/// <summary>
/// A standard dropdown that presents a list of options when clicked, of which one can be chosen.
/// </summary>
/// <remarks>
/// The dropdown component is a Selectable. When an option is chosen, the label and/or image of the control changes to show the chosen option.
///
/// When a dropdown event occurs a callback is sent to any registered listeners of onValueChanged.
/// </remarks>
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]
/// <summary>
/// Class to store the text and/or image of a single option in the dropdown list.
/// </summary>
public class OptionData
{
[SerializeField]
private string m_Text;
[SerializeField]
private Sprite m_Image;
[SerializeField]
private Color m_Color = Color.white;
/// <summary>
/// The text associated with the option.
/// </summary>
public string text { get { return m_Text; } set { m_Text = value; } }
/// <summary>
/// The image associated with the option.
/// </summary>
public Sprite image { get { return m_Image; } set { m_Image = value; } }
/// <summary>
/// The color associated with the option.
/// </summary>
public Color color { get { return m_Color; } set { m_Color = value; } }
public OptionData() { }
public OptionData(string text)
{
this.text = text;
}
public OptionData(Sprite image)
{
this.image = image;
}
/// <summary>
/// Create an object representing a single option for the dropdown list.
/// </summary>
/// <param name="text">Optional text for the option.</param>
/// <param name="image">Optional image for the option.</param>
/// <param name="image">Optional color for the option.</param>
public OptionData(string text, Sprite image, Color color)
{
this.text = text;
this.image = image;
this.color = color;
}
}
[Serializable]
/// <summary>
/// Class used internally to store the list of options for the dropdown list.
/// </summary>
/// <remarks>
/// The usage of this class is not exposed in the runtime API. It's only relevant for the PropertyDrawer drawing the list of options.
/// </remarks>
public class OptionDataList
{
[SerializeField]
private List<OptionData> m_Options;
/// <summary>
/// The list of options for the dropdown list.
/// </summary>
public List<OptionData> options { get { return m_Options; } set { m_Options = value; } }
public OptionDataList()
{
options = new List<OptionData>();
}
}
[Serializable]
/// <summary>
/// UnityEvent callback for when a dropdown current option is changed.
/// </summary>
public class DropdownEvent : UnityEvent<int> { }
static readonly OptionData k_NothingOption = new OptionData { text = "Nothing" };
static readonly OptionData k_EverythingOption = new OptionData { text = "Everything" };
static readonly OptionData k_MixedOption = new OptionData { text = "Mixed..." };
// Template used to create the dropdown.
[SerializeField]
private RectTransform m_Template;
/// <summary>
/// The Rect Transform of the template for the dropdown list.
/// </summary>
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;
/// <summary>
/// The Text component to hold the text of the currently selected option.
/// </summary>
public TMP_Text captionText { get { return m_CaptionText; } set { m_CaptionText = value; RefreshShownValue(); } }
[SerializeField]
private Image m_CaptionImage;
/// <summary>
/// The Image component to hold the image of the currently selected option.
/// </summary>
public Image captionImage { get { return m_CaptionImage; } set { m_CaptionImage = value; RefreshShownValue(); } }
[SerializeField]
private Graphic m_Placeholder;
/// <summary>
/// The placeholder Graphic component. Shown when no option is selected.
/// </summary>
public Graphic placeholder { get { return m_Placeholder; } set { m_Placeholder = value; RefreshShownValue(); } }
[Space]
[SerializeField]
private TMP_Text m_ItemText;
/// <summary>
/// The Text component to hold the text of the item.
/// </summary>
public TMP_Text itemText { get { return m_ItemText; } set { m_ItemText = value; RefreshShownValue(); } }
[SerializeField]
private Image m_ItemImage;
/// <summary>
/// The Image component to hold the image of the item
/// </summary>
public Image itemImage { get { return m_ItemImage; } set { m_ItemImage = value; RefreshShownValue(); } }
[Space]
[SerializeField]
private int m_Value;
[SerializeField]
private bool m_MultiSelect;
[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();
/// <summary>
/// The list of possible options. A text string and an image can be specified for each option.
/// </summary>
/// <remarks>
/// This is the list of options within the Dropdown. Each option contains Text and/or image data that you can specify using UI.Dropdown.OptionData before adding to the Dropdown list.
/// This also unlocks the ability to edit the Dropdown, including the insertion, removal, and finding of options, as well as other useful tools
/// </remarks>
/// /// <example>
/// <code>
/// //Create a new Dropdown GameObject by going to the Hierarchy and clicking Create>UI>Dropdown - TextMeshPro. Attach this script to the Dropdown GameObject.
///
/// using UnityEngine;
/// using UnityEngine.UI;
/// using System.Collections.Generic;
/// using TMPro;
///
/// public class Example : MonoBehaviour
/// {
/// //Use these for adding options to the Dropdown List
/// TMP_Dropdown.OptionData m_NewData, m_NewData2;
/// //The list of messages for the Dropdown
/// List<TMP_Dropdown.OptionData> m_Messages = new List<TMP_Dropdown.OptionData>();
///
///
/// //This is the Dropdown
/// TMP_Dropdown m_Dropdown;
/// string m_MyString;
/// int m_Index;
///
/// void Start()
/// {
/// //Fetch the Dropdown GameObject the script is attached to
/// m_Dropdown = GetComponent<TMP_Dropdown>();
/// //Clear the old options of the Dropdown menu
/// m_Dropdown.ClearOptions();
///
/// //Create a new option for the Dropdown menu which reads "Option 1" and add to messages List
/// m_NewData = new TMP_Dropdown.OptionData();
/// m_NewData.text = "Option 1";
/// m_Messages.Add(m_NewData);
///
/// //Create a new option for the Dropdown menu which reads "Option 2" and add to messages List
/// m_NewData2 = new TMP_Dropdown.OptionData();
/// m_NewData2.text = "Option 2";
/// m_Messages.Add(m_NewData2);
///
/// //Take each entry in the message List
/// foreach (TMP_Dropdown.OptionData message in m_Messages)
/// {
/// //Add each entry to the Dropdown
/// m_Dropdown.options.Add(message);
/// //Make the index equal to the total number of entries
/// m_Index = m_Messages.Count - 1;
/// }
/// }
///
/// //This OnGUI function is used here for a quick demonstration. See the [[wiki:UISystem|UI Section]] for more information about setting up your own UI.
/// void OnGUI()
/// {
/// //TextField for user to type new entry to add to Dropdown
/// m_MyString = GUI.TextField(new Rect(0, 40, 100, 40), m_MyString);
///
/// //Press the "Add" Button to add a new entry to the Dropdown
/// if (GUI.Button(new Rect(0, 0, 100, 40), "Add"))
/// {
/// //Make the index the last number of entries
/// m_Index = m_Messages.Count;
/// //Create a temporary option
/// TMP_Dropdown.OptionData temp = new TMP_Dropdown.OptionData();
/// //Make the option the data from the TextField
/// temp.text = m_MyString;
///
/// //Update the messages list with the TextField data
/// m_Messages.Add(temp);
///
/// //Add the Textfield data to the Dropdown
/// m_Dropdown.options.Insert(m_Index, temp);
/// }
///
/// //Press the "Remove" button to delete the selected option
/// if (GUI.Button(new Rect(110, 0, 100, 40), "Remove"))
/// {
/// //Remove the current selected item from the Dropdown from the messages List
/// m_Messages.RemoveAt(m_Dropdown.value);
/// //Remove the current selection from the Dropdown
/// m_Dropdown.options.RemoveAt(m_Dropdown.value);
/// }
/// }
/// }
/// </code>
/// </example>
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();
/// <summary>
/// A UnityEvent that is invoked when a user has clicked one of the options in the dropdown list.
/// </summary>
/// <remarks>
/// Use this to detect when a user selects one or more options in the Dropdown. Add a listener to perform an action when this UnityEvent detects a selection by the user. See https://unity3d.com/learn/tutorials/topics/scripting/delegates for more information on delegates.
/// </remarks>
/// <example>
/// <code>
/// //Create a new Dropdown GameObject by going to the Hierarchy and clicking Create>UI>Dropdown - TextMeshPro. Attach this script to the Dropdown GameObject.
/// //Set your own Text in the Inspector window
///
/// using UnityEngine;
/// using UnityEngine.UI;
/// using TMPro;
///
/// public class Example : MonoBehaviour
/// {
/// TMP_Dropdown m_Dropdown;
/// public Text m_Text;
///
/// void Start()
/// {
/// //Fetch the Dropdown GameObject
/// m_Dropdown = GetComponent<TMP_Dropdown>();
/// //Add listener for when the value of the Dropdown changes, to take action
/// m_Dropdown.onValueChanged.AddListener(delegate {
/// DropdownValueChanged(m_Dropdown);
/// });
///
/// //Initialize the Text to say the first value of the Dropdown
/// m_Text.text = "First Value : " + m_Dropdown.value;
/// }
///
/// //Output the new value of the Dropdown into Text
/// void DropdownValueChanged(TMP_Dropdown change)
/// {
/// m_Text.text = "New Value : " + change.value;
/// }
/// }
/// </code>
/// </example>
public DropdownEvent onValueChanged { get { return m_OnValueChanged; } set { m_OnValueChanged = value; } }
[SerializeField]
private float m_AlphaFadeSpeed = 0.15f;
/// <summary>
/// The time interval at which a drop down will appear and disappear
/// </summary>
public float alphaFadeSpeed { get { return m_AlphaFadeSpeed; } set { m_AlphaFadeSpeed = 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 Coroutine m_Coroutine = null;
private static OptionData s_NoOptionData = new OptionData();
/// <summary>
/// The Value is the index number of the current selection in the Dropdown. 0 is the first option in the Dropdown, 1 is the second, and so on.
/// </summary>
/// <example>
/// <code>
/// //Create a new Dropdown GameObject by going to the Hierarchy and clicking Create>UI>Dropdown - TextMeshPro. Attach this script to the Dropdown GameObject.
/// //Set your own Text in the Inspector window
///
/// using UnityEngine;
/// using UnityEngine.UI;
/// using TMPro;
///
/// public class Example : MonoBehaviour
/// {
/// //Attach this script to a Dropdown GameObject
/// TMP_Dropdown m_Dropdown;
/// //This is the string that stores the current selection m_Text of the Dropdown
/// string m_Message;
/// //This Text outputs the current selection to the screen
/// public Text m_Text;
/// //This is the index value of the Dropdown
/// int m_DropdownValue;
///
/// void Start()
/// {
/// //Fetch the DropDown component from the GameObject
/// m_Dropdown = GetComponent<TMP_Dropdown>();
/// //Output the first Dropdown index value
/// Debug.Log("Starting Dropdown Value : " + m_Dropdown.value);
/// }
///
/// void Update()
/// {
/// //Keep the current index of the Dropdown in a variable
/// m_DropdownValue = m_Dropdown.value;
/// //Change the message to say the name of the current Dropdown selection using the value
/// m_Message = m_Dropdown.options[m_DropdownValue].text;
/// //Change the on screen Text to reflect the current Dropdown selection
/// m_Text.text = m_Message;
/// }
/// }
/// </code>
/// </example>
public int value
{
get
{
return m_Value;
}
set
{
SetValue(value);
}
}
/// <summary>
/// Set index number of the current selection in the Dropdown without invoking onValueChanged callback.
/// </summary>
/// <param name="input">The new index for the current selection.</param>
public void SetValueWithoutNotify(int input)
{
SetValue(input, false);
}
void SetValue(int value, bool sendCallback = true)
{
if (Application.isPlaying && (value == m_Value || options.Count == 0))
return;
if (m_MultiSelect)
m_Value = value;
else
m_Value = Mathf.Clamp(value, m_Placeholder ? -1 : 0, options.Count - 1);
RefreshShownValue();
if (sendCallback)
{
// Notify all listeners
UISystemProfilerApi.AddMarker("Dropdown.value", this);
m_OnValueChanged.Invoke(m_Value);
}
}
public bool IsExpanded { get { return m_Dropdown != null; } }
public bool MultiSelect { get { return m_MultiSelect; } set { m_MultiSelect = value; } }
protected TMP_Dropdown() { }
protected override void Awake()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
return;
#endif
if (m_CaptionImage)
m_CaptionImage.enabled = (m_CaptionImage.sprite != null && m_CaptionImage.color.a > 0);
if (m_Template)
m_Template.gameObject.SetActive(false);
}
protected override void Start()
{
m_AlphaTweenRunner = new TweenRunner<FloatTween>();
m_AlphaTweenRunner.Init(this);
base.Start();
RefreshShownValue();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (!IsActive())
return;
RefreshShownValue();
}
#endif
protected override void OnDisable()
{
//Destroy dropdown and blocker in case user deactivates the dropdown when they click an option (case 935649)
ImmediateDestroyDropdownList();
if (m_Blocker != null)
DestroyBlocker(m_Blocker);
m_Blocker = null;
base.OnDisable();
}
/// <summary>
/// Refreshes the text and image (if available) of the currently selected option.
/// </summary>
/// <remarks>
/// If you have modified the list of options, you should call this method afterwards to ensure that the visual state of the dropdown corresponds to the updated options.
/// </remarks>
public void RefreshShownValue()
{
OptionData data = s_NoOptionData;
if (options.Count > 0)
{
if (m_MultiSelect)
{
int firstActiveFlag = FirstActiveFlagIndex(m_Value);
if (m_Value == 0 || firstActiveFlag >= options.Count)
data = k_NothingOption;
else if (IsEverythingValue(options.Count, m_Value))
data = k_EverythingOption;
else if (Mathf.IsPowerOfTwo(m_Value) && m_Value > 0)
data = options[firstActiveFlag];
else
data = k_MixedOption;
}
else if (m_Value >= 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)
{
m_CaptionImage.sprite = data.image;
m_CaptionImage.color = data.color;
m_CaptionImage.enabled = (m_CaptionImage.sprite != null && m_CaptionImage.color.a > 0);
}
if (m_Placeholder)
{
m_Placeholder.enabled = options.Count == 0 || m_Value == -1;
}
}
/// <summary>
/// Add multiple options to the options of the Dropdown based on a list of OptionData objects.
/// </summary>
/// <param name="options">The list of OptionData to add.</param>
/// /// <remarks>
/// See AddOptions(List<string> options) for code example of usages.
/// </remarks>
public void AddOptions(List<OptionData> options)
{
this.options.AddRange(options);
RefreshShownValue();
}
/// <summary>
/// Add multiple text-only options to the options of the Dropdown based on a list of strings.
/// </summary>
/// <remarks>
/// Add a List of string messages to the Dropdown. The Dropdown shows each member of the list as a separate option.
/// </remarks>
/// <param name="options">The list of text strings to add.</param>
/// <example>
/// <code>
/// //Create a new Dropdown GameObject by going to the Hierarchy and clicking Create>UI>Dropdown - TextMeshPro. Attach this script to the Dropdown GameObject.
///
/// using System.Collections.Generic;
/// using UnityEngine;
/// using UnityEngine.UI;
/// using TMPro;
///
/// public class Example : MonoBehaviour
/// {
/// //Create a List of new Dropdown options
/// List<string> m_DropOptions = new List<string> { "Option 1", "Option 2"};
/// //This is the Dropdown
/// TMP_Dropdown m_Dropdown;
///
/// void Start()
/// {
/// //Fetch the Dropdown GameObject the script is attached to
/// m_Dropdown = GetComponent<TMP_Dropdown>();
/// //Clear the old options of the Dropdown menu
/// m_Dropdown.ClearOptions();
/// //Add the options created in the List above
/// m_Dropdown.AddOptions(m_DropOptions);
/// }
/// }
/// </code>
/// </example>
public void AddOptions(List<string> options)
{
for (int i = 0; i < options.Count; i++)
this.options.Add(new OptionData(options[i]));
RefreshShownValue();
}
/// <summary>
/// Add multiple image-only options to the options of the Dropdown based on a list of Sprites.
/// </summary>
/// <param name="options">The list of Sprites to add.</param>
/// <remarks>
/// See AddOptions(List<string> options) for code example of usages.
/// </remarks>
public void AddOptions(List<Sprite> options)
{
for (int i = 0; i < options.Count; i++)
this.options.Add(new OptionData(options[i]));
RefreshShownValue();
}
/// <summary>
/// Clear the list of options in the Dropdown.
/// </summary>
public void ClearOptions()
{
options.Clear();
m_Value = m_Placeholder ? -1 : 0;
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;
// Find the Canvas that this dropdown is a part of
Canvas parentCanvas = null;
Transform parentTransform = m_Template.parent;
while (parentTransform != null)
{
parentCanvas = parentTransform.GetComponent<Canvas>();
if (parentCanvas != null)
break;
parentTransform = parentTransform.parent;
}
Canvas popupCanvas = GetOrAddComponent<Canvas>(templateGo);
popupCanvas.overrideSorting = true;
popupCanvas.sortingOrder = 30000;
// If we have a parent canvas, apply the same raycasters as the parent for consistency.
if (parentCanvas != null)
{
Component[] components = parentCanvas.GetComponents<BaseRaycaster>();
for (int i = 0; i < components.Length; i++)
{
Type raycasterType = components[i].GetType();
if (templateGo.GetComponent(raycasterType) == null)
{
templateGo.AddComponent(raycasterType);
}
}
}
else
{
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;
}
/// <summary>
/// Handling for when the dropdown is initially 'clicked'. Typically shows the dropdown
/// </summary>
/// <param name="eventData">The associated event data.</param>
public virtual void OnPointerClick(PointerEventData eventData)
{
Show();
}
/// <summary>
/// Handling for when the dropdown is selected and a submit event is processed. Typically shows the dropdown
/// </summary>
/// <param name="eventData">The associated event data.</param>
public virtual void OnSubmit(BaseEventData eventData)
{
Show();
}
/// <summary>
/// This will hide the dropdown list.
/// </summary>
/// <remarks>
/// Called by a BaseInputModule when a Cancel event occurs.
/// </remarks>
/// <param name="eventData">The associated event data.</param>
public virtual void OnCancel(BaseEventData eventData)
{
Hide();
}
/// <summary>
/// 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 contain dropdowns.
/// </summary>
public void Show()
{
if (m_Coroutine != null)
{
StopCoroutine(m_Coroutine);
ImmediateDestroyDropdownList();
}
if (!IsActive() || !IsInteractable() || m_Dropdown != null)
return;
// Get root Canvas.
var list = TMP_ListPool<Canvas>.Get();
gameObject.GetComponentsInParent(false, list);
if (list.Count == 0)
return;
Canvas rootCanvas = list[list.Count - 1];
for (int i = 0; i < list.Count; i++)
{
if (list[i].isRootCanvas)
{
rootCanvas = list[i];
break;
}
}
TMP_ListPool<Canvas>.Release(list);
if (!validTemplate)
{
SetupTemplate();
if (!validTemplate)
return;
}
m_Template.gameObject.SetActive(true);
// popupCanvas used to assume the root canvas had the default sorting Layer, next line fixes (case 958281 - [UI] Dropdown list does not copy the parent canvas layer when the panel is opened)
m_Template.GetComponent<Canvas>().sortingLayerID = rootCanvas.sortingLayerID;
// 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;
if (m_MultiSelect && options.Count > 0)
{
DropdownItem item = AddItem(k_NothingOption, value == 0, itemTemplate, m_Items);
if (item.image != null)
item.image.gameObject.SetActive(false);
Toggle nothingToggle = item.toggle;
nothingToggle.isOn = value == 0;
nothingToggle.onValueChanged.AddListener(x => OnSelectItem(nothingToggle));
prev = nothingToggle;
bool isEverythingValue = IsEverythingValue(options.Count, value);
item = AddItem(k_EverythingOption, isEverythingValue, itemTemplate, m_Items);
if (item.image != null)
item.image.gameObject.SetActive(false);
Toggle everythingToggle = item.toggle;
everythingToggle.isOn = isEverythingValue;
everythingToggle.onValueChanged.AddListener(x => OnSelectItem(everythingToggle));
// 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;
}
}
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
if (m_MultiSelect)
item.toggle.isOn = (value & (1 << i)) != 0;
else
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] && !Mathf.Approximately(corner[axis], rootCanvasRect.min[axis])) ||
(corner[axis] > rootCanvasRect.max[axis] && !Mathf.Approximately(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(m_AlphaFadeSpeed, 0f, 1f);
// Make drop-down template and item template inactive
m_Template.gameObject.SetActive(false);
itemTemplate.gameObject.SetActive(false);
m_Blocker = CreateBlocker(rootCanvas);
}
static bool IsEverythingValue(int count, int value)
{
var result = true;
for (var i = 0; i < count; i++)
{
if ((value & 1 << i) == 0)
result = false;
}
return result;
}
static int EverythingValue(int count)
{
int result = 0;
for (var i = 0; i < count; i++)
{
result |= 1 << i;
}
return result;
}
/// <summary>
/// Create a blocker that blocks clicks to other controls while the dropdown list is open.
/// </summary>
/// <remarks>
/// Override this method to implement a different way to obtain a blocker GameObject.
/// </remarks>
/// <param name="rootCanvas">The root canvas the dropdown is under.</param>
/// <returns>The created blocker object</returns>
protected virtual GameObject CreateBlocker(Canvas rootCanvas)
{
// Create blocker GameObject.
GameObject blocker = new GameObject("Blocker");
// Set the game object layer to match the Canvas' game object layer, as not doing this can lead to issues
// especially in XR applications like PolySpatial on VisionOS (UUM-62470).
blocker.layer = rootCanvas.gameObject.layer;
// 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;
// Find the Canvas that this dropdown is a part of
Canvas parentCanvas = null;
Transform parentTransform = m_Template.parent;
while (parentTransform != null)
{
parentCanvas = parentTransform.GetComponent<Canvas>();
if (parentCanvas != null)
break;
parentTransform = parentTransform.parent;
}
// If we have a parent canvas, apply the same raycasters as the parent for consistency.
if (parentCanvas != null)
{
Component[] components = parentCanvas.GetComponents<BaseRaycaster>();
for (int i = 0; i < components.Length; i++)
{
Type raycasterType = components[i].GetType();
if (blocker.GetComponent(raycasterType) == null)
{
blocker.AddComponent(raycasterType);
}
}
}
else
{
// Add raycaster since it's needed to block.
GetOrAddComponent<GraphicRaycaster>(blocker);
}
// 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);
//add canvas group to ensure clicking outside the dropdown will hide it (UUM-33691)
CanvasGroup blockerCanvasGroup = blocker.AddComponent<CanvasGroup>();
blockerCanvasGroup.ignoreParentGroups = true;
return blocker;
}
/// <summary>
/// Convenience method to explicitly destroy the previously generated blocker object
/// </summary>
/// <remarks>
/// Override this method to implement a different way to dispose of a blocker GameObject that blocks clicks to other controls while the dropdown list is open.
/// </remarks>
/// <param name="blocker">The blocker object to destroy.</param>
protected virtual void DestroyBlocker(GameObject blocker)
{
Destroy(blocker);
}
/// <summary>
/// Create the dropdown list to be shown when the dropdown is clicked. The dropdown list should correspond to the provided template GameObject, equivalent to instantiating a copy of it.
/// </summary>
/// <remarks>
/// Override this method to implement a different way to obtain a dropdown list GameObject.
/// </remarks>
/// <param name="template">The template to create the dropdown list from.</param>
/// <returns>The created drop down list gameobject.</returns>
protected virtual GameObject CreateDropdownList(GameObject template)
{
return (GameObject)Instantiate(template);
}
/// <summary>
/// Convenience method to explicitly destroy the previously generated dropdown list
/// </summary>
/// <remarks>
/// Override this method to implement a different way to dispose of a dropdown list GameObject.
/// </remarks>
/// <param name="dropdownList">The dropdown list GameObject to destroy</param>
protected virtual void DestroyDropdownList(GameObject dropdownList)
{
Destroy(dropdownList);
}
/// <summary>
/// Create a dropdown item based upon the item template.
/// </summary>
/// <remarks>
/// Override this method to implement a different way to obtain an option item.
/// The option item should correspond to the provided template DropdownItem and its GameObject, equivalent to instantiating a copy of it.
/// </remarks>
/// <param name="itemTemplate">e template to create the option item from.</param>
/// <returns>The created dropdown item component</returns>
protected virtual DropdownItem CreateItem(DropdownItem itemTemplate)
{
return (DropdownItem)Instantiate(itemTemplate);
}
/// <summary>
/// Convenience method to explicitly destroy the previously generated Items.
/// </summary>
/// <remarks>
/// Override this method to implement a different way to dispose of an option item.
/// Likely no action needed since destroying the dropdown list destroys all contained items as well.
/// </remarks>
/// <param name="item">The Item to destroy.</param>
protected virtual void DestroyItem(DropdownItem item) { }
// 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.color = data.color;
item.image.enabled = (item.image.sprite != null && data.color.a > 0);
}
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;
}
/// <summary>
/// Hide the dropdown list. I.e. close it.
/// </summary>
public void Hide()
{
if (m_Coroutine == null)
{
if (m_Dropdown != null)
{
AlphaFadeList(m_AlphaFadeSpeed, 0f);
// User could have disabled the dropdown during the OnValueChanged call.
if (IsActive())
m_Coroutine = StartCoroutine(DelayedDestroyDropdownList(m_AlphaFadeSpeed));
}
if (m_Blocker != null)
DestroyBlocker(m_Blocker);
m_Blocker = null;
Select();
}
}
private IEnumerator DelayedDestroyDropdownList(float delay)
{
yield return new WaitForSecondsRealtime(delay);
ImmediateDestroyDropdownList();
}
private void ImmediateDestroyDropdownList()
{
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);
if (m_AlphaTweenRunner != null)
m_AlphaTweenRunner.StopTween();
m_Dropdown = null;
m_Coroutine = null;
}
// Change the value and hide the dropdown.
private void OnSelectItem(Toggle toggle)
{
int selectedIndex = -1;
Transform tr = toggle.transform;
Transform parent = tr.parent;
for (int i = 1; i < parent.childCount; i++)
{
if (parent.GetChild(i) == tr)
{
// Subtract one to account for template child.
selectedIndex = i - 1;
break;
}
}
if (selectedIndex < 0)
return;
if (m_MultiSelect)
{
switch (selectedIndex)
{
case 0: // Nothing
value = 0;
for (var i = 3; i < parent.childCount; i++)
{
var toggleComponent = parent.GetChild(i).GetComponentInChildren<Toggle>();
if (toggleComponent)
toggleComponent.SetIsOnWithoutNotify(false);
}
toggle.isOn = true;
break;
case 1: // Everything
value = EverythingValue(options.Count);
for (var i = 3; i < parent.childCount; i++)
{
var toggleComponent = parent.GetChild(i).GetComponentInChildren<Toggle>();
if (toggleComponent)
toggleComponent.SetIsOnWithoutNotify(i > 2);
}
break;
default:
var flagValue = 1 << (selectedIndex - 2);
var wasSelected = (value & flagValue) != 0;
toggle.SetIsOnWithoutNotify(!wasSelected);
if (wasSelected)
value &= ~flagValue;
else
value |= flagValue;
break;
}
}
else
{
if (!toggle.isOn)
toggle.SetIsOnWithoutNotify(true);
value = selectedIndex;
}
Hide();
}
static int FirstActiveFlagIndex(int value)
{
if (value == 0)
return 0;
const int bits = sizeof(int) * 8;
for (var i = 0; i < bits; i++)
if ((value & 1 << i) != 0)
return i;
return 0;
}
}
}