Unity Editor下拉框,支持搜索,多层级

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);
        }
    }

}
相关推荐
waving-black1 小时前
windows系统下安装测试kafka
windows·分布式·kafka
傻啦嘿哟3 小时前
Python 高效清理 Excel 空白行列:从原理到实战
windows
晋人在秦 老K4 小时前
鼠标连点效率低?MouseClickTool 2.0三步配置,自动化操作提速80% 重复点击太耗时?Windows鼠标连点器实操教程,开发测试效率翻倍
windows·自动化·计算机外设·鼠标连点器·gui模拟点击·自动化点击解决方案
shykevin4 小时前
uni-app x商城,商品列表组件封装以及使用
windows·uni-app
开发游戏的老王8 小时前
虚幻引擎入门教程:虚幻编辑器的基本操作
编辑器·游戏引擎·虚幻
AA陈超11 小时前
虚幻引擎5 GAS开发俯视角RPG游戏 P05-08 UI 部件数据表
c++·游戏·ue5·游戏引擎·虚幻
CHH321314 小时前
在 Mac/linux 的 VSCode 中使用Remote-SSH远程连接 Windows
linux·windows·vscode·macos
future_studio14 小时前
聊聊 Unity(小白专享、C# 小程序 之 播放器)
unity·小程序·c#
向宇it15 小时前
【unity实战】MapMagic 2实战例子
游戏·3d·unity·c#·游戏引擎
kobe_OKOK_16 小时前
windows 下载 pip包,debian离线安装
windows·debian·pip