日前在處理 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:
- 某些時候好像很難點到.
不支援摸糊對比, 更新後支援包含空格
// 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); }
Pingback: Clonefactor | Unity3D Editor TextField AutoCompelete (Version 2)
The name `Kit’ does not exist in the current context
Kit.Extend.StringExtend.LevenshteinDistance
was posted above, sorry for the namespace.
just erase the Part <> and it works (after you paste the “fuzzy matching” stuff)
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.
“Levenshtein Distance” its the core to do this kind of checking,
I study from https://blogs.msdn.microsoft.com/toub/2006/05/05/generic-levenshtein-edit-distance-with-c/
the rest of my code only depend on unity, hope it can help you.
Pingback: Опыт написания кастомного редактора, окна для инструментов, расширения для ускорение и автоматизации рутинных задач - myunity.dev