Unity Editor下拉框,支持搜索,多层级
csharp
using Sirenix.OdinInspector;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Tools
{
public class TGDropdownView
{
private List<DropdownItem> rootItems;
private DropdownItem selectedItem = null;
private Action<DropdownItem> onSelect;
private string searchText = "";
private Vector2 scrollPos;
private bool showAllItemsOnOpen = true; // 控制弹窗是否显示所有项(空搜索时)
private bool showFullPathOnHeader = true; // 控制下拉框按钮显示完整路径还是只显示节点名
private DropdownItem hoverItem = null;
private GUIStyle itemStyle;
private HashSet<DropdownItem> matchedItems = new();
public TGDropdownView(List<DropdownItem> items, Action<DropdownItem> onSelectCallback)
{
rootItems = items;
selectedItem = GetFirstLeaf(items);
onSelect = onSelectCallback;
itemStyle = new GUIStyle(EditorStyles.label)
{
normal = { textColor = EditorGUIUtility.isProSkin ? Color.white : Color.black }
};
RefreshMatchedItems();
// 初始化时如果有默认选择项,触发回调
if (selectedItem != null)
{
onSelect?.Invoke(selectedItem);
}
}
// 外部控制属性
public bool ShowAllItemsOnOpen
{
get => showAllItemsOnOpen;
set => showAllItemsOnOpen = value;
}
public bool ShowFullPathOnHeader
{
get => showFullPathOnHeader;
set => showFullPathOnHeader = value;
}
// 获取当前选择的项
public DropdownItem SelectedItem => selectedItem;
// 只画按钮,点击弹出窗口
public void DrawInline(string label = "", float labelWidth = 100f, bool showLabel = true)
{
EditorGUILayout.BeginHorizontal();
if (showLabel)
{
EditorGUILayout.LabelField(label, GUILayout.Width(labelWidth));
}
DrawHeader();
EditorGUILayout.EndHorizontal();
}
// 下拉框按钮绘制和弹窗触发
private void DrawHeader()
{
string buttonText;
if (rootItems == null || rootItems.Count == 0)
{
buttonText = "当前无任务";
}
else if (selectedItem == null)
{
buttonText = "请选择 ▼";
}
else
{
buttonText = showFullPathOnHeader ? selectedItem.GetFullPath() : selectedItem.Name;
}
float maxWidth = EditorGUIUtility.currentViewWidth / 2 + 50;
Rect buttonRect = GUILayoutUtility.GetRect(new GUIContent(buttonText), EditorStyles.popup,
GUILayout.ExpandWidth(true), GUILayout.MaxWidth(maxWidth));
GUI.enabled = !(rootItems == null || rootItems.Count == 0);
if (GUI.Button(buttonRect, buttonText, EditorStyles.popup))
{
if (rootItems != null && rootItems.Count > 0)
{
PopupWindow.Show(buttonRect, new TGDropdownPopup(this, buttonRect.width));
}
}
GUI.enabled = true;
}
// 弹窗内绘制内容
public void DrawPopupContent()
{
DrawSearchBox();
Rect lineRect = EditorGUILayout.GetControlRect(false, 1);
EditorGUI.DrawRect(lineRect, new Color(0.5f, 0.5f, 0.5f, 1f));
DrawItemList();
}
private void DrawSearchBox()
{
float lineHeight = 20f;
EditorGUILayout.BeginHorizontal(GUILayout.Height(lineHeight));
// 搜索图标,限制宽高并垂直居中
GUILayout.Label(EditorGUIUtility.IconContent("Search Icon"), GUILayout.Width(lineHeight), GUILayout.Height(lineHeight));
// 搜索文字,固定宽度,高度和图标一样
GUILayout.Label("搜索", GUILayout.Width(40), GUILayout.Height(lineHeight));
// 输入框,高度和图标一致,宽度自动扩展
EditorGUI.BeginChangeCheck();
searchText = EditorGUILayout.TextField(searchText, GUILayout.Height(lineHeight));
if (EditorGUI.EndChangeCheck())
{
RefreshMatchedItems();
}
EditorGUILayout.EndHorizontal();
}
private void DrawItemList()
{
if (matchedItems.Count == 0)
{
EditorGUILayout.LabelField("没有匹配的项目", EditorStyles.miniLabel);
return;
}
scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Height(400));
foreach (var root in rootItems)
{
DrawItemRecursive(root);
}
EditorGUILayout.EndScrollView();
}
private void DrawItemRecursive(DropdownItem item)
{
if (!matchedItems.Contains(item)) return;
// 先计算缩进后按钮区域的Rect
float indent = item.Depth * 20;
// 先获取整行rect(不含缩进)
Rect fullRect = GUILayoutUtility.GetRect(1, 20, GUILayout.ExpandWidth(true));
// 缩进后的按钮区域
Rect buttonRect = new Rect(fullRect.x + indent, fullRect.y, fullRect.width - indent, fullRect.height);
Vector2 mousePos = Event.current.mousePosition;
bool isHover = buttonRect.Contains(mousePos);
if (isHover)
{
hoverItem = item;
EditorWindow.focusedWindow.Repaint();
}
EditorGUI.DrawRect(fullRect, isHover ? new Color(0.24f, 0.48f, 0.90f, 1f) : Color.clear);
GUI.enabled = item.IsLeaf;
if (GUI.Button(buttonRect, item.Name, itemStyle))
{
if (item.IsLeaf)
{
selectedItem = item;
onSelect?.Invoke(item);
EditorWindow.focusedWindow?.Close();
}
}
GUI.enabled = true;
var lineRect = GUILayoutUtility.GetRect(1, 1, GUILayout.ExpandWidth(true));
EditorGUI.DrawRect(lineRect, new Color(0.3f, 0.3f, 0.3f, 0.4f));
foreach (var child in item.Children)
{
DrawItemRecursive(child);
}
}
private bool MatchItemRecursive(DropdownItem item)
{
// 去掉搜索词中的空格
string trimmedSearchText = searchText.Replace(" ", "");
// 去掉路径中的空格
string itemPath = item.GetFullPath().Replace(" ", "");
// 判断是否匹配
bool matched = string.IsNullOrEmpty(trimmedSearchText) ||
itemPath.IndexOf(trimmedSearchText, StringComparison.OrdinalIgnoreCase) >= 0;
bool childMatched = false;
foreach (var child in item.Children)
{
childMatched |= MatchItemRecursive(child);
}
if (matched || childMatched)
{
matchedItems.Add(item);
if (item.Parent != null) matchedItems.Add(item.Parent);
return true;
}
return false;
}
// 刷新匹配项的公共方法
public void RefreshMatchedItems()
{
matchedItems.Clear();
if (showAllItemsOnOpen && string.IsNullOrEmpty(searchText))
{
// 直接把所有节点及子节点都加进matchedItems
void AddAllItems(DropdownItem item)
{
matchedItems.Add(item);
foreach (var child in item.Children)
{
AddAllItems(child);
}
}
foreach (var root in rootItems)
{
AddAllItems(root);
}
}
else
{
foreach (var root in rootItems)
{
MatchItemRecursive(root);
}
}
}
private DropdownItem GetFirstLeaf(List<DropdownItem> items)
{
foreach (var item in items)
{
if (item.IsLeaf) return item;
var leaf = GetFirstLeaf(item.Children);
if (leaf != null) return leaf;
}
return null;
}
}
// 弹窗类
public class TGDropdownPopup : PopupWindowContent
{
private TGDropdownView dropdown;
private float popupWidth;
public TGDropdownPopup(TGDropdownView dropdownView, float width)
{
dropdown = dropdownView;
popupWidth = width;
}
public override Vector2 GetWindowSize()
{
return new Vector2(popupWidth, 425);
}
public override void OnGUI(Rect rect)
{
dropdown.DrawPopupContent();
}
}
public class DropdownItem
{
public string Name;
public int Depth;
//public object[] UserData;
public object UserData;
public DropdownItem Parent;
public List<DropdownItem> Children = new();
public DropdownItem(string name, int depth = 0, object userData = null)
{
Name = name;
Depth = depth;
UserData = userData;
}
public bool IsLeaf => Children.Count == 0;
public string GetFullPath()
{
return Parent == null ? Name : Parent.GetFullPath() + "/" + Name;
}
}
}
csharp
using Newtonsoft.Json;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace Tools
{
[Serializable]
public class TGDropdownViewBox
{
private TGDropdownView dropdown;
public TGDropdownView Dropdown => dropdown;
public TGDropdownViewBox(List<DropdownItem> items, Action<DropdownItem> onSelectCallback)
{
dropdown = new TGDropdownView(items, onSelectCallback);
}
[OnInspectorGUI]
private void DrawCustomInspector()
{
//GUILayout.Space(10);
dropdown?.DrawInline(showLabel: false);
}
}
}