AutoCompleteComboBox.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. ///Credit perchik
  2. ///Sourced from - http://forum.unity3d.com/threads/receive-onclick-event-and-pass-it-on-to-lower-ui-elements.293642/
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. namespace UnityEngine.UI.Extensions
  6. {
  7. public enum AutoCompleteSearchType
  8. {
  9. ArraySort,
  10. Linq
  11. }
  12. [RequireComponent(typeof(RectTransform))]
  13. [AddComponentMenu("UI/Extensions/AutoComplete ComboBox")]
  14. public class AutoCompleteComboBox : MonoBehaviour
  15. {
  16. public Color disabledTextColor;
  17. public DropDownListItem SelectedItem { get; private set; } //outside world gets to get this, not set it
  18. /// <summary>
  19. /// Contains the included items. To add and remove items to/from this list, use the <see cref="AddItem(string)"/>,
  20. /// <see cref="RemoveItem(string)"/> and <see cref="SetAvailableOptions(List{string})"/> methods as these also execute
  21. /// the required methods to update to the current collection.
  22. /// </summary>
  23. public List<string> AvailableOptions;
  24. //private bool isInitialized = false;
  25. private bool _isPanelActive = false;
  26. private bool _hasDrawnOnce = false;
  27. private InputField _mainInput;
  28. private RectTransform _inputRT;
  29. //private Button _arrow_Button;
  30. private RectTransform _rectTransform;
  31. private RectTransform _overlayRT;
  32. private RectTransform _scrollPanelRT;
  33. private RectTransform _scrollBarRT;
  34. private RectTransform _slidingAreaRT;
  35. private RectTransform _scrollHandleRT;
  36. private RectTransform _itemsPanelRT;
  37. private Canvas _canvas;
  38. private RectTransform _canvasRT;
  39. private ScrollRect _scrollRect;
  40. private List<string> _panelItems; //items that will get shown in the drop-down
  41. private List<string> _prunedPanelItems; //items that used to show in the drop-down
  42. private Dictionary<string, GameObject> panelObjects;
  43. private GameObject itemTemplate;
  44. public string Text { get; private set; }
  45. [SerializeField]
  46. private float _scrollBarWidth = 20.0f;
  47. public float ScrollBarWidth
  48. {
  49. get { return _scrollBarWidth; }
  50. set
  51. {
  52. _scrollBarWidth = value;
  53. RedrawPanel();
  54. }
  55. }
  56. // private int scrollOffset; //offset of the selected item
  57. // private int _selectedIndex = 0;
  58. [SerializeField]
  59. private int _itemsToDisplay;
  60. public int ItemsToDisplay
  61. {
  62. get { return _itemsToDisplay; }
  63. set
  64. {
  65. _itemsToDisplay = value;
  66. RedrawPanel();
  67. }
  68. }
  69. public bool SelectFirstItemOnStart = false;
  70. [SerializeField]
  71. [Tooltip("Change input text color based on matching items")]
  72. private bool _ChangeInputTextColorBasedOnMatchingItems = false;
  73. public bool InputColorMatching{
  74. get { return _ChangeInputTextColorBasedOnMatchingItems; }
  75. set
  76. {
  77. _ChangeInputTextColorBasedOnMatchingItems = value;
  78. if (_ChangeInputTextColorBasedOnMatchingItems) {
  79. SetInputTextColor ();
  80. }
  81. }
  82. }
  83. public float DropdownOffset = 10f;
  84. public Color ValidSelectionTextColor = Color.green;
  85. public Color MatchingItemsRemainingTextColor = Color.black;
  86. public Color NoItemsRemainingTextColor = Color.red;
  87. public AutoCompleteSearchType autocompleteSearchType = AutoCompleteSearchType.Linq;
  88. [SerializeField]
  89. private bool _displayPanelAbove = false;
  90. private bool _selectionIsValid = false;
  91. [System.Serializable]
  92. public class SelectionChangedEvent : UnityEngine.Events.UnityEvent<string, bool> {
  93. }
  94. [System.Serializable]
  95. public class SelectionTextChangedEvent : UnityEngine.Events.UnityEvent<string> {
  96. }
  97. [System.Serializable]
  98. public class SelectionValidityChangedEvent : UnityEngine.Events.UnityEvent<bool> {
  99. }
  100. // fires when input text is changed;
  101. public SelectionTextChangedEvent OnSelectionTextChanged;
  102. // fires when an Item gets selected / deselected (including when items are added/removed once this is possible)
  103. public SelectionValidityChangedEvent OnSelectionValidityChanged;
  104. // fires in both cases
  105. public SelectionChangedEvent OnSelectionChanged;
  106. public void Awake()
  107. {
  108. Initialize();
  109. }
  110. public void Start()
  111. {
  112. if (SelectFirstItemOnStart && AvailableOptions.Count > 0) {
  113. ToggleDropdownPanel (false);
  114. OnItemClicked (AvailableOptions [0]);
  115. }
  116. RedrawPanel();
  117. }
  118. private bool Initialize()
  119. {
  120. bool success = true;
  121. try
  122. {
  123. _rectTransform = GetComponent<RectTransform>();
  124. _inputRT = _rectTransform.Find("InputField").GetComponent<RectTransform>();
  125. _mainInput = _inputRT.GetComponent<InputField>();
  126. //_arrow_Button = _rectTransform.FindChild ("ArrowBtn").GetComponent<Button> ();
  127. _overlayRT = _rectTransform.Find("Overlay").GetComponent<RectTransform>();
  128. _overlayRT.gameObject.SetActive(false);
  129. _scrollPanelRT = _overlayRT.Find("ScrollPanel").GetComponent<RectTransform>();
  130. _scrollBarRT = _scrollPanelRT.Find("Scrollbar").GetComponent<RectTransform>();
  131. _slidingAreaRT = _scrollBarRT.Find("SlidingArea").GetComponent<RectTransform>();
  132. _scrollHandleRT = _slidingAreaRT.Find("Handle").GetComponent<RectTransform>();
  133. _itemsPanelRT = _scrollPanelRT.Find("Items").GetComponent<RectTransform>();
  134. //itemPanelLayout = itemsPanelRT.gameObject.GetComponent<LayoutGroup>();
  135. _canvas = GetComponentInParent<Canvas>();
  136. _canvasRT = _canvas.GetComponent<RectTransform>();
  137. _scrollRect = _scrollPanelRT.GetComponent<ScrollRect>();
  138. _scrollRect.scrollSensitivity = _rectTransform.sizeDelta.y / 2;
  139. _scrollRect.movementType = ScrollRect.MovementType.Clamped;
  140. _scrollRect.content = _itemsPanelRT;
  141. itemTemplate = _rectTransform.Find("ItemTemplate").gameObject;
  142. itemTemplate.SetActive(false);
  143. }
  144. catch (System.NullReferenceException ex)
  145. {
  146. Debug.LogException(ex);
  147. Debug.LogError("Something is setup incorrectly with the dropdownlist component causing a Null Reference Exception");
  148. success = false;
  149. }
  150. panelObjects = new Dictionary<string, GameObject>();
  151. _prunedPanelItems = new List<string>();
  152. _panelItems = new List<string>();
  153. RebuildPanel();
  154. return success;
  155. }
  156. /// <summary>
  157. /// Adds the item to <see cref="this.AvailableOptions"/> if it is not a duplicate and rebuilds the panel.
  158. /// </summary>
  159. /// <param name="item">Item to add.</param>
  160. public void AddItem(string item)
  161. {
  162. if (!this.AvailableOptions.Contains(item))
  163. {
  164. this.AvailableOptions.Add(item);
  165. this.RebuildPanel();
  166. }
  167. else
  168. {
  169. Debug.LogWarning($"{nameof(AutoCompleteComboBox)}.{nameof(AddItem)}: items may only exists once. '{item}' can not be added.");
  170. }
  171. }
  172. /// <summary>
  173. /// Removes the item from <see cref="this.AvailableOptions"/> and rebuilds the panel.
  174. /// </summary>
  175. /// <param name="item">Item to remove.</param>
  176. public void RemoveItem(string item)
  177. {
  178. if (this.AvailableOptions.Contains(item))
  179. {
  180. this.AvailableOptions.Remove(item);
  181. this.RebuildPanel();
  182. }
  183. }
  184. /// <summary>
  185. /// Sets the given items as new content for the comboBox. Previous entries will be cleared.
  186. /// </summary>
  187. /// <param name="newOptions">New entries.</param>
  188. public void SetAvailableOptions(List<string> newOptions)
  189. {
  190. var uniqueOptions = newOptions.Distinct().ToList();
  191. if (newOptions.Count != uniqueOptions.Count)
  192. {
  193. Debug.LogWarning($"{nameof(AutoCompleteComboBox)}.{nameof(SetAvailableOptions)}: items may only exists once. {newOptions.Count - uniqueOptions.Count} duplicates.");
  194. }
  195. this.AvailableOptions.Clear();
  196. this.AvailableOptions = uniqueOptions;
  197. this.RebuildPanel();
  198. }
  199. /// <summary>
  200. /// Sets the given items as new content for the comboBox. Previous entries will be cleared.
  201. /// </summary>
  202. /// <param name="newOptions">New entries.</param>
  203. public void SetAvailableOptions(string[] newOptions)
  204. {
  205. var uniqueOptions = newOptions.Distinct().ToList();
  206. if (newOptions.Length != uniqueOptions.Count)
  207. {
  208. Debug.LogWarning($"{nameof(AutoCompleteComboBox)}.{nameof(SetAvailableOptions)}: items may only exists once. {newOptions.Length - uniqueOptions.Count} duplicates.");
  209. }
  210. this.AvailableOptions.Clear();
  211. for (int i = 0; i < newOptions.Length; i++)
  212. {
  213. this.AvailableOptions.Add(newOptions[i]);
  214. }
  215. this.RebuildPanel();
  216. }
  217. public void ResetItems()
  218. {
  219. AvailableOptions.Clear();
  220. RebuildPanel();
  221. }
  222. /// <summary>
  223. /// Rebuilds the contents of the panel in response to items being added.
  224. /// </summary>
  225. private void RebuildPanel()
  226. {
  227. if (_isPanelActive) ToggleDropdownPanel();
  228. //panel starts with all options
  229. _panelItems.Clear();
  230. _prunedPanelItems.Clear();
  231. panelObjects.Clear();
  232. //clear Autocomplete children in scene
  233. foreach (Transform child in _itemsPanelRT.transform)
  234. {
  235. Destroy(child.gameObject);
  236. }
  237. foreach (string option in AvailableOptions)
  238. {
  239. _panelItems.Add(option.ToLower());
  240. }
  241. List<GameObject> itemObjs = new List<GameObject>(panelObjects.Values);
  242. int indx = 0;
  243. while (itemObjs.Count < AvailableOptions.Count)
  244. {
  245. GameObject newItem = Instantiate(itemTemplate) as GameObject;
  246. newItem.name = "Item " + indx;
  247. newItem.transform.SetParent(_itemsPanelRT, false);
  248. itemObjs.Add(newItem);
  249. indx++;
  250. }
  251. for (int i = 0; i < itemObjs.Count; i++)
  252. {
  253. itemObjs[i].SetActive(i <= AvailableOptions.Count);
  254. if (i < AvailableOptions.Count)
  255. {
  256. itemObjs[i].name = "Item " + i + " " + _panelItems[i];
  257. itemObjs[i].transform.Find("Text").GetComponent<Text>().text = AvailableOptions[i]; //set the text value
  258. Button itemBtn = itemObjs[i].GetComponent<Button>();
  259. itemBtn.onClick.RemoveAllListeners();
  260. string textOfItem = _panelItems[i]; //has to be copied for anonymous function or it gets garbage collected away
  261. itemBtn.onClick.AddListener(() =>
  262. {
  263. OnItemClicked(textOfItem);
  264. });
  265. panelObjects[_panelItems[i]] = itemObjs[i];
  266. }
  267. }
  268. SetInputTextColor ();
  269. }
  270. /// <summary>
  271. /// what happens when an item in the list is selected
  272. /// </summary>
  273. /// <param name="item"></param>
  274. private void OnItemClicked(string item)
  275. {
  276. //Debug.Log("item " + item + " clicked");
  277. Text = item;
  278. _mainInput.text = Text;
  279. ToggleDropdownPanel(true);
  280. }
  281. //private void UpdateSelected()
  282. //{
  283. // SelectedItem = (_selectedIndex > -1 && _selectedIndex < Items.Count) ? Items[_selectedIndex] : null;
  284. // if (SelectedItem == null) return;
  285. // bool hasImage = SelectedItem.Image != null;
  286. // if (hasImage)
  287. // {
  288. // mainButton.img.sprite = SelectedItem.Image;
  289. // mainButton.img.color = Color.white;
  290. // //if (Interactable) mainButton.img.color = Color.white;
  291. // //else mainButton.img.color = new Color(1, 1, 1, .5f);
  292. // }
  293. // else
  294. // {
  295. // mainButton.img.sprite = null;
  296. // }
  297. // mainButton.txt.text = SelectedItem.Caption;
  298. // //update selected index color
  299. // for (int i = 0; i < itemsPanelRT.childCount; i++)
  300. // {
  301. // panelItems[i].btnImg.color = (_selectedIndex == i) ? mainButton.btn.colors.highlightedColor : new Color(0, 0, 0, 0);
  302. // }
  303. //}
  304. private void RedrawPanel()
  305. {
  306. float scrollbarWidth = _panelItems.Count > ItemsToDisplay ? _scrollBarWidth : 0f;//hide the scrollbar if there's not enough items
  307. _scrollBarRT.gameObject.SetActive(_panelItems.Count > ItemsToDisplay);
  308. if (!_hasDrawnOnce || _rectTransform.sizeDelta != _inputRT.sizeDelta)
  309. {
  310. _hasDrawnOnce = true;
  311. _inputRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, _rectTransform.sizeDelta.x);
  312. _inputRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, _rectTransform.sizeDelta.y);
  313. _scrollPanelRT.SetParent(transform, true);//break the scroll panel from the overlay
  314. _scrollPanelRT.anchoredPosition = _displayPanelAbove ?
  315. new Vector2(0, DropdownOffset + _rectTransform.sizeDelta.y * _panelItems.Count - 1) :
  316. new Vector2(0, -_rectTransform.sizeDelta.y);
  317. //make the overlay fill the screen
  318. _overlayRT.SetParent(_canvas.transform, false); //attach it to top level object
  319. _overlayRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, _canvasRT.sizeDelta.x);
  320. _overlayRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, _canvasRT.sizeDelta.y);
  321. _overlayRT.SetParent(transform, true);//reattach to this object
  322. _scrollPanelRT.SetParent(_overlayRT, true); //reattach the scrollpanel to the overlay
  323. }
  324. if (_panelItems.Count < 1) return;
  325. float dropdownHeight = _rectTransform.sizeDelta.y * Mathf.Min(_itemsToDisplay, _panelItems.Count) + DropdownOffset;
  326. _scrollPanelRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, dropdownHeight);
  327. _scrollPanelRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, _rectTransform.sizeDelta.x);
  328. _itemsPanelRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, _scrollPanelRT.sizeDelta.x - scrollbarWidth - 5);
  329. _itemsPanelRT.anchoredPosition = new Vector2(5, 0);
  330. _scrollBarRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, scrollbarWidth);
  331. _scrollBarRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, dropdownHeight);
  332. if (scrollbarWidth == 0) _scrollHandleRT.gameObject.SetActive(false); else _scrollHandleRT.gameObject.SetActive(true);
  333. _slidingAreaRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 0);
  334. _slidingAreaRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, dropdownHeight - _scrollBarRT.sizeDelta.x);
  335. }
  336. public void OnValueChanged(string currText)
  337. {
  338. Text = currText;
  339. PruneItems(currText);
  340. RedrawPanel();
  341. //Debug.Log("value changed to: " + currText);
  342. if (_panelItems.Count == 0)
  343. {
  344. _isPanelActive = true;//this makes it get turned off
  345. ToggleDropdownPanel(false);
  346. }
  347. else if (!_isPanelActive)
  348. {
  349. ToggleDropdownPanel(false);
  350. }
  351. bool validity_changed = (_panelItems.Contains (Text) != _selectionIsValid);
  352. _selectionIsValid = _panelItems.Contains (Text);
  353. OnSelectionChanged.Invoke (Text, _selectionIsValid);
  354. OnSelectionTextChanged.Invoke (Text);
  355. if(validity_changed){
  356. OnSelectionValidityChanged.Invoke (_selectionIsValid);
  357. }
  358. SetInputTextColor ();
  359. }
  360. private void SetInputTextColor(){
  361. if (InputColorMatching) {
  362. if (_selectionIsValid) {
  363. _mainInput.textComponent.color = ValidSelectionTextColor;
  364. } else if (_panelItems.Count > 0) {
  365. _mainInput.textComponent.color = MatchingItemsRemainingTextColor;
  366. } else {
  367. _mainInput.textComponent.color = NoItemsRemainingTextColor;
  368. }
  369. }
  370. }
  371. /// <summary>
  372. /// Toggle the drop down list
  373. /// </summary>
  374. /// <param name="directClick"> whether an item was directly clicked on</param>
  375. public void ToggleDropdownPanel(bool directClick = false)
  376. {
  377. _isPanelActive = !_isPanelActive;
  378. _overlayRT.gameObject.SetActive(_isPanelActive);
  379. if (_isPanelActive)
  380. {
  381. transform.SetAsLastSibling();
  382. }
  383. else if (directClick)
  384. {
  385. // scrollOffset = Mathf.RoundToInt(itemsPanelRT.anchoredPosition.y / _rectTransform.sizeDelta.y);
  386. }
  387. }
  388. private void PruneItems(string currText)
  389. {
  390. if (autocompleteSearchType == AutoCompleteSearchType.Linq)
  391. {
  392. PruneItemsLinq(currText);
  393. }
  394. else
  395. {
  396. PruneItemsArray(currText);
  397. }
  398. }
  399. private void PruneItemsLinq(string currText)
  400. {
  401. currText = currText.ToLower();
  402. var toPrune = _panelItems.Where(x => !x.Contains(currText)).ToArray();
  403. foreach (string key in toPrune)
  404. {
  405. panelObjects[key].SetActive(false);
  406. _panelItems.Remove(key);
  407. _prunedPanelItems.Add(key);
  408. }
  409. var toAddBack = _prunedPanelItems.Where(x => x.Contains(currText)).ToArray();
  410. foreach (string key in toAddBack)
  411. {
  412. panelObjects[key].SetActive(true);
  413. _panelItems.Add(key);
  414. _prunedPanelItems.Remove(key);
  415. }
  416. }
  417. //Updated to not use Linq
  418. private void PruneItemsArray(string currText)
  419. {
  420. string _currText = currText.ToLower();
  421. for (int i = _panelItems.Count - 1; i >= 0; i--)
  422. {
  423. string _item = _panelItems[i];
  424. if (!_item.Contains(_currText))
  425. {
  426. panelObjects[_panelItems[i]].SetActive(false);
  427. _panelItems.RemoveAt(i);
  428. _prunedPanelItems.Add(_item);
  429. }
  430. }
  431. for (int i = _prunedPanelItems.Count - 1; i >= 0; i--)
  432. {
  433. string _item = _prunedPanelItems[i];
  434. if (_item.Contains(_currText))
  435. {
  436. panelObjects[_prunedPanelItems[i]].SetActive(true);
  437. _prunedPanelItems.RemoveAt(i);
  438. _panelItems.Add(_item);
  439. }
  440. }
  441. }
  442. }
  443. }