Unity3D Editor TextField AutoCompelete

Unity3D Editor TextField AutoCompelete

New solution can be find here.
日前在處理 170 多個 BlendShape 時處理設定的 Mapping,
那堆 1080×1920 螢幕也不能完全顯示的 Popup 當中用滑鼠點取自己需要的項目真的非常有難度.

為了可以在超過 100多個的選項當中找出自己需要的字串, (是眼力訓練的一種)
所以花了點時間把一直想用的 Auto Complete 功能做出來….
嗯… 一半罷.

結果如下:

Source Code

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
public sealed class EditorExtend
{
#region Text AutoComplete
	private const string m_AutoCompleteField = "AutoCompleteField";
	private static List<string> m_CacheCheckList = null;
	private static string m_AutoCompleteLastInput;
	private static string m_EditorFocusAutoComplete;
	/// <summary>A textField to popup a matching popup, based on developers input values.</summary>
	/// <param name="input">string input.</param>
	/// <param name="source">the data of all possible values (string).</param>
	/// <param name="maxShownCount">the amount to display result.</param>
	/// <param name="levenshteinDistance">
	/// value between 0f ~ 1f,
	/// - more then 0f will enable the fuzzy matching
	/// - 1f = anything thing is okay.
	/// - 0f = require full match to the reference
	/// - recommend 0.4f ~ 0.7f
	/// </param>
	/// <returns>output string.</returns>
	public static string TextFieldAutoComplete(Rect position, string input, string[] source, int maxShownCount = 5, float levenshteinDistance = 0.5f)
	{
		string tag = m_AutoCompleteField + GUIUtility.GetControlID(FocusType.Passive);
		int uiDepth = GUI.depth;
		GUI.SetNextControlName(tag);
		string rst = EditorGUI.TextField(position, input);
		if (input.Length > 0 && GUI.GetNameOfFocusedControl() == tag)
		{
			if (m_AutoCompleteLastInput != input || // input changed
				m_EditorFocusAutoComplete != tag) // another field.
			{
				// Update cache
				m_EditorFocusAutoComplete = tag;
				m_AutoCompleteLastInput = input;

				List<string> uniqueSrc = new List<string>(new HashSet<string>(source)); // remove duplicate
				int srcCnt = uniqueSrc.Count;
				m_CacheCheckList = new List<string>(System.Math.Min(maxShownCount, srcCnt)); // optimize memory alloc

				// Start with - slow
				for (int i = 0; i < srcCnt && m_CacheCheckList.Count < maxShownCount; i++)
				{
					if (uniqueSrc[i].ToLower().StartsWith(input.ToLower()))
					{
						m_CacheCheckList.Add(uniqueSrc[i]);
						uniqueSrc.RemoveAt(i);
						srcCnt--;
						i--;
					}
				}

				// Contains - very slow
				if (m_CacheCheckList.Count == 0)
				{
					for (int i = 0; i < srcCnt && m_CacheCheckList.Count < maxShownCount; i++)
					{
						if (uniqueSrc[i].ToLower().Contains(input.ToLower()))
						{
							m_CacheCheckList.Add(uniqueSrc[i]);
							uniqueSrc.RemoveAt(i);
							srcCnt--;
							i--;
						}
					}
				}

				// Levenshtein Distance - very very slow.
				if (levenshteinDistance > 0f && // only developer request
					input.Length > 3 && // 3 characters on input, hidden value to avoid doing too early.
					m_CacheCheckList.Count < maxShownCount) // have some empty space for matching.
				{
					levenshteinDistance = Mathf.Clamp01(levenshteinDistance);
					string keywords = input.ToLower();
					for (int i = 0; i < srcCnt && m_CacheCheckList.Count < maxShownCount; i++)
					{
						int distance = Kit.Extend.StringExtend.LevenshteinDistance(uniqueSrc[i], keywords, caseSensitive: false);
						bool closeEnough = (int)(levenshteinDistance * uniqueSrc[i].Length) > distance;
						if (closeEnough)
						{
							m_CacheCheckList.Add(uniqueSrc[i]);
							uniqueSrc.RemoveAt(i);
							srcCnt--;
							i--;
						}
					}
				}
			}

			// Draw recommend keyward(s)
			if (m_CacheCheckList.Count > 0)
			{
				int cnt = m_CacheCheckList.Count;
				float height = cnt * EditorGUIUtility.singleLineHeight;
				Rect area = position;
				area = new Rect(area.x, area.y - height, area.width, height);
				GUI.depth-=10;
				// GUI.BeginGroup(area);
				// area.position = Vector2.zero;
				GUI.BeginClip(area);
				Rect line = new Rect(0, 0, area.width, EditorGUIUtility.singleLineHeight);
					
				for (int i = 0; i < cnt; i++)
				{
					if (GUI.Button(line, m_CacheCheckList[i]))//, EditorStyles.toolbarDropDown))
					{
						rst = m_CacheCheckList[i];
						GUI.changed = true;
						GUI.FocusControl(""); // force update
					}
					line.y += line.height;
				}
				GUI.EndClip();
				//GUI.EndGroup();
				GUI.depth+=10;
			}
		}
		return rst;
	}

	public static string TextFieldAutoComplete(string input, string[] source, int maxShownCount = 5, float levenshteinDistance = 0.5f)
	{
		Rect rect = EditorGUILayout.GetControlRect();
		return TextFieldAutoComplete(rect, input, source, maxShownCount, levenshteinDistance);
	}
	#endregion
}

 

使用很簡單在 Editor 的範圍內直接呼叫即可.

Usage Demo :

// string[] someNames = new[] { "Apple", "Banana", "Circle", "Rect" }
// string temp;
public override void OnInspectorGUI()
{
	temp = EditorExtend.TextFieldAutoComplete(temp, someNames, maxShownCount: 10, levenshteinDistance: 0.5f);
}

 

Bugs:

  1. 某些時候好像很難點到.
  2. 不支援摸糊對比, 更新後支援
  3. 包含空格

// Updated : Fuzzy matching///////////////////////////////////

更新後支援有限度的模糊對比, 參考的是 萊文斯坦距離(Levenshtein Distance) 的做法.
原始版本: https://blogs.msdn.microsoft.com/toub/2006/05/05/generic-levenshtein-edit-distance-with-c/

這邊放一個備份:

/// <summary>Computes the Levenshtein Edit Distance between two enumerables.</summary>
/// <typeparam name="T">The type of the items in the enumerables.</typeparam>
/// <param name="lhs">The first enumerable.</param>
/// <param name="rhs">The second enumerable.</param>
/// <returns>The edit distance.</returns>
/// <see cref="https://blogs.msdn.microsoft.com/toub/2006/05/05/generic-levenshtein-edit-distance-with-c/"/>
public static int LevenshteinDistance<T>(IEnumerable<T> lhs, IEnumerable<T> rhs) where T : System.IEquatable<T>
{
	// Validate parameters
	if (lhs == null) throw new System.ArgumentNullException("lhs");
	if (rhs == null) throw new System.ArgumentNullException("rhs");

	// Convert the parameters into IList instances
	// in order to obtain indexing capabilities
	IList<T> first = lhs as IList<T> ?? new List<T>(lhs);
	IList<T> second = rhs as IList<T> ?? new List<T>(rhs);

	// Get the length of both.  If either is 0, return
	// the length of the other, since that number of insertions
	// would be required.
	int n = first.Count, m = second.Count;
	if (n == 0) return m;
	if (m == 0) return n;

	// Rather than maintain an entire matrix (which would require O(n*m) space),
	// just store the current row and the next row, each of which has a length m+1,
	// so just O(m) space. Initialize the current row.
	int curRow = 0, nextRow = 1;

	int[][] rows = new int[][] { new int[m + 1], new int[m + 1] };
	for (int j = 0; j <= m; ++j)
		rows[curRow][j] = j;

	// For each virtual row (since we only have physical storage for two)
	for (int i = 1; i <= n; ++i)
	{
		// Fill in the values in the row
		rows[nextRow][0] = i;

		for (int j = 1; j <= m; ++j)
		{
			int dist1 = rows[curRow][j] + 1;
			int dist2 = rows[nextRow][j - 1] + 1;
			int dist3 = rows[curRow][j - 1] +
				(first[i - 1].Equals(second[j - 1]) ? 0 : 1);

			rows[nextRow][j] = System.Math.Min(dist1, System.Math.Min(dist2, dist3));
		}

		// Swap the current and next rows
		if (curRow == 0)
		{
			curRow = 1;
			nextRow = 0;
		}
		else
		{
			curRow = 0;
			nextRow = 1;
		}
	}

	// Return the computed edit distance
	return rows[curRow][m];
}

這邊再做一個 overloader 為了簡單的 string 接口.

/// <summary>Computes the Levenshtein Edit Distance between two enumerables.</summary>
/// <param name="lhs">The first enumerable.</param>
/// <param name="rhs">The second enumerable.</param>
/// <returns>The edit distance.</returns>
/// <see cref="https://en.wikipedia.org/wiki/Levenshtein_distance"/>
public static int LevenshteinDistance(string lhs, string rhs, bool caseSensitive = true)
{
	if (!caseSensitive)
	{
		lhs = lhs.ToLower();
		rhs = rhs.ToLower();
	}
	char[] first = lhs.ToCharArray();
	char[] second = rhs.ToCharArray();
	return LevenshteinDistance<char>(first, second);
}

 

7 Comments

  1. Pingback: Clonefactor | Unity3D Editor TextField AutoCompelete (Version 2)

  2. Hello!
    Sorry for bothering. Ehm, of course I am a newbie. I need a “prediction search box” in my app. This app is a sort of dictionary, downloading data from an external sql database. So .. i “know” how to fill arrays from mysql, but now … i’d need a searching box for terms that would autocomplete like i saw in you demo. Is this possible with your code? How do you suggest me to proceed? Thanks!
    G.

  3. Pingback: Опыт написания кастомного редактора, окна для инструментов, расширения для ускорение и автоматизации рутинных задач - myunity.dev

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

*

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料