/* * Copyright 2022 Peter Han * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace PeterHan.PLib.UI { /// /// An improved variant of DropDown/Dropdown. /// internal sealed class PComboBoxComponent : KMonoBehaviour { /// /// The container where the combo box items will be placed. /// internal RectTransform ContentContainer { get; set; } /// /// The color for the checkbox if it is selected. /// internal Color CheckColor { get; set; } /// /// The prefab used to display each row. /// internal GameObject EntryPrefab { get; set; } /// /// The maximum number of rows to be shown. /// internal int MaxRowsShown { get; set; } /// /// Called when an item is selected. /// internal Action OnSelectionChanged { get; set; } /// /// The object which contains the pull down section of the combo box. /// internal GameObject Pulldown { get; set; } /// /// The selected label. /// internal TMP_Text SelectedLabel { get; set; } /// /// The items which are currently shown in this combo box. /// private readonly IList currentItems; /// /// The currently active mouse event handler, or null if not yet configured. /// private MouseEventHandler handler; /// /// Whether the combo box is expanded. /// private bool open; internal PComboBoxComponent() { CheckColor = Color.white; currentItems = new List(32); handler = null; MaxRowsShown = 8; open = false; } /// /// Closes the pulldown. The selected choice is not saved. /// public void Close() { Pulldown?.SetActive(false); open = false; } /// /// Triggered when the combo box is clicked. /// public void OnClick() { if (open) Close(); else Open(); } protected override void OnPrefabInit() { base.OnPrefabInit(); handler = gameObject.AddOrGet(); } /// /// Opens the pulldown. /// public void Open() { var pdn = Pulldown; if (pdn != null) { float rowHeight = 0.0f; int itemCount = currentItems.Count, rows = Math.Min(MaxRowsShown, itemCount); var canvas = pdn.AddOrGet(); pdn.SetActive(true); // Calculate desired height of scroll pane if (itemCount > 0) { var rt = currentItems[0].rowInstance.rectTransform(); LayoutRebuilder.ForceRebuildLayoutImmediate(rt); rowHeight = LayoutUtility.GetPreferredHeight(rt); } // Update size and enable/disable scrolling if not needed pdn.rectTransform().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rows * rowHeight); if (pdn.TryGetComponent(out ScrollRect sp)) sp.vertical = itemCount >= MaxRowsShown; // Move above normal UI elements but below the tooltips if (canvas != null) { canvas.overrideSorting = true; canvas.sortingOrder = 2; } } open = true; } /// /// Sets the items which will be shown in this combo box. /// /// The items to show. public void SetItems(IEnumerable items) { if (items != null) { var content = ContentContainer; var pdn = Pulldown; int n = content.childCount; var prefab = EntryPrefab; bool wasOpen = open; if (wasOpen) Close(); // Destroy only destroys it at end of frame! for (int i = 0; i < n; i++) Destroy(content.GetChild(i)); currentItems.Clear(); foreach (var item in items) { string tooltip = ""; var rowInstance = Util.KInstantiate(prefab, content.gameObject); // Update the text shown rowInstance.GetComponentInChildren().SetText(item. GetProperName()); // Apply the listener for the button var button = rowInstance.GetComponentInChildren(); button.ClearOnClick(); button.onClick += () => { SetSelectedItem(item, true); Close(); }; // Assign the tooltip if possible if (item is ITooltipListableOption extended) tooltip = extended.GetToolTipText(); if (rowInstance.TryGetComponent(out ToolTip tt)) { if (string.IsNullOrEmpty(tooltip)) tt.ClearMultiStringTooltip(); else tt.SetSimpleTooltip(tooltip); } rowInstance.SetActive(true); currentItems.Add(new ComboBoxItem(item, rowInstance)); } SelectedLabel?.SetText((currentItems.Count > 0) ? currentItems[0].data. GetProperName() : ""); if (wasOpen) Open(); } } /// /// Sets the selected item in the combo box. /// /// The option that was chosen. /// true to also fire the option selected listener, or false otherwise. public void SetSelectedItem(IListableOption option, bool fireListener = false) { if (option != null) { SelectedLabel?.SetText(option.GetProperName()); // No guarantee that the options are hashable foreach (var item in currentItems) { var data = item.data; // Show or hide the check mark next to the selected option item.rowImage.color = (data != null && data.Equals(option)) ? CheckColor : PUITuning.Colors.Transparent; } if (fireListener) OnSelectionChanged?.Invoke(this, option); } } /// /// Called each frame by Unity, checks to see if the user clicks/scrolls outside of /// the dropdown while open, and closes it if so. /// internal void Update() { if (open && handler != null && !handler.IsOver && (Input.GetMouseButton(0) || Input.GetAxis("Mouse ScrollWheel") != 0.0f)) Close(); } /// /// The items in a combo box, paired with the game object owning that row and the /// object that goes there. /// private struct ComboBoxItem { public readonly IListableOption data; public readonly Image rowImage; public readonly GameObject rowInstance; public ComboBoxItem(IListableOption data, GameObject rowInstance) { this.data = data; this.rowInstance = rowInstance; rowImage = rowInstance.GetComponentInChildrenOnly(); } } /// /// Handles mouse events on the pulldown. /// private sealed class MouseEventHandler : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler { /// /// Whether the mouse is over this component. /// public bool IsOver { get; private set; } internal MouseEventHandler() { IsOver = true; } public void OnPointerEnter(PointerEventData data) { IsOver = true; } public void OnPointerExit(PointerEventData data) { IsOver = false; } } } }