namespace UIWidgets { using System; using System.Collections; using System.Collections.Generic; using UIWidgets.l10n; using UIWidgets.Styles; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.Serialization; using UnityEngine.UI; /// /// AutocompleteFilter. /// public enum AutocompleteFilter { /// /// Value should beginning with Input. /// Startswith = 0, /// /// Input occurs with value. /// Contains = 1, } /// /// AutocompleteInput. /// public enum AutocompleteInput { /// /// Use current word in input. /// Word = 0, /// /// Use entire input. /// AllInput = 1, } /// /// AutocompleteResult. /// Append - add to end of input. /// Result - replace input. /// public enum AutocompleteResult { /// /// Append value to input /// Append = 0, /// /// Replace input. /// Replace = 1, } /// /// Autocomplete. /// Allow quickly find and select from a list of values as user type. /// DisplayListView - used to display list of values. /// TargetListView - if specified selected value will be added to this list. /// DataSource - list of values. /// /// Type of value. /// Type of ListView.DefaultItem. /// Type of ListView. public abstract class AutocompleteCustom : MonoBehaviour, IStylable, IUpgradeable, IUpdatable, ILateUpdatable where TListView : ListViewCustom where TListViewComponent : ListViewItem { /// /// Select event. /// [Serializable] public class SelectEvent : UnityEvent { } /// /// Select event. /// [Serializable] public class NotFoundEvent : UnityEvent { } /// /// InputField for autocomplete. /// [SerializeField] [FormerlySerializedAs("InputField")] [HideInInspector] [Obsolete("Replaced with InputFieldAdapter")] protected InputField inputField; /// /// InputField for autocomplete. /// [Obsolete("Replaced with InputFieldAdapter")] public InputField InputField { get { return inputField; } set { inputField = value; inputFieldAdapter = null; Utilities.GetOrAddComponent(inputField, ref inputFieldAdapter); InitInputField(); } } /// /// InputField for autocomplete. /// [SerializeField] protected InputFieldAdapter inputFieldAdapter; /// /// InputField for autocomplete. /// public InputFieldAdapter InputFieldAdapter { get { return inputFieldAdapter; } set { inputFieldAdapter = value; InitInputField(); } } /// /// Proxy for InputField. /// Required to improve compatibility between different InputFields (like Unity.UI and TextMeshPro versions). /// [Obsolete("Replaced with InputFieldAdapter")] protected IInputFieldProxy inputFieldProxy { get { return inputFieldAdapter; } } /// /// Gets the InputFieldProxy. /// [Obsolete("Replaced with InputFieldAdapter")] public virtual IInputFieldProxy InputFieldProxy { get { return inputFieldAdapter; } } /// /// ListView to display available values. /// [SerializeField] public TListView TargetListView; /// /// Selected value will be added to this ListView. /// [SerializeField] public TListView DisplayListView; /// /// The allow duplicate of TargetListView items. /// [SerializeField] public bool AllowDuplicate = false; /// /// List of the values. /// [SerializeField] public List DataSource = new List(); /// /// The filter. /// [SerializeField] protected AutocompleteFilter filter = AutocompleteFilter.Contains; /// /// Gets or sets the filter. /// /// The filter. public AutocompleteFilter Filter { get { return filter; } set { filter = value; CustomFilter = null; } } /// /// Is filter case sensitive? /// [SerializeField] public bool CaseSensitive; /// /// The delimiter chars to find word for autocomplete if InputType == Word. /// [SerializeField] public char[] DelimiterChars = new char[] { ' ', '\n' }; /// /// Custom filter. /// public Func> CustomFilter; /// /// Use entire input or current word in input. /// [SerializeField] protected AutocompleteInput InputType = AutocompleteInput.AllInput; /// /// Append value to input or replace input. /// [SerializeField] protected AutocompleteResult Result = AutocompleteResult.Replace; /// /// Current query - word in input or whole input for autocomplete. /// [HideInInspector] protected string Query = string.Empty; /// /// Current word in input or whole input for autocomplete. /// [HideInInspector] [Obsolete("Use Query instead.")] protected string Input { get { return Query; } set { Query = value; } } /// /// The previous query string. /// protected string PrevQuery; /// /// The previous query string. /// protected string PrevInput { get { return PrevQuery; } set { PrevQuery = value; } } /// /// InputField.caretPosition. Used to keep caretPosition with Up and Down actions. /// protected int CaretPosition; /// /// The minimum number of characters a user must type before a search is performed. /// [SerializeField] public int MinLength = 0; /// /// The delay in seconds between when a keystroke occurs and when a search is performed. /// [SerializeField] public float SearchDelay = 0f; /// /// Use unscaled time. /// [SerializeField] public bool UnscaledTime = true; /// /// Reset DisplayListView selected items. /// [SerializeField] public bool ResetListViewSelection = true; /// /// Canvas will be used as parent for DisplayListView. /// [SerializeField] public RectTransform ParentCanvas; /// /// To keep DisplayListView position if InputField inside scrollable area. /// protected Vector2 DisplayListViewAnchoredPosition; /// /// Default parent for DisplayListView. /// protected RectTransform DisplayListViewParent; /// /// Coroutine to performs search. /// protected IEnumerator SearchCoroutine; /// /// The modal key. /// protected int? ModalKey; /// /// Option selected event. /// public UnityEvent OnOptionSelected = new UnityEvent(); /// /// Item selected event. /// public SelectEvent OnOptionSelectedItem = new SelectEvent(); /// /// Item not found event. /// public NotFoundEvent OnItemNotFound = new NotFoundEvent(); /// /// Cancel event. /// public UnityEvent OnCancelInput = new UnityEvent(); /// /// Search completed event. /// public UnityEvent OnSearchCompleted = new UnityEvent(); /// /// Allow cancel on deselect event. /// public Func, bool> AllowCancelOnDeselect; /// /// Search results. /// protected ObservableList SearchResults = new ObservableList(); /// /// Determines whether the beginning of value matches the Input. /// /// Value. /// true if beginning of value matches the Input; otherwise, false. public abstract bool Startswith(TValue value); /// /// Returns a value indicating whether Input occurs within specified value. /// /// Value. /// true if the Input occurs within value parameter; otherwise, false. public abstract bool Contains(TValue value); /// /// Convert value to string. /// /// The string value. /// Value. protected abstract string GetStringValue(TValue value); /// /// Start this instance. /// protected virtual void Start() { Init(); } bool isInited; /// /// Init this instance. /// public virtual void Init() { if (isInited) { return; } isInited = true; InitInputField(); DisplayListView.gameObject.SetActive(false); DisplayListView.MultipleSelect = false; DisplayListView.OnSelect.AddListener(ItemSelected); Localization.OnLocaleChanged += LocaleChanged; } /// /// Process locale changes. /// public virtual void LocaleChanged() { if (DisplayListView.SelectedIndex >= 0) { var item = DisplayListView.DataSource[DisplayListView.SelectedIndex]; if (Result == AutocompleteResult.Replace) { InputFieldAdapter.text = GetStringValue(item); } } } /// /// Init input field. /// protected virtual void InitInputField() { if (!isInited) { return; } InputFieldAdapter.onValueChanged.AddListener(ApplyFilter); InputFieldAdapter.onEndEdit.AddListener(CheckCancel); var inputListener = Utilities.GetOrAddComponent(InputFieldAdapter.gameObject); inputListener.OnMoveEvent.AddListener(SelectResult); inputListener.OnSubmitEvent.AddListener(SubmitResult); inputListener.onDeselect.AddListener(InputDeselected); } /// /// Show all options. /// public void ShowAllOptions() { InputFieldAdapter.Focus(); ApplyFilter(string.Empty, false); } /// /// Gets the input field text. /// /// The input field text. public virtual string GetInputFieldText() { return InputFieldAdapter.text; } /// /// Allow to handle item selection event. /// protected bool AllowItemSelectionEvent; /// /// Handle input deselected event. /// /// Event data. protected virtual void InputDeselected(BaseEventData eventData) { var ev = eventData as PointerEventData; AllowItemSelectionEvent = (ev != null) && (ev.pointerCurrentRaycast.gameObject != null) && ev.pointerCurrentRaycast.gameObject.transform.IsChildOf(DisplayListView.transform); if (!AllowItemSelectionEvent && (AllowCancelOnDeselect != null)) { AllowItemSelectionEvent = AllowCancelOnDeselect(eventData, this); } if (!AllowItemSelectionEvent) { Cancel(); } } /// /// Handle item selected event. /// /// Index. /// Component. protected virtual void ItemSelected(int index, ListViewItem component) { if (AllowItemSelectionEvent) { AllowItemSelectionEvent = false; SubmitResult(null); } } /// /// Closes the options. /// /// Input. protected virtual void HideOptions(string input) { HideOptions(); } /// /// Closes the options. /// protected virtual void HideOptions() { if (ModalKey.HasValue) { ModalHelper.Close(ModalKey.Value); ModalKey = null; } if (ParentCanvas != null) { Utilities.GetOrAddComponent(DisplayListView).Restore(); } DisplayListView.gameObject.SetActive(false); } /// /// Check if input was canceled. /// /// Input. protected virtual void CheckCancel(string input) { if (inputFieldAdapter.wasCanceled) { Cancel(); } } /// /// Cancel. /// protected virtual void Cancel() { HideOptions(); OnCancelInput.Invoke(); } /// /// Shows the options. /// protected virtual void ShowOptions() { if (ParentCanvas == null) { ParentCanvas = UtilitiesUI.FindTopmostCanvas(DisplayListView.transform); } if (!ModalKey.HasValue) { ModalKey = ModalHelper.Open(this, null, new Color(0, 0, 0, 0f), Cancel, ParentCanvas); } if (ParentCanvas != null) { Utilities.GetOrAddComponent(DisplayListView).SetParent(ParentCanvas); } DisplayListView.gameObject.transform.SetAsLastSibling(); DisplayListView.gameObject.SetActive(true); } /// /// Gets the results. /// /// Values matches filter. protected virtual ObservableList GetResults() { if (CustomFilter != null) { return CustomFilter(Query); } else { SearchResults.Clear(); if (Filter == AutocompleteFilter.Startswith) { UtilitiesCollections.FindAll(DataSource, Startswith, SearchResults); } else { UtilitiesCollections.FindAll(DataSource, Contains, SearchResults); } return SearchResults; } } /// /// Sets the input. /// /// Input string. /// Query string. protected virtual string Input2Query(string input) { if (InputType == AutocompleteInput.AllInput) { return input; } int end_position = InputFieldAdapter.caretPosition; if (input.Length >= end_position) { var text = input.Substring(0, end_position); var start_position = text.LastIndexOfAny(DelimiterChars) + 1; return text.Substring(start_position).Trim(); } return input; } /// /// Applies the filter. /// /// Input. protected virtual void ApplyFilter(string input) { if (InputFieldAdapter.wasCanceled) { return; } ApplyFilter(input, true); } /// /// Applies the filter. /// /// Input. /// Check if InputField has focus? protected virtual void ApplyFilter(string input, bool skipIfSame) { if (SearchCoroutine != null) { StopCoroutine(SearchCoroutine); } if (EventSystem.current.currentSelectedGameObject != InputFieldAdapter.gameObject) { return; } Query = Input2Query(input); if (skipIfSame && (Query == PrevQuery)) { return; } PrevQuery = Query; if (Query.Length < MinLength) { DisplayListView.DataSource.Clear(); HideOptions(); return; } DisplayListView.Init(); DisplayListView.MultipleSelect = false; SearchCoroutine = Search(); StartCoroutine(SearchCoroutine); } /// /// Performs search with delay. /// /// Yield instruction. [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "HAA0401:Possible allocation of reference type enumerator", Justification = "Enumerator is reusable.")] protected virtual IEnumerator Search() { if (SearchDelay > 0) { yield return UtilitiesTime.Wait(SearchDelay, UnscaledTime); } DisplayListView.DataSource.BeginUpdate(); DisplayListView.DataSource.Clear(); DisplayListView.DataSource.AddRange(GetResults()); DisplayListView.DataSource.EndUpdate(); OnSearchCompleted.Invoke(); if (DisplayListView.DataSource.Count > 0) { ShowOptions(); if (ResetListViewSelection) { DisplayListView.SelectedIndex = -1; } } else { HideOptions(); } } /// /// Process the enable event. /// protected virtual void OnEnable() { Updater.Add(this); Updater.AddLateUpdate(this); } /// /// Process the disable event. /// protected virtual void OnDisable() { Updater.Remove(this); Updater.RemoveLateUpdate(this); } /// /// Update this instance. /// public virtual void RunUpdate() { if (!AllowItemSelectionEvent) { CaretPosition = InputFieldAdapter.caretPosition; } } /// /// Caret position after Enter pressed. /// protected int FixCaretPosition = -1; /// /// LateUpdate. /// public virtual void RunLateUpdate() { if (FixCaretPosition != -1) { #if UNITY_5_1 || UNITY_5_2 || UNITY_5_3 || UNITY_5_3_OR_NEWER InputFieldAdapter.caretPosition = FixCaretPosition; #else InputFieldAdapter.MoveToEnd(); #endif FixCaretPosition = -1; } } /// /// Selects the result. /// /// Event data. protected virtual void SelectResult(AxisEventData eventData) { if (!DisplayListView.gameObject.activeInHierarchy) { return; } if (DisplayListView.DataSource.Count == 0) { return; } switch (eventData.moveDir) { case MoveDirection.Up: if (DisplayListView.SelectedIndex == 0) { DisplayListView.SelectedIndex = DisplayListView.DataSource.Count - 1; } else { DisplayListView.SelectedIndex -= 1; } DisplayListView.ScrollTo(DisplayListView.SelectedIndex); InputFieldAdapter.caretPosition = CaretPosition; break; case MoveDirection.Down: if (DisplayListView.SelectedIndex == (DisplayListView.DataSource.Count - 1)) { DisplayListView.SelectedIndex = 0; } else { DisplayListView.SelectedIndex += 1; } DisplayListView.ScrollTo(DisplayListView.SelectedIndex); InputFieldAdapter.caretPosition = CaretPosition; break; default: if (Input2Query(InputFieldAdapter.text) != Query) { ApplyFilter(InputFieldAdapter.text); } break; } } /// /// Submits the result. /// /// Event data. protected virtual void SubmitResult(BaseEventData eventData) { SubmitResult(eventData, false); } /// /// Submits the result. /// /// Event data. /// Is Enter pressed? protected virtual void SubmitResult(BaseEventData eventData, bool isEnter) { if (DisplayListView.SelectedIndex == -1) { OnItemNotFound.Invoke(InputFieldAdapter.text); HideOptions(); return; } if (InputFieldAdapter.IsMultiLineNewline()) { if (!DisplayListView.gameObject.activeInHierarchy) { return; } else { isEnter = false; } } var item = DisplayListView.DataSource[DisplayListView.SelectedIndex]; if (TargetListView != null) { TargetListView.Init(); TargetListView.Set(item, AllowDuplicate); } var value = GetStringValue(item); if (Result == AutocompleteResult.Append) { int end_position = (DisplayListView.gameObject.activeInHierarchy && eventData != null && !isEnter) ? InputFieldAdapter.caretPosition : CaretPosition; var text = InputFieldAdapter.text.Substring(0, end_position); var start_position = text.LastIndexOfAny(DelimiterChars) + 1; InputFieldAdapter.text = string.Format("{0}{1}{2}", InputFieldAdapter.text.Substring(0, start_position), value, InputFieldAdapter.text.Substring(end_position)); InputFieldAdapter.caretPosition = start_position + value.Length; #if UNITY_5_1 || UNITY_5_2 || UNITY_5_3 || UNITY_5_3_OR_NEWER // InputField.selectionFocusPosition = start_position + value.Length; #else InputFieldAdapter.MoveToEnd(); #endif if (isEnter) { FixCaretPosition = start_position + value.Length; InputFieldAdapter.ActivateInputField(); } } else { InputFieldAdapter.text = value; #if UNITY_5_1 || UNITY_5_2 || UNITY_5_3 || UNITY_5_3_OR_NEWER InputFieldAdapter.caretPosition = value.Length; #else InputFieldAdapter.ActivateInputField(); #endif FixCaretPosition = value.Length; } OnOptionSelected.Invoke(); OnOptionSelectedItem.Invoke(item); HideOptions(); } /// /// Upgrade this instance. /// public virtual void Upgrade() { #pragma warning disable 0618 Utilities.GetOrAddComponent(InputField, ref inputFieldAdapter); #pragma warning restore 0618 } #if UNITY_EDITOR /// /// Validate this instance. /// protected virtual void OnValidate() { Compatibility.Upgrade(this); if (ParentCanvas == null) { var t = (DisplayListView != null) ? DisplayListView.transform : transform; ParentCanvas = UtilitiesUI.FindTopmostCanvas(t); } } /// /// Reset this instance. /// protected virtual void Reset() { if (ParentCanvas == null) { ParentCanvas = UtilitiesUI.FindTopmostCanvas(transform); } } #endif /// /// This function is called when the MonoBehaviour will be destroyed. /// protected virtual void OnDestroy() { Localization.OnLocaleChanged -= LocaleChanged; if (DisplayListView != null) { DisplayListView.OnSelect.RemoveListener(ItemSelected); } if (InputFieldAdapter != null) { InputFieldAdapter.onValueChanged.RemoveListener(ApplyFilter); InputFieldAdapter.onEndEdit.RemoveListener(CheckCancel); var inputListener = InputFieldAdapter.gameObject.GetComponent(); if (inputListener != null) { inputListener.OnMoveEvent.RemoveListener(SelectResult); inputListener.OnSubmitEvent.RemoveListener(SubmitResult); inputListener.onDeselect.RemoveListener(InputDeselected); } } } #region IStylable implementation /// /// Set InputField style. /// /// Style data. protected virtual void SetStyleInput(Style style) { if (InputFieldAdapter == null) { return; } if (InputFieldAdapter.textComponent != null) { style.Autocomplete.InputField.ApplyTo(InputFieldAdapter.textComponent.gameObject, true); } if (InputFieldAdapter.placeholder != null) { style.Autocomplete.Placeholder.ApplyTo(InputFieldAdapter.placeholder.gameObject); } } /// public virtual bool SetStyle(Style style) { style.Autocomplete.Background.ApplyTo(GetComponent()); SetStyleInput(style); if (DisplayListView != null) { DisplayListView.SetStyle(style); } if (TargetListView != null) { TargetListView.SetStyle(style); } return true; } /// /// Set style options from InputField. /// /// Style data. protected virtual void GetStyleInput(Style style) { if (InputFieldAdapter == null) { return; } if (InputFieldAdapter.textComponent != null) { style.Autocomplete.InputField.GetFrom(InputFieldAdapter.textComponent.gameObject, true); } if (InputFieldAdapter.placeholder != null) { style.Autocomplete.Placeholder.GetFrom(InputFieldAdapter.placeholder.gameObject); } } /// public virtual bool GetStyle(Style style) { style.Autocomplete.Background.GetFrom(GetComponent()); GetStyleInput(style); if (DisplayListView != null) { DisplayListView.GetStyle(style); } if (TargetListView != null) { TargetListView.GetStyle(style); } return true; } #endregion } }