2015-02-11 04:56:12 +08:00
///Credit perchik
///Sourced from - http://forum.unity3d.com/threads/receive-onclick-event-and-pass-it-on-to-lower-ui-elements.293642/
using System.Collections.Generic ;
using System.Linq ;
namespace UnityEngine.UI.Extensions
{
2017-06-10 21:33:12 +08:00
public enum AutoCompleteSearchType
{
ArraySort ,
Linq
}
2015-02-11 04:56:12 +08:00
[RequireComponent(typeof(RectTransform))]
2023-01-02 00:52:08 +08:00
[AddComponentMenu("UI/Extensions/ComboBox/AutoComplete ComboBox")]
2015-02-11 04:56:12 +08:00
public class AutoCompleteComboBox : MonoBehaviour
{
public Color disabledTextColor ;
public DropDownListItem SelectedItem { get ; private set ; } //outside world gets to get this, not set it
2021-05-11 01:38:31 +08:00
/// <summary>
/// Contains the included items. To add and remove items to/from this list, use the <see cref="AddItem(string)"/>,
/// <see cref="RemoveItem(string)"/> and <see cref="SetAvailableOptions(List{string})"/> methods as these also execute
/// the required methods to update to the current collection.
/// </summary>
2015-02-11 04:56:12 +08:00
public List < string > AvailableOptions ;
private bool _isPanelActive = false ;
private bool _hasDrawnOnce = false ;
private InputField _mainInput ;
private RectTransform _inputRT ;
private RectTransform _rectTransform ;
private RectTransform _overlayRT ;
private RectTransform _scrollPanelRT ;
private RectTransform _scrollBarRT ;
private RectTransform _slidingAreaRT ;
2021-05-11 01:38:31 +08:00
private RectTransform _scrollHandleRT ;
2015-02-11 04:56:12 +08:00
private RectTransform _itemsPanelRT ;
private Canvas _canvas ;
private RectTransform _canvasRT ;
private ScrollRect _scrollRect ;
2020-07-09 03:38:28 +08:00
private List < string > _panelItems ; //items that will get shown in the drop-down
private List < string > _prunedPanelItems ; //items that used to show in the drop-down
2015-02-11 04:56:12 +08:00
private Dictionary < string , GameObject > panelObjects ;
2023-01-02 00:52:08 +08:00
2015-02-11 04:56:12 +08:00
private GameObject itemTemplate ;
public string Text { get ; private set ; }
2023-01-02 00:52:08 +08:00
[SerializeField]
private float dropdownOffset ;
2015-02-11 04:56:12 +08:00
[SerializeField]
private float _scrollBarWidth = 20.0f ;
public float ScrollBarWidth
{
get { return _scrollBarWidth ; }
set
{
_scrollBarWidth = value ;
RedrawPanel ( ) ;
}
}
[SerializeField]
private int _itemsToDisplay ;
public int ItemsToDisplay
{
get { return _itemsToDisplay ; }
set
{
_itemsToDisplay = value ;
RedrawPanel ( ) ;
}
}
2017-03-27 18:03:05 +08:00
2023-01-02 00:52:08 +08:00
public bool SelectFirstItemOnStart = false ;
2017-03-27 18:52:51 +08:00
2023-01-02 00:52:08 +08:00
[SerializeField]
2017-05-05 21:06:52 +08:00
[Tooltip("Change input text color based on matching items")]
private bool _ChangeInputTextColorBasedOnMatchingItems = false ;
2023-01-02 00:52:08 +08:00
public bool InputColorMatching
{
get { return _ChangeInputTextColorBasedOnMatchingItems ; }
set
{
_ChangeInputTextColorBasedOnMatchingItems = value ;
if ( _ChangeInputTextColorBasedOnMatchingItems )
{
SetInputTextColor ( ) ;
}
}
}
2017-03-27 20:03:39 +08:00
2020-09-25 04:47:00 +08:00
public float DropdownOffset = 10f ;
public Color ValidSelectionTextColor = Color . green ;
2023-01-02 00:52:08 +08:00
public Color MatchingItemsRemainingTextColor = Color . black ;
public Color NoItemsRemainingTextColor = Color . red ;
2017-03-27 20:03:39 +08:00
2017-06-10 21:33:12 +08:00
public AutoCompleteSearchType autocompleteSearchType = AutoCompleteSearchType . Linq ;
2021-05-11 01:38:31 +08:00
[SerializeField]
private bool _displayPanelAbove = false ;
2017-06-10 21:33:12 +08:00
private bool _selectionIsValid = false ;
2017-03-27 20:03:39 +08:00
2023-01-02 00:52:08 +08:00
[System.Serializable]
public class SelectionChangedEvent : Events . UnityEvent < string , bool > { }
2017-03-27 20:33:08 +08:00
2017-05-05 21:06:52 +08:00
[System.Serializable]
2023-01-02 00:52:08 +08:00
public class SelectionTextChangedEvent : Events . UnityEvent < string > { }
2017-03-27 20:33:08 +08:00
2023-01-02 00:52:08 +08:00
[System.Serializable]
public class SelectionValidityChangedEvent : Events . UnityEvent < bool > { }
2017-03-27 20:33:08 +08:00
2023-01-02 00:52:08 +08:00
// fires when input text is changed;
public SelectionTextChangedEvent OnSelectionTextChanged ;
// fires when an Item gets selected / deselected (including when items are added/removed once this is possible)
public SelectionValidityChangedEvent OnSelectionValidityChanged ;
// fires in both cases
public SelectionChangedEvent OnSelectionChanged ;
2017-03-27 20:33:08 +08:00
2015-02-11 04:56:12 +08:00
public void Awake ( )
{
Initialize ( ) ;
}
2021-05-11 01:38:31 +08:00
2023-01-02 00:52:08 +08:00
public void Start ( )
{
if ( SelectFirstItemOnStart & & AvailableOptions . Count > 0 )
{
ToggleDropdownPanel ( false ) ;
OnItemClicked ( AvailableOptions [ 0 ] ) ;
}
2021-05-11 01:38:31 +08:00
RedrawPanel ( ) ;
}
2015-02-11 04:56:12 +08:00
private bool Initialize ( )
{
bool success = true ;
try
{
_rectTransform = GetComponent < RectTransform > ( ) ;
2017-06-04 00:59:12 +08:00
_inputRT = _rectTransform . Find ( "InputField" ) . GetComponent < RectTransform > ( ) ;
2015-02-11 04:56:12 +08:00
_mainInput = _inputRT . GetComponent < InputField > ( ) ;
2017-06-04 00:59:12 +08:00
_overlayRT = _rectTransform . Find ( "Overlay" ) . GetComponent < RectTransform > ( ) ;
2015-02-11 04:56:12 +08:00
_overlayRT . gameObject . SetActive ( false ) ;
2017-06-04 00:59:12 +08:00
_scrollPanelRT = _overlayRT . Find ( "ScrollPanel" ) . GetComponent < RectTransform > ( ) ;
_scrollBarRT = _scrollPanelRT . Find ( "Scrollbar" ) . GetComponent < RectTransform > ( ) ;
_slidingAreaRT = _scrollBarRT . Find ( "SlidingArea" ) . GetComponent < RectTransform > ( ) ;
2021-05-11 01:38:31 +08:00
_scrollHandleRT = _slidingAreaRT . Find ( "Handle" ) . GetComponent < RectTransform > ( ) ;
2017-06-04 00:59:12 +08:00
_itemsPanelRT = _scrollPanelRT . Find ( "Items" ) . GetComponent < RectTransform > ( ) ;
2015-02-11 04:56:12 +08:00
_canvas = GetComponentInParent < Canvas > ( ) ;
_canvasRT = _canvas . GetComponent < RectTransform > ( ) ;
_scrollRect = _scrollPanelRT . GetComponent < ScrollRect > ( ) ;
_scrollRect . scrollSensitivity = _rectTransform . sizeDelta . y / 2 ;
_scrollRect . movementType = ScrollRect . MovementType . Clamped ;
_scrollRect . content = _itemsPanelRT ;
2017-06-04 00:59:12 +08:00
itemTemplate = _rectTransform . Find ( "ItemTemplate" ) . gameObject ;
2015-02-11 04:56:12 +08:00
itemTemplate . SetActive ( false ) ;
}
catch ( System . NullReferenceException ex )
{
Debug . LogException ( ex ) ;
2020-07-09 03:38:28 +08:00
Debug . LogError ( "Something is setup incorrectly with the dropdownlist component causing a Null Reference Exception" ) ;
2015-02-11 04:56:12 +08:00
success = false ;
}
panelObjects = new Dictionary < string , GameObject > ( ) ;
_prunedPanelItems = new List < string > ( ) ;
2017-06-10 21:33:12 +08:00
_panelItems = new List < string > ( ) ;
2015-02-11 04:56:12 +08:00
RebuildPanel ( ) ;
return success ;
}
2021-05-11 01:38:31 +08:00
/// <summary>
/// Adds the item to <see cref="this.AvailableOptions"/> if it is not a duplicate and rebuilds the panel.
/// </summary>
/// <param name="item">Item to add.</param>
2020-08-28 01:20:49 +08:00
public void AddItem ( string item )
{
2021-05-11 01:38:31 +08:00
if ( ! this . AvailableOptions . Contains ( item ) )
{
this . AvailableOptions . Add ( item ) ;
this . RebuildPanel ( ) ;
}
else
{
Debug . LogWarning ( $"{nameof(AutoCompleteComboBox)}.{nameof(AddItem)}: items may only exists once. '{item}' can not be added." ) ;
}
2020-08-28 01:20:49 +08:00
}
2021-05-11 01:38:31 +08:00
/// <summary>
/// Removes the item from <see cref="this.AvailableOptions"/> and rebuilds the panel.
/// </summary>
/// <param name="item">Item to remove.</param>
2020-08-28 01:20:49 +08:00
public void RemoveItem ( string item )
{
2021-05-11 01:38:31 +08:00
if ( this . AvailableOptions . Contains ( item ) )
{
this . AvailableOptions . Remove ( item ) ;
this . RebuildPanel ( ) ;
}
2020-08-28 01:20:49 +08:00
}
2021-05-11 01:38:31 +08:00
/// <summary>
/// Sets the given items as new content for the comboBox. Previous entries will be cleared.
/// </summary>
/// <param name="newOptions">New entries.</param>
2020-08-28 01:20:49 +08:00
public void SetAvailableOptions ( List < string > newOptions )
{
2023-01-02 00:52:08 +08:00
var uniqueOptions = newOptions . Distinct ( ) . ToArray ( ) ;
SetAvailableOptions ( uniqueOptions ) ;
2020-08-28 01:20:49 +08:00
}
2021-05-11 01:38:31 +08:00
/// <summary>
/// Sets the given items as new content for the comboBox. Previous entries will be cleared.
/// </summary>
/// <param name="newOptions">New entries.</param>
2020-08-28 01:20:49 +08:00
public void SetAvailableOptions ( string [ ] newOptions )
{
2021-05-11 01:38:31 +08:00
var uniqueOptions = newOptions . Distinct ( ) . ToList ( ) ;
if ( newOptions . Length ! = uniqueOptions . Count )
{
Debug . LogWarning ( $"{nameof(AutoCompleteComboBox)}.{nameof(SetAvailableOptions)}: items may only exists once. {newOptions.Length - uniqueOptions.Count} duplicates." ) ;
}
2020-08-28 01:20:49 +08:00
2021-05-11 01:38:31 +08:00
this . AvailableOptions . Clear ( ) ;
2023-01-02 00:52:08 +08:00
2020-08-28 01:20:49 +08:00
for ( int i = 0 ; i < newOptions . Length ; i + + )
{
2021-05-11 01:38:31 +08:00
this . AvailableOptions . Add ( newOptions [ i ] ) ;
2020-08-28 01:20:49 +08:00
}
2021-05-11 01:38:31 +08:00
this . RebuildPanel ( ) ;
2023-01-02 00:52:08 +08:00
this . RedrawPanel ( ) ;
2020-08-28 01:20:49 +08:00
}
public void ResetItems ( )
{
AvailableOptions . Clear ( ) ;
RebuildPanel ( ) ;
2023-01-02 00:52:08 +08:00
RedrawPanel ( ) ;
2020-08-28 01:20:49 +08:00
}
2015-02-11 04:56:12 +08:00
/// <summary>
/// Rebuilds the contents of the panel in response to items being added.
/// </summary>
private void RebuildPanel ( )
{
2020-09-25 04:47:00 +08:00
if ( _isPanelActive ) ToggleDropdownPanel ( ) ;
2015-02-11 04:56:12 +08:00
//panel starts with all options
_panelItems . Clear ( ) ;
2017-06-10 21:33:12 +08:00
_prunedPanelItems . Clear ( ) ;
panelObjects . Clear ( ) ;
2020-09-25 04:47:00 +08:00
//clear Autocomplete children in scene
foreach ( Transform child in _itemsPanelRT . transform )
{
Destroy ( child . gameObject ) ;
}
2015-02-11 04:56:12 +08:00
foreach ( string option in AvailableOptions )
{
_panelItems . Add ( option . ToLower ( ) ) ;
}
List < GameObject > itemObjs = new List < GameObject > ( panelObjects . Values ) ;
int indx = 0 ;
while ( itemObjs . Count < AvailableOptions . Count )
{
GameObject newItem = Instantiate ( itemTemplate ) as GameObject ;
newItem . name = "Item " + indx ;
newItem . transform . SetParent ( _itemsPanelRT , false ) ;
itemObjs . Add ( newItem ) ;
indx + + ;
}
for ( int i = 0 ; i < itemObjs . Count ; i + + )
{
itemObjs [ i ] . SetActive ( i < = AvailableOptions . Count ) ;
if ( i < AvailableOptions . Count )
{
itemObjs [ i ] . name = "Item " + i + " " + _panelItems [ i ] ;
2021-05-11 01:38:31 +08:00
itemObjs [ i ] . transform . Find ( "Text" ) . GetComponent < Text > ( ) . text = AvailableOptions [ i ] ; //set the text value
2015-02-11 04:56:12 +08:00
Button itemBtn = itemObjs [ i ] . GetComponent < Button > ( ) ;
itemBtn . onClick . RemoveAllListeners ( ) ;
string textOfItem = _panelItems [ i ] ; //has to be copied for anonymous function or it gets garbage collected away
itemBtn . onClick . AddListener ( ( ) = >
{
OnItemClicked ( textOfItem ) ;
} ) ;
panelObjects [ _panelItems [ i ] ] = itemObjs [ i ] ;
}
}
2023-01-02 00:52:08 +08:00
SetInputTextColor ( ) ;
2015-02-11 04:56:12 +08:00
}
/// <summary>
/// what happens when an item in the list is selected
/// </summary>
/// <param name="item"></param>
private void OnItemClicked ( string item )
{
//Debug.Log("item " + item + " clicked");
Text = item ;
_mainInput . text = Text ;
ToggleDropdownPanel ( true ) ;
}
private void RedrawPanel ( )
{
float scrollbarWidth = _panelItems . Count > ItemsToDisplay ? _scrollBarWidth : 0f ; //hide the scrollbar if there's not enough items
_scrollBarRT . gameObject . SetActive ( _panelItems . Count > ItemsToDisplay ) ;
if ( ! _hasDrawnOnce | | _rectTransform . sizeDelta ! = _inputRT . sizeDelta )
{
_hasDrawnOnce = true ;
_inputRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Horizontal , _rectTransform . sizeDelta . x ) ;
_inputRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Vertical , _rectTransform . sizeDelta . y ) ;
2023-01-02 00:52:08 +08:00
var itemsRemaining = _panelItems . Count - ItemsToDisplay ;
itemsRemaining = itemsRemaining < 0 ? 0 : itemsRemaining ;
_scrollPanelRT . SetParent ( transform , true ) ;
2021-05-11 01:38:31 +08:00
_scrollPanelRT . anchoredPosition = _displayPanelAbove ?
2023-01-02 00:52:08 +08:00
new Vector2 ( 0 , dropdownOffset + _rectTransform . sizeDelta . y * ( _panelItems . Count - itemsRemaining ) - 1 ) :
2021-05-11 01:38:31 +08:00
new Vector2 ( 0 , - _rectTransform . sizeDelta . y ) ;
2015-02-11 04:56:12 +08:00
//make the overlay fill the screen
2023-01-02 00:52:08 +08:00
_overlayRT . SetParent ( _canvas . transform , false ) ;
2015-02-11 04:56:12 +08:00
_overlayRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Horizontal , _canvasRT . sizeDelta . x ) ;
_overlayRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Vertical , _canvasRT . sizeDelta . y ) ;
2023-01-02 00:52:08 +08:00
_overlayRT . SetParent ( transform , true ) ;
_scrollPanelRT . SetParent ( _overlayRT , true ) ;
2015-02-11 04:56:12 +08:00
}
if ( _panelItems . Count < 1 ) return ;
2023-01-02 00:52:08 +08:00
float dropdownHeight = _rectTransform . sizeDelta . y * Mathf . Min ( _itemsToDisplay , _panelItems . Count ) + dropdownOffset ;
2015-02-11 04:56:12 +08:00
_scrollPanelRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Vertical , dropdownHeight ) ;
_scrollPanelRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Horizontal , _rectTransform . sizeDelta . x ) ;
_itemsPanelRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Horizontal , _scrollPanelRT . sizeDelta . x - scrollbarWidth - 5 ) ;
_itemsPanelRT . anchoredPosition = new Vector2 ( 5 , 0 ) ;
_scrollBarRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Horizontal , scrollbarWidth ) ;
_scrollBarRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Vertical , dropdownHeight ) ;
2023-01-02 00:52:08 +08:00
if ( scrollbarWidth = = 0 ) _scrollHandleRT . gameObject . SetActive ( false ) ; else _scrollHandleRT . gameObject . SetActive ( true ) ;
2015-02-11 04:56:12 +08:00
_slidingAreaRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Horizontal , 0 ) ;
_slidingAreaRT . SetSizeWithCurrentAnchors ( RectTransform . Axis . Vertical , dropdownHeight - _scrollBarRT . sizeDelta . x ) ;
}
public void OnValueChanged ( string currText )
{
Text = currText ;
PruneItems ( currText ) ;
RedrawPanel ( ) ;
if ( _panelItems . Count = = 0 )
{
_isPanelActive = true ; //this makes it get turned off
ToggleDropdownPanel ( false ) ;
}
else if ( ! _isPanelActive )
{
ToggleDropdownPanel ( false ) ;
}
2017-03-27 20:33:08 +08:00
2023-01-02 00:52:08 +08:00
bool validity_changed = ( _panelItems . Contains ( Text ) ! = _selectionIsValid ) ;
_selectionIsValid = _panelItems . Contains ( Text ) ;
OnSelectionChanged . Invoke ( Text , _selectionIsValid ) ;
OnSelectionTextChanged . Invoke ( Text ) ;
if ( validity_changed )
{
OnSelectionValidityChanged . Invoke ( _selectionIsValid ) ;
}
2017-03-27 20:33:08 +08:00
2023-01-02 00:52:08 +08:00
SetInputTextColor ( ) ;
2015-02-11 04:56:12 +08:00
}
2023-01-02 00:52:08 +08:00
private void SetInputTextColor ( )
{
if ( InputColorMatching )
{
if ( _selectionIsValid )
{
_mainInput . textComponent . color = ValidSelectionTextColor ;
}
else if ( _panelItems . Count > 0 )
{
_mainInput . textComponent . color = MatchingItemsRemainingTextColor ;
}
else
{
_mainInput . textComponent . color = NoItemsRemainingTextColor ;
}
}
}
2017-03-27 20:03:39 +08:00
2015-02-11 04:56:12 +08:00
/// <summary>
/// Toggle the drop down list
/// </summary>
/// <param name="directClick"> whether an item was directly clicked on</param>
2020-09-25 04:47:00 +08:00
public void ToggleDropdownPanel ( bool directClick = false )
2015-02-11 04:56:12 +08:00
{
_isPanelActive = ! _isPanelActive ;
_overlayRT . gameObject . SetActive ( _isPanelActive ) ;
if ( _isPanelActive )
{
transform . SetAsLastSibling ( ) ;
}
else if ( directClick )
{
// scrollOffset = Mathf.RoundToInt(itemsPanelRT.anchoredPosition.y / _rectTransform.sizeDelta.y);
}
}
private void PruneItems ( string currText )
{
2017-06-10 21:33:12 +08:00
if ( autocompleteSearchType = = AutoCompleteSearchType . Linq )
{
PruneItemsLinq ( currText ) ;
}
else
{
PruneItemsArray ( currText ) ;
}
}
private void PruneItemsLinq ( string currText )
{
currText = currText . ToLower ( ) ;
var toPrune = _panelItems . Where ( x = > ! x . Contains ( currText ) ) . ToArray ( ) ;
2015-02-11 04:56:12 +08:00
foreach ( string key in toPrune )
{
panelObjects [ key ] . SetActive ( false ) ;
_panelItems . Remove ( key ) ;
_prunedPanelItems . Add ( key ) ;
}
2017-06-10 21:33:12 +08:00
var toAddBack = _prunedPanelItems . Where ( x = > x . Contains ( currText ) ) . ToArray ( ) ;
2015-02-11 04:56:12 +08:00
foreach ( string key in toAddBack )
{
panelObjects [ key ] . SetActive ( true ) ;
_panelItems . Add ( key ) ;
_prunedPanelItems . Remove ( key ) ;
}
}
2017-06-10 21:33:12 +08:00
//Updated to not use Linq
private void PruneItemsArray ( string currText )
{
string _currText = currText . ToLower ( ) ;
for ( int i = _panelItems . Count - 1 ; i > = 0 ; i - - )
{
string _item = _panelItems [ i ] ;
if ( ! _item . Contains ( _currText ) )
{
panelObjects [ _panelItems [ i ] ] . SetActive ( false ) ;
_panelItems . RemoveAt ( i ) ;
_prunedPanelItems . Add ( _item ) ;
}
}
for ( int i = _prunedPanelItems . Count - 1 ; i > = 0 ; i - - )
{
string _item = _prunedPanelItems [ i ] ;
if ( _item . Contains ( _currText ) )
{
panelObjects [ _prunedPanelItems [ i ] ] . SetActive ( true ) ;
_prunedPanelItems . RemoveAt ( i ) ;
_panelItems . Add ( _item ) ;
}
}
}
2015-02-11 04:56:12 +08:00
}
2023-01-02 00:52:08 +08:00
}