You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
440 lines
11 KiB
440 lines
11 KiB
2 years ago
|
using UnityEngine;
|
||
|
using UnityEngine.UI;
|
||
|
using UnityEngine.Events;
|
||
|
using UnityEngine.EventSystems;
|
||
|
using System.Collections;
|
||
|
using System.Collections.Generic;
|
||
|
using System;
|
||
|
using System.Linq;
|
||
|
|
||
|
namespace UIWidgets {
|
||
|
/// <summary>
|
||
|
/// AutocompleteFilter.
|
||
|
/// Startswith - value should beginnig with Input.
|
||
|
/// Contains - Input occurs with value.
|
||
|
/// </summary>
|
||
|
public enum AutocompleteFilter {
|
||
|
Startswith = 0,
|
||
|
Contains = 1,
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// AutocompleteInput.
|
||
|
/// Word - Use current word in input
|
||
|
/// AllInput - use entire input.
|
||
|
/// </summary>
|
||
|
public enum AutocompleteInput {
|
||
|
Word = 0,
|
||
|
AllInput = 1,
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// AutocompleteResult.
|
||
|
/// Append - append value to input.
|
||
|
/// Result - replace input.
|
||
|
/// </summary>
|
||
|
public enum AutocompleteResult {
|
||
|
Append = 0,
|
||
|
Replace = 1,
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// 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.
|
||
|
/// </summary>
|
||
|
public abstract class AutocompleteCustom<TValue,TListViewComponent,TListView> : MonoBehaviour
|
||
|
where TListView : ListViewCustom<TListViewComponent,TValue>
|
||
|
where TListViewComponent : ListViewItem
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// InputField for autocomplete.
|
||
|
/// </summary>
|
||
|
[SerializeField]
|
||
|
protected InputField InputField;
|
||
|
|
||
|
IInputFieldProxy inputFieldProxy;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the InputFieldProxy.
|
||
|
/// </summary>
|
||
|
protected virtual IInputFieldProxy InputFieldProxy {
|
||
|
get {
|
||
|
if (inputFieldProxy==null)
|
||
|
{
|
||
|
inputFieldProxy = new InputFieldProxy(InputField);
|
||
|
}
|
||
|
return inputFieldProxy;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// ListView to display available values.
|
||
|
/// </summary>
|
||
|
[SerializeField]
|
||
|
public TListView TargetListView;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Selected value will be added to this ListView.
|
||
|
/// </summary>
|
||
|
[SerializeField]
|
||
|
public TListView DisplayListView;
|
||
|
|
||
|
/// <summary>
|
||
|
/// List of values.
|
||
|
/// </summary>
|
||
|
[SerializeField]
|
||
|
public List<TValue> DataSource;
|
||
|
|
||
|
/// <summary>
|
||
|
/// The filter.
|
||
|
/// </summary>
|
||
|
[SerializeField]
|
||
|
protected AutocompleteFilter filter;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets or sets the filter.
|
||
|
/// </summary>
|
||
|
/// <value>The filter.</value>
|
||
|
public AutocompleteFilter Filter {
|
||
|
get {
|
||
|
return filter;
|
||
|
}
|
||
|
set {
|
||
|
filter = value;
|
||
|
CustomFilter = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Is filter case sensitive?
|
||
|
/// </summary>
|
||
|
[SerializeField]
|
||
|
public bool CaseSensitive;
|
||
|
|
||
|
/// <summary>
|
||
|
/// The delimiter chars to find word for autocomplete if InputType==Word.
|
||
|
/// </summary>
|
||
|
[SerializeField]
|
||
|
public char[] DelimiterChars = new char[] {' '};
|
||
|
|
||
|
/// <summary>
|
||
|
/// Custom filter.
|
||
|
/// </summary>
|
||
|
public Func<string,ObservableList<TValue>> CustomFilter;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Use entire input or current word in input.
|
||
|
/// </summary>
|
||
|
[SerializeField]
|
||
|
protected AutocompleteInput InputType = AutocompleteInput.Word;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Append value to input or replace input.
|
||
|
/// </summary>
|
||
|
[SerializeField]
|
||
|
protected AutocompleteResult Result = AutocompleteResult.Append;
|
||
|
|
||
|
/// <summary>
|
||
|
/// OnOptionSelected event.
|
||
|
/// </summary>
|
||
|
public UnityEvent OnOptionSelected = new UnityEvent();
|
||
|
|
||
|
/// <summary>
|
||
|
/// Current word in input or whole input for autocomplete.
|
||
|
/// </summary>
|
||
|
[HideInInspector]
|
||
|
protected string Input = string.Empty;
|
||
|
|
||
|
/// <summary>
|
||
|
/// InputField.caretPosition. Used to keep caretPosition with Up and Down actions.
|
||
|
/// </summary>
|
||
|
protected int CaretPosition;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Determines whether the beginnig of value matches the Input.
|
||
|
/// </summary>
|
||
|
/// <param name="value">Value.</param>
|
||
|
/// <returns>true if beginnig of value matches the Input; otherwise, false.</returns>
|
||
|
public abstract bool Startswith(TValue value);
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns a value indicating whether Input occurs within specified value.
|
||
|
/// </summary>
|
||
|
/// <param name="value">Value.</param>
|
||
|
/// <returns>true if the Input occurs within value parameter; otherwise, false.</returns>
|
||
|
public abstract bool Contains(TValue value);
|
||
|
|
||
|
/// <summary>
|
||
|
/// Convert value to string.
|
||
|
/// </summary>
|
||
|
/// <returns>The string value.</returns>
|
||
|
/// <param name="value">Value.</param>
|
||
|
protected abstract string GetStringValue(TValue value);
|
||
|
|
||
|
/// <summary>
|
||
|
/// Start this instance.
|
||
|
/// </summary>
|
||
|
protected virtual void Start()
|
||
|
{
|
||
|
InputFieldProxy.onValueChanged.AddListener(ApplyFilter);
|
||
|
InputFieldProxy.onEndEdit.AddListener(HideOptions);
|
||
|
|
||
|
var inputListener = InputField.GetComponent<InputFieldListener>();
|
||
|
if (inputListener==null)
|
||
|
{
|
||
|
inputListener = InputField.gameObject.AddComponent<InputFieldListener>();
|
||
|
}
|
||
|
inputListener.OnMoveEvent.AddListener(SelectResult);
|
||
|
inputListener.OnSubmitEvent.AddListener(SubmitResult);
|
||
|
|
||
|
DisplayListView.gameObject.SetActive(false);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Canvas will be used as parent for DisplayListView.
|
||
|
/// </summary>
|
||
|
protected Transform CanvasTransform;
|
||
|
|
||
|
/// <summary>
|
||
|
/// To keep DisplayListView position if InputField inside scrollable area.
|
||
|
/// </summary>
|
||
|
protected Vector2 DisplayListViewAnchoredPosition;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Default parent for DisplayListView.
|
||
|
/// </summary>
|
||
|
protected Transform DisplayListViewParent;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Closes the options.
|
||
|
/// </summary>
|
||
|
/// <param name="input">Input.</param>
|
||
|
protected virtual void HideOptions(string input)
|
||
|
{
|
||
|
HideOptions();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Closes the options.
|
||
|
/// </summary>
|
||
|
protected virtual void HideOptions()
|
||
|
{
|
||
|
if (CanvasTransform!=null)
|
||
|
{
|
||
|
DisplayListView.transform.SetParent(DisplayListViewParent);
|
||
|
(DisplayListView.transform as RectTransform).anchoredPosition = DisplayListViewAnchoredPosition;
|
||
|
}
|
||
|
|
||
|
DisplayListView.gameObject.SetActive(false);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Shows the options.
|
||
|
/// </summary>
|
||
|
protected virtual void ShowOptions()
|
||
|
{
|
||
|
CanvasTransform = Utilites.FindTopmostCanvas(DisplayListView.transform);
|
||
|
if (CanvasTransform!=null)
|
||
|
{
|
||
|
DisplayListViewAnchoredPosition = (DisplayListView.transform as RectTransform).anchoredPosition;
|
||
|
DisplayListViewParent = DisplayListView.transform.parent;
|
||
|
DisplayListView.transform.SetParent(CanvasTransform);
|
||
|
}
|
||
|
|
||
|
DisplayListView.gameObject.SetActive(true);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets the results.
|
||
|
/// </summary>
|
||
|
/// <returns>Values matches filter.</returns>
|
||
|
protected virtual ObservableList<TValue> GetResults()
|
||
|
{
|
||
|
if (CustomFilter!=null)
|
||
|
{
|
||
|
return CustomFilter(Input);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if (Filter==AutocompleteFilter.Startswith)
|
||
|
{
|
||
|
return DataSource.Where(Startswith).ToObservableList();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return DataSource.Where(Contains).ToObservableList();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Sets the input.
|
||
|
/// </summary>
|
||
|
protected virtual void SetInput()
|
||
|
{
|
||
|
if (InputType==AutocompleteInput.AllInput)
|
||
|
{
|
||
|
Input = InputFieldProxy.text;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
int end_position = InputFieldProxy.caretPosition;
|
||
|
|
||
|
var text = InputFieldProxy.text.Substring(0, end_position);
|
||
|
var start_position = text.LastIndexOfAny(DelimiterChars) + 1;
|
||
|
|
||
|
Input = text.Substring(start_position).Trim();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Applies the filter.
|
||
|
/// </summary>
|
||
|
/// <param name="input">Input.</param>
|
||
|
protected virtual void ApplyFilter(string input)
|
||
|
{
|
||
|
SetInput();
|
||
|
if (Input.Length==0)
|
||
|
{
|
||
|
HideOptions();
|
||
|
return ;
|
||
|
}
|
||
|
|
||
|
DisplayListView.Start();
|
||
|
DisplayListView.Multiple = false;
|
||
|
|
||
|
DisplayListView.DataSource = GetResults();
|
||
|
|
||
|
if (DisplayListView.DataSource.Count > 0)
|
||
|
{
|
||
|
ShowOptions();
|
||
|
DisplayListView.SelectedIndex = 0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
HideOptions();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Update this instance.
|
||
|
/// </summary>
|
||
|
protected virtual void Update()
|
||
|
{
|
||
|
CaretPosition = InputFieldProxy.caretPosition;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Selects the result.
|
||
|
/// </summary>
|
||
|
/// <param name="eventData">Event data.</param>
|
||
|
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);
|
||
|
InputFieldProxy.caretPosition = CaretPosition;
|
||
|
break;
|
||
|
case MoveDirection.Down:
|
||
|
if (DisplayListView.SelectedIndex==(DisplayListView.DataSource.Count - 1))
|
||
|
{
|
||
|
DisplayListView.SelectedIndex = 0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
DisplayListView.SelectedIndex += 1;
|
||
|
}
|
||
|
DisplayListView.ScrollTo(DisplayListView.SelectedIndex);
|
||
|
InputFieldProxy.caretPosition = CaretPosition;
|
||
|
break;
|
||
|
default:
|
||
|
var oldInput = Input;
|
||
|
SetInput();
|
||
|
if (oldInput!=Input)
|
||
|
{
|
||
|
ApplyFilter("");
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Submits the result.
|
||
|
/// </summary>
|
||
|
/// <param name="eventData">Event data.</param>
|
||
|
protected virtual void SubmitResult(BaseEventData eventData)
|
||
|
{
|
||
|
if (DisplayListView.SelectedIndex==-1)
|
||
|
{
|
||
|
return ;
|
||
|
}
|
||
|
|
||
|
if ((TargetListView!=null) && (DisplayListView.SelectedIndex!=-1))
|
||
|
{
|
||
|
TargetListView.Set(DisplayListView.DataSource[DisplayListView.SelectedIndex]);
|
||
|
}
|
||
|
|
||
|
int end_position = (DisplayListView.gameObject.activeInHierarchy) ? InputFieldProxy.caretPosition : CaretPosition;
|
||
|
|
||
|
var text = InputFieldProxy.text.Substring(0, end_position);
|
||
|
var start_position = text.LastIndexOfAny(DelimiterChars) + 1;
|
||
|
|
||
|
var value = GetStringValue(DisplayListView.DataSource[DisplayListView.SelectedIndex]);
|
||
|
InputFieldProxy.text = InputFieldProxy.text.Substring(0, start_position) + value + InputFieldProxy.text.Substring(end_position);
|
||
|
|
||
|
|
||
|
#if UNITY_4_6 || UNITY_4_7 || UNITY_5_0
|
||
|
//InputField.gameObject.SetActive(false);
|
||
|
//InputField.gameObject.SetActive(true);
|
||
|
InputField.ActivateInputField();
|
||
|
#else
|
||
|
InputFieldProxy.caretPosition = start_position + value.Length;
|
||
|
#endif
|
||
|
|
||
|
OnOptionSelected.Invoke();
|
||
|
|
||
|
HideOptions();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// This function is called when the MonoBehaviour will be destroyed.
|
||
|
/// </summary>
|
||
|
protected virtual void OnDestroy()
|
||
|
{
|
||
|
if (InputField!=null)
|
||
|
{
|
||
|
InputFieldProxy.onValueChanged.RemoveListener(ApplyFilter);
|
||
|
InputFieldProxy.onEndEdit.RemoveListener(HideOptions);
|
||
|
|
||
|
var inputListener = InputField.GetComponent<InputFieldListener>();
|
||
|
if (inputListener!=null)
|
||
|
{
|
||
|
inputListener.OnMoveEvent.RemoveListener(SelectResult);
|
||
|
inputListener.OnSubmitEvent.RemoveListener(SubmitResult);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|