using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using TyphoonGUIStyle; using TyphoonIniConfig; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace TyphoonFontTool { public class CollectCharacterWindow : EditorWindow { //过滤文件 private static HashSet _filterExtensions = new HashSet() { ".unity", ".prefab", ".asset", ".txt", ".xml", ".csv", ".json", ".cs", ".js", ".ts", ".py", ".lua", ".html", ".bytes", }; public class AssetItemInfo { public string AssetPath; public GUIContent Label; public GUIContent MissingLabel; private bool? _missing = null; public bool IsMissing { get { if (_missing == null) { _missing = AssetDatabase.LoadAssetAtPath(AssetPath) == null; } return _missing.Value; } } public AssetItemInfo(string assetPath, GUIContent label, GUIContent missingLabel) { AssetPath = assetPath; Label = label; MissingLabel = missingLabel; } } private const string COLLECT_CHARACTER_WINDOW_SELECT_SETTING_NAME = "COLLECT_CHARACTER_WINDOW_SELECT_SETTING_NAME"; private const float MENU_WIDTH = 226; private const float BOTTOM_LAYOUT_HEIGHT = 44; private const float ASSET_ITEM_HEIGHT = 16; private static CollectCharactersSetting _select = null; private static List _settings; private static INIConfig _iniConfig = null; private static Vector2 _scrollMenu; private static Vector2 _scrollAssetsView; private static Rect _dropArea; private static Rect _assetItemsViewArea; private static bool _isDragIn = false; private static string _searchInput; private static Dictionary _assetItemInfo = new Dictionary(); //资源labels private static Dictionary _assetRects = new Dictionary(); //缓存资源UI Rect private static HashSet _selectAssetsGUID = new HashSet(); private static string _lastClickAssetGUID = ""; private static List _viewAssetsGUID = new List(); private static GUIStyle _styleHelpBox = null; private static GUIStyle StyleHelpBox { get { if (_styleHelpBox == null) { _styleHelpBox = new GUIStyle(Styles.HelpBox); _styleHelpBox.richText = true; } return _styleHelpBox; } } private static GUIStyle _styleLabelAssetInfo = null; private static GUIStyle StyleLabelAssetInfo { get { if (_styleLabelAssetInfo == null) { _styleLabelAssetInfo = new GUIStyle("label"); _styleLabelAssetInfo.fontSize = 11; _styleLabelAssetInfo.richText = true; _styleLabelAssetInfo.padding = new RectOffset(6, 0, 0, 0); _styleLabelAssetInfo.margin = new RectOffset(0, 0, 0, 0); _styleLabelAssetInfo.border = new RectOffset(0, 0, 0, 0); _styleLabelAssetInfo.overflow = new RectOffset(0, 0, 0, 0); _styleLabelAssetInfo.alignment = TextAnchor.MiddleLeft; _styleLabelAssetInfo.normal.textColor = EditorGUIUtility.isProSkin ? new Color(0.43f, 0.43f, 0.43f, 1f) : new Color(0.40F, 0.40F, 0.40F, 1f); } return _styleLabelAssetInfo; } } private static INIConfig IniConfig { get { if (_iniConfig == null) { _iniConfig = new INIConfig(_Editor.INIConfigPath); } return _iniConfig; } } private static AssetItemInfo GetAssetItemInfo(string guid) { if (_assetItemInfo.TryGetValue(guid, out var match)) { return match; } var hex = EditorGUIUtility.isProSkin ? "#d0d0d0" : "#070707"; var path = AssetDatabase.GUIDToAssetPath(guid); var items = path.Split('/'); if (items.Length > 0) { var content = items[items.Length - 1]; //加粗显示 items[items.Length - 1] = $"{content}"; } var labelString = string.Join("/", items); //获取资源图标 var icon = AssetDatabase.GetCachedIcon(path); var label = new GUIContent(labelString, icon); var missing = new GUIContent($"(missing) {path}", Styles.IconWarning); var result = new AssetItemInfo(path, label, missing); _assetItemInfo[guid] = result; return result; } [MenuItem("Typhoon/FontTool/收集字符")] private static void Open() { var win = GetWindow(); win.minSize = new Vector2(800, 600); win.titleContent = new GUIContent("收集字符"); win.Show(); } private void OnEnable() { _isDragIn = false; _assetItemInfo.Clear(); RefreshSettings(); SelectLast(); // EditorApplication.delayCall -= OnEditorApplicationDelay; // EditorApplication.delayCall += OnEditorApplicationDelay; } private void OnDisable() { // EditorApplication.delayCall -= OnEditorApplicationDelay; } private void OnDestroy() { // EditorApplication.delayCall -= OnEditorApplicationDelay; } private void OnGUI() { //更新数据 HandleEventInput(); var area = new Rect(Vector2.zero, position.size); var leftLayout = area; leftLayout.width = MENU_WIDTH; DrawLeftLayoutGUI(leftLayout); var line = leftLayout; line.x = leftLayout.xMax; line.width = 1; var center = line.center; var draw = line; draw.height -= 8; draw.center = center; EditorGUI.DrawRect(draw, Color.black); var rightLayout = line; rightLayout.x = line.xMax; rightLayout.width = area.xMax - rightLayout.x; rightLayout.height -= BOTTOM_LAYOUT_HEIGHT; DrawRightLayoutGUI(rightLayout); var bottomLayout = rightLayout; bottomLayout.y = rightLayout.yMax; bottomLayout.height = area.yMax - bottomLayout.y; DrawBottomLayoutGUI(bottomLayout); DrawEventInputGUI(); Repaint(); } private void OnFocus() { _isDragIn = false; _assetItemInfo.Clear(); RefreshSettings(); SelectLast(); } private void DrawLeftLayoutGUI(Rect area) { var draw = area; var center = area.center; draw.height -= 8; draw.width -= 8; draw.center = center; GUILayout.BeginArea(draw); GUILayout.Label("配置", Styles.TitleBar); //绘制创建创建按钮 if (GUILayout.Button("+创建", Styles.BtnGreen)) { var setting = CreateSetting(); RefreshSettings(); Select(setting); } _scrollMenu = GUILayout.BeginScrollView(_scrollMenu, false, false, GUIStyle.none, GUIStyle.none); foreach (var setting in _settings) { //绘制 if (setting == null) { continue; } var style = _select == setting ? Styles.Square.MenuButtonActive : Styles.Square.MenuButton; if (GUILayout.Button(setting.name, style, GUILayout.Width(draw.width - 5))) { if (_select == setting) { Selection.activeObject = setting; _Editor.Ping(setting); } Select(setting); } GUILayout.Space(1); } GUILayout.Space(26); GUILayout.EndScrollView(); GUILayout.EndArea(); Repaint(); } private void DrawRightLayoutGUI(Rect area) { var draw = area; var center = area.center; draw.height -= 8; draw.width -= 8; draw.center = center; GUILayout.BeginArea(draw); var rect = new Rect(Vector2.zero, draw.size); if (_select == null) { GUILayout.Label(new GUIContent("请选择配置", Styles.IconInfo), Styles.HelpBox); } else { GUILayout.Label(_select.name, Styles.TitleBar); var include = _select.RunMode == CollectCharactersSetting.Mode.Include; var txt = include ? "拖拽包含的资源项到此窗口,最终进行字符汇总分析" : "拖拽排除的资源项到此窗口,最终进行字符汇总分析"; GUILayout.Label(new GUIContent(txt, Styles.IconInfo), StyleHelpBox); var last = GUILayoutUtility.GetLastRect(); var btnModeRect = last; btnModeRect.width = 120; btnModeRect.height = 22; btnModeRect.center = last.center; btnModeRect.x = last.xMax - btnModeRect.width; btnModeRect.x -= 4; var modeTxt = include ? "模式:包含" : "模式:排除"; if (GUI.Button(btnModeRect, modeTxt, "PopUp")) { var menu = new GenericMenu(); menu.AddItem(new GUIContent("包含"), include, () => { _select.RunMode = CollectCharactersSetting.Mode.Include; _select.Save(); }); menu.AddItem(new GUIContent("排除"), !include, () => { _select.RunMode = CollectCharactersSetting.Mode.Exclude; _select.Save(); }); menu.ShowAsContext(); } last.y = last.yMax; last.height = rect.yMax - last.y; if (Event.current.type == EventType.Repaint) { _dropArea = last; _dropArea.position += draw.position; } } GUILayout.EndArea(); //绘制资源项 DrawAssetsSources(_dropArea); } private void DrawAssetsSources(Rect area) { if (_select != null) { var searchArea = area; searchArea.height = 18; var center = searchArea.center; var draw = searchArea; draw.height = 16; draw.width -= 4; draw.center = center; _searchInput = EditorGUI.TextField(draw, _searchInput); var viewArea = area; viewArea.height -= searchArea.height; viewArea.y = searchArea.yMax; switch (_select.RunMode) { case CollectCharactersSetting.Mode.Include: DrawAssetsViewGUI(viewArea, _select.Includes); break; case CollectCharactersSetting.Mode.Exclude: DrawAssetsViewGUI(viewArea, _select.Excludes); break; default: throw new ArgumentOutOfRangeException(); } _assetItemsViewArea = viewArea; } } private void DrawAssetsViewGUI(Rect area, List inputAssets) { _assetRects.Clear(); var matches = inputAssets.ToList(); if (!string.IsNullOrEmpty(_searchInput)) { var low = _searchInput.ToLower(); for (int i = matches.Count - 1; i >= 0; i--) { var guid = matches[i]; var label = GetAssetItemInfo(guid); if (!label.Label.text.ToLower().Contains(low)) { matches.RemoveAt(i); } } } var assets = matches; //缓存当前预览项 _viewAssetsGUID = assets.ToList(); var padding = new RectOffset(0, 0, 4, 20); var spacing = 0; var height = assets.Count * ASSET_ITEM_HEIGHT + padding.vertical + spacing * assets.Count; var viewRect = new Rect(Vector2.zero, new Vector2(area.width, height)); _scrollAssetsView = GUI.BeginScrollView(area, _scrollAssetsView, viewRect, GUIStyle.none, GUI.skin.verticalScrollbar); //计算绘制范围 var eye = new Vector2(_scrollAssetsView.y - ASSET_ITEM_HEIGHT, _scrollAssetsView.y + area.height + padding.vertical + ASSET_ITEM_HEIGHT * 2); var index = 0; foreach (var item in assets) { var itemRect = viewRect; itemRect.height = ASSET_ITEM_HEIGHT; itemRect.y = index * ASSET_ITEM_HEIGHT; itemRect.y += padding.top; itemRect.y += index * spacing; itemRect.x += padding.left; itemRect.width -= padding.horizontal; DrawAssetItem(itemRect, item, index, eye); index += 1; var windowRect = itemRect; windowRect.position += area.position; windowRect.position -= _scrollAssetsView; _assetRects.Add(item, windowRect); } GUI.EndScrollView(); } private void DrawAssetItem(Rect rect, string guid, int index, Vector2 eye) { if (index % 2 == 0) { EditorGUI.DrawRect(rect, Color.black * 0.1f); } if (_selectAssetsGUID.Contains(guid)) { EditorGUI.DrawRect(rect, Styles.ColorSelected * 0.8f); } var renderFlag = rect.y >= eye.x && rect.y < eye.y; var info = GetAssetItemInfo(guid); //绘制toggle var tog = rect; tog.width = rect.height; var center = tog.center; var draw = tog; draw.width -= 2; draw.height -= 2; draw.center = center; var isSelect = _selectAssetsGUID.Contains(guid); var select = isSelect; if (renderFlag) { select = GUI.Toggle(draw, select, ""); } if (select != isSelect) { if (select) { _selectAssetsGUID.Add(guid); } else { _selectAssetsGUID.Remove(guid); } } var labelRect = rect; labelRect.x = tog.xMax; labelRect.width -= tog.width; if (renderFlag) { if (info.IsMissing) { GUI.Label(labelRect, info.MissingLabel, StyleLabelAssetInfo); } else { GUI.Label(labelRect, info.Label, StyleLabelAssetInfo); } } if (renderFlag && rect.Contains(Event.current.mousePosition)) { EditorGUI.DrawRect(rect, Styles.ColorYellow * 0.2f); } } private void DrawBottomLayoutGUI(Rect area) { EditorGUI.DrawRect(area, Color.white * 0.1f); var line = area; line.height = 1; EditorGUI.DrawRect(line, Color.black); var btnParserRect = area; btnParserRect.width = 140; btnParserRect.x = area.xMax - btnParserRect.width; var center = btnParserRect.center; var draw = btnParserRect; draw.width -= 8; draw.height -= 8; draw.center = center; var temEnable = GUI.enabled; GUI.enabled = _select != null; if (GUI.Button(draw, "收集字符", Styles.BtnGreen)) { CollectCharacters(_select); } GUI.enabled = temEnable; } private void HandleEventInput() { if (Event.current.type == EventType.MouseDown && Event.current.button == 1) { //右键菜单 if (_assetItemsViewArea.Contains(Event.current.mousePosition)) { var menu = new GenericMenu(); GenericMenu.MenuFunction remove = null; if (_selectAssetsGUID.Count > 0) { remove = () => { //从当前所选的移除项目 _select.RemoveAssets(_selectAssetsGUID.ToArray()); _selectAssetsGUID.Clear(); }; } menu.AddItem(new GUIContent("移除所选"), false, remove); if (TryCheckAssetItemIsClick(Event.current.mousePosition, out var clickGuid)) { menu.AddItem(new GUIContent("复制路径"), false, () => { EditorGUIUtility.systemCopyBuffer = AssetDatabase.GUIDToAssetPath(clickGuid); }); menu.AddItem(new GUIContent("Ping"), false, () => { var path = AssetDatabase.GUIDToAssetPath(clickGuid); if (!string.IsNullOrEmpty(path)) { _Editor.Ping(path, true); } }); } menu.ShowAsContext(); Event.current.Use(); } } if (Event.current.type == EventType.MouseDown && Event.current.button == 0) { var clickFlag = TryCheckAssetItemIsClick(Event.current.mousePosition, out var clickGuid); if (clickFlag) { if (Event.current.clickCount >= 2) { _Editor.Ping(AssetDatabase.GUIDToAssetPath(clickGuid)); } switch (Event.current.modifiers) { case EventModifiers.Control: { //补选 if (!_selectAssetsGUID.Add(clickGuid)) { _selectAssetsGUID.Remove(clickGuid); } } break; case EventModifiers.Shift: { var viewAssets = _viewAssetsGUID.ToHashSet(); //范围选中 if (viewAssets.Contains(_lastClickAssetGUID) && viewAssets.Contains(clickGuid)) { var indexA = _viewAssetsGUID.IndexOf(_lastClickAssetGUID); var indexB = _viewAssetsGUID.IndexOf(clickGuid); var start = indexA; var end = indexB; if (indexA > indexB) { start = indexB; end = indexA; } var list = viewAssets.ToList(); _selectAssetsGUID.Clear(); for (int i = start; i <= end; i++) { var guid = list[i]; _selectAssetsGUID.Add(guid); } } else { _selectAssetsGUID = new HashSet() { clickGuid }; } } break; default: { if (_selectAssetsGUID.Contains(clickGuid)) { _selectAssetsGUID.Clear(); } else { _selectAssetsGUID = new HashSet() { clickGuid }; } } break; } _lastClickAssetGUID = clickGuid; Event.current.Use(); } } if (Event.current.type == EventType.DragUpdated || Event.current.type == EventType.DragPerform) { _isDragIn = true; } if (Event.current.type == EventType.DragExited) { var mousePosition = Event.current.mousePosition; if (_select != null && _dropArea.Contains(mousePosition)) { //添加资源项 var refs = DragAndDrop.objectReferences; _select.AddAssets(refs); } _isDragIn = false; Event.current.Use(); } } private bool TryCheckAssetItemIsClick(Vector2 mousePos, out string guid) { guid = string.Empty; foreach (var kv in _assetRects) { if (kv.Value.Contains(mousePos)) { guid = kv.Key; return true; } } return false; } private void DrawEventInputGUI() { if (_select != null && _isDragIn && _dropArea.Contains(Event.current.mousePosition)) { DragAndDrop.visualMode = DragAndDropVisualMode.Link; DrawDragInTips(_dropArea); } } //选择上次 private void SelectLast() { var last = IniConfig.GetString(COLLECT_CHARACTER_WINDOW_SELECT_SETTING_NAME); if (!string.IsNullOrEmpty(last)) { var match = _settings.LastOrDefault(e => e.name == last); if (match != null) { _select = match; } } } //选择对象 private void Select(CollectCharactersSetting setting) { var last = _select; _select = setting; IniConfig.Set(COLLECT_CHARACTER_WINDOW_SELECT_SETTING_NAME, setting.name); if (last != null && last != setting) { _selectAssetsGUID.Clear(); } } private void RefreshSettings() { _settings = LoadSettings(); foreach (var setting in _settings) { setting.Reload(); } } private void DrawDragInTips(Rect rect) { var labelRect = rect; labelRect.height = 24; EditorGUI.DrawRect(labelRect, Styles.ColorSelected); EditorGUI.DrawRect(rect, Styles.ColorSelected * 0.5F); var tips = _select.RunMode == CollectCharactersSetting.Mode.Include ? "+添加包含项" : "+添加排除项"; GUI.Label(rect, new GUIContent(tips), Styles.BoldLabel); } //加载设置 private static List LoadSettings() { var result = new List(); var guids = AssetDatabase.FindAssets($"t:{typeof(CollectCharactersSetting).FullName}", new[] { "Assets" }); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var setting = AssetDatabase.LoadAssetAtPath(path); if (setting != null) { result.Add(setting); } } return result; } private CollectCharactersSetting CreateSetting() { var lastIndex = _settings.Select(e => { var name = e.name; if (name.StartsWith("配置_")) { var code = name.Replace("配置_", ""); if (int.TryParse(code, out var index)) { return index; } } return 0; }).OrderBy(e => e).LastOrDefault(); var next = lastIndex + 1; var path = $"Assets/Typhoon_Gen/FontTool/Editor/CollectCharactersSetting/配置_{next}.asset"; var setting = CreateInstance(); _Editor.CreateFileDirectoryIfNotExists(path); AssetDatabase.CreateAsset(setting, path); AssetDatabase.Refresh(); _Editor.Ping(setting); return setting; } public static Task ExecuteCollectCharacters(CollectCharactersSetting setting) { var mode = setting.RunMode; var paths = Array.Empty(); switch (mode) { case CollectCharactersSetting.Mode.Include: paths = GetPathsIncludeMode(setting); break; case CollectCharactersSetting.Mode.Exclude: paths = GetPathsExcludeMode(setting); break; } var outfile = $"Assets/Typhoon_Gen/FontTool/OutPut_CollectCharacter/{setting.name}_output.txt"; return CollectFilesCharacters(paths, outfile); } //获取排除模式文件清单 private static string[] GetPathsExcludeMode(CollectCharactersSetting setting) { //读取所有的匹配项 var dirInfo = new DirectoryInfo("Assets"); //筛选所有的匹配项文件 var allFiles = dirInfo.GetFiles("*", SearchOption.AllDirectories) .Where(e => _filterExtensions.Contains(Path.GetExtension(e.FullName))).Select(e => e.FullName); var all = new List(); all.AddRange(allFiles); //补充.meta all.AddRange(allFiles.Select(e => $"{e}.meta")); var matches = all.ToHashSet(); var paths = setting.Excludes.Where(e => !string.IsNullOrEmpty(e)) .Select(AssetDatabase.GUIDToAssetPath).Where(e => !string.IsNullOrEmpty(e)); //剔除排除项 foreach (var path in paths) { if (Directory.Exists(path)) { var dir = new DirectoryInfo(path); var files = dir.GetFiles("*", SearchOption.AllDirectories); foreach (var fileInfo in files) { matches.Remove(fileInfo.FullName); } } else { matches.Remove(Path.GetFullPath(path)); matches.Remove($"{Path.GetFullPath(path)}.meta"); } } return matches.ToArray(); } //获取包含模式文件清单 private static string[] GetPathsIncludeMode(CollectCharactersSetting setting) { //读取所有 var paths = setting.Includes.Where(e => !string.IsNullOrEmpty(e)) .Select(AssetDatabase.GUIDToAssetPath).Where(e => !string.IsNullOrEmpty(e)); var allFiles = new HashSet(); //分析路径,读取所有路径文件 foreach (var path in paths) { if (Directory.Exists(path)) { var dir = new DirectoryInfo(path); var files = dir.GetFiles("*", SearchOption.AllDirectories); foreach (var fileInfo in files) { allFiles.Add(fileInfo.FullName); } } else { allFiles.Add(Path.GetFullPath(path)); } } //剔除.meta ,剔除不匹配的后缀文件 var final = allFiles.ToList().Where(e => !e.EndsWith(".meta")).Where(e => { var extension = Path.GetExtension(e); return _filterExtensions.Contains(extension); }); //补充文件对应的.meta文件 var metaFiles = final.Select(e => $"{e}.meta"); var matches = final.ToList(); matches.AddRange(metaFiles); return matches.ToArray(); } //收集 private static void CollectCharacters(CollectCharactersSetting setting) { var mode = setting.RunMode; var outfile = $"Assets/Typhoon_Gen/FontTool/OutPut_CollectCharacter/{setting.name}_output.txt"; switch (mode) { case CollectCharactersSetting.Mode.Exclude: { var paths = GetPathsExcludeMode(setting); _Editor.ShowMessageBox($"有效匹配文件数(包含.mate):{paths.Length} 个,进行字符收集?\n生成路径:{outfile}", () => CollectFilesCharacters(paths, outfile)); } break; case CollectCharactersSetting.Mode.Include: { var paths = GetPathsIncludeMode(setting); _Editor.ShowMessageBox($"有效匹配文件数(包含.mate):{paths.Length} 个,进行字符收集?\n生成路径:{outfile}", () => CollectFilesCharacters(paths, outfile)); } break; } } //收集文件所有字符 public static async Task CollectFilesCharacters(string[] paths, string saveFile) { var result = new HashSet(); var total = paths.Length; var current = 0; var cacheTime = DateTime.Now; var cancel = false; foreach (var path in paths) { var sec = (DateTime.Now - cacheTime).TotalSeconds; if (sec > 0.1f) { cacheTime = DateTime.Now; await Task.Yield(); } current += 1; var process = (float)current / total; if (EditorUtility.DisplayCancelableProgressBar("字符收集中,请稍等...", $"剩余项:{total - current},当前:{Path.GetFileName(path)}", process)) { cancel = true; break; } var characters = ReadFileCharacters(path); foreach (var character in characters) { result.Add(character); } } Debug.Log($"发现字符:{result.Count}"); EditorUtility.ClearProgressBar(); if (!cancel) { var sb = new StringBuilder(); foreach (var s in result) { sb.Append(s); } _Editor.CreateFileDirectoryIfNotExists(saveFile); //生成文件 File.WriteAllText(saveFile, sb.ToString()); _Editor.Ping(saveFile); Debug.Log($"生成:{saveFile}"); AssetDatabase.Refresh(); } else { Debug.Log($"取消收集"); } } //读取单文件字符 private static HashSet ReadFileCharacters(string file) { var result = new HashSet(); if (File.Exists(file)) { var txt = File.ReadAllText(file); //提取字符串的单个字符 var enumerator = StringInfo.GetTextElementEnumerator(txt); while (enumerator.MoveNext()) { var character = enumerator.GetTextElement(); result.Add(character); } var pattern = @"\\u[0-9\w]*"; var matches = Regex.Matches(txt, pattern); foreach (Match e in matches) { var unicodeString = e.Value; var str = unicodeString.Replace("\\u", "0x"); var v = Convert.ToInt32(str, 16); try { result.Add(char.ConvertFromUtf32(v)); } catch (Exception _) { // ignored } } } return result; } } }