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

}
相关推荐
cpsvps1 小时前
Windows远程FX的编解码器性能优化
windows·性能优化
心疼你的一切3 小时前
Unity 多人游戏框架学习系列一
学习·游戏·unity·c#·游戏引擎
示申○言舌3 小时前
Unity沉浸式/360View/全景渲染
unity·游戏引擎·沉浸式·360view·全景视图·全景渲染
枯萎穿心攻击8 小时前
响应式编程入门教程第三节:ReactiveCommand 与 UI 交互
开发语言·ui·unity·架构·c#·游戏引擎·交互
死也不注释8 小时前
第一章编辑器开发基础第一节绘制编辑器元素_4输入字段(4/7)
unity·编辑器
TinpeaV8 小时前
Elasticsearch8 Windows安装教程
windows·spring boot·后端·elasticsearch
乘风!9 小时前
SpringBoot集成SAP,本地IDEA启动和Windows服务器部署
windows·spring boot·intellij-idea
@淡 定9 小时前
Sentinel热点参数限流完整示例实现
windows·sentinel
李詹10 小时前
Pitaya 是一个简单、快速、轻量级的游戏服务器框架,它为分布式多人游戏和服务器端应用程序提供了一个基本的开发框架
游戏·游戏引擎·游戏程序