// Copyright (c) 2025 EFramework Innovation. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using FairyGUI;
using FairyGUIEditor;
using EFramework.Unity.Utility;
using EFramework.Unity.Editor;
namespace EFramework.Unity.FairyGUI.Editor
{
///
/// UIManifestEditor 是 UIManifest 组件的编辑器工具,提供了自定义图标、创建、导入和依赖管理功能。
///
///
///
/// 功能特性
/// - 为 UIManifest 预制体在项目窗口中显示自定义图标
/// - 提供 UIManifest 资源的创建与导入功能
/// - 自动处理 UIManifest 资源的依赖关系
/// - 支持从 FairyGUI 导出的文档中创建 UIManifest
///
/// 更多信息请参考模块文档。
///
public class UIManifestEditor
{
///
/// icon 是 UIManifest 组件的自定义图标,用于优化资源的显示。
///
internal static Texture2D icon;
///
/// manifest 维护了项目中所有的 UIManifest 实例。
///
internal static List manifests;
///
/// CachingFile 是项目中 UIManifest 资源的路径索引缓存。
///
internal const string CachingFile = "Library/UIManifest.db";
///
/// watchers 维护了对所有 UIManifest 目录的监听。
///
internal static Dictionary watchers = new();
///
/// skips 维护了 UIManifest 资源导入的忽略列表,避免循环导入。
///
internal static Dictionary skips = new();
///
/// OnProjectWindowItemOnGUI 在项目窗口绘制回调,为 UIManifest 预制体添加自定义图标。
///
/// 资源的 GUID
/// 项目窗口中的绘制区域
internal static void OnProjectWindowItemOnGUI(string guid, Rect selectionRect)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (path.EndsWith("prefab") && AssetDatabase.LoadAssetAtPath(path))
{
Rect iconRect = new Rect(selectionRect.x + selectionRect.width - 20, selectionRect.y, 16, 16);
GUI.DrawTexture(iconRect, icon);
}
}
///
/// Collect 查找并缓存项目中所有 UIManifest 资源,用于依赖关系处理。
///
/// 要添加到缓存列表中的 UIManifest 路径
/// 是否从索引缓存中查找 UIManifest 列表
internal static void Collect(string path = null, bool cache = true)
{
var changed = false;
if (manifests == null)
{
var startTime = XTime.GetMillisecond();
manifests = new List();
var succeeded = false;
if (cache && XFile.HasFile(CachingFile))
{
succeeded = true;
try
{
var lines = File.ReadAllLines(CachingFile);
foreach (var asset in lines)
{
if (string.IsNullOrEmpty(asset)) continue;
if (!XFile.HasFile(asset))
{
succeeded = false;
break;
}
var mani = AssetDatabase.LoadAssetAtPath(asset);
if (mani == null)
{
succeeded = false;
break;
}
if (mani && !manifests.Contains(asset))
{
manifests.Add(asset);
Watch(mani.RawPath, asset);
}
}
}
catch (System.Exception e)
{
XLog.Panic(e, $"Read caching file failed: {CachingFile}");
succeeded = false;
}
}
if (!succeeded)
{
manifests.Clear(); // 清理并全量查找
var assets = AssetDatabase.FindAssets("t:Prefab"); // 在OnInit中无法通过AssetDatabase.FindAssets查询
for (var i = 0; i < assets.Length; i++)
{
var asset = XFile.NormalizePath(AssetDatabase.GUIDToAssetPath(assets[i]));
var mani = AssetDatabase.LoadAssetAtPath(asset);
if (mani && !manifests.Contains(asset))
{
manifests.Add(asset);
Watch(mani.RawPath, asset);
}
}
changed = true;
}
if (manifests.Count > 0) XLog.Debug("UIManifestEditor.Collect: found {0} manifest(s) in {1}, elapsed {2}ms.", manifests.Count, succeeded ? "library" : "assets", XTime.GetMillisecond() - startTime);
}
if (!string.IsNullOrEmpty(path) && !manifests.Contains(path))
{
var mani = AssetDatabase.LoadAssetAtPath(path);
if (mani)
{
manifests.Add(path);
changed = true;
}
}
if (changed)
{
try
{
File.WriteAllLines(CachingFile, manifests);
XLog.Debug("UIManifestEditor.Collect: wrote index caching into {1}.", Path.GetFullPath(CachingFile), CachingFile);
}
catch (System.Exception e) { XLog.Panic(e, $"Write caching file failed: {CachingFile}"); }
}
}
///
/// Watch 监控 UIManifest 原始路径是否变更,若发生变更,则调用 Import 重新导入之。
///
///
///
internal static void Watch(string rawPath, string assetPath)
{
if (string.IsNullOrEmpty(rawPath) || string.IsNullOrEmpty(assetPath)) return;
rawPath = XFile.NormalizePath(rawPath);
if (!XFile.HasDirectory(rawPath)) return;
if (!watchers.ContainsKey(rawPath))
{
var watcher = new FileSystemWatcher(rawPath)
{
IncludeSubdirectories = false,
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size,
};
var maniName = Path.GetFileNameWithoutExtension(assetPath);
var maniPrefix = maniName + "_";
var maniFile = maniPrefix + "fui.bytes";
var dirty = new List();
var expected = new List();
void proc(string path)
{
if (path.Contains(maniPrefix))
{
dirty.Add(Path.GetFileName(path));
if (path.Contains(maniFile))
{
// 卸载原有的 Package,避免加载失败
if (UIPackage.GetByName(maniName) != null) UIPackage.RemovePackage(maniName);
var pkg = UIPackage.AddPackage(XFile.OpenFile(path), maniName, null);
if (pkg != null)
{
UIPackage.RemovePackage(maniName);
expected.Clear();
expected.Add(maniFile);
var itemsById = typeof(UIPackage).GetField("_itemsById", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(pkg) as Dictionary;
foreach (var kvp in itemsById)
{
var file = kvp.Value.file;
if (!string.IsNullOrEmpty(file) && !expected.Contains(file))
{
expected.Add(file);
}
}
// // manifest 文件的索引不为 1,则认定为非正常的导入流程,可能是外部的操作引起文件变更,如:Git 更新等
// var index = dirty.IndexOf(maniFile);
// if (index > 0 && index != 1)
// {
// // 重置监听范围,这种方式不一定可靠,依赖 FairyGUI Editor 的生成顺序:manifest 文件的索引为 1
// // 否则可能导致监听失效:外部变更,但是 dirty 未清除
// dirty = dirty.GetRange(index - 1, dirty.Count - (index - 1));
// XLog.Notice("invalid watch");
// }
}
}
// 等待所有依赖都被重新发布完成
if (expected.Count > 0 && expected.All(e => dirty.Contains(e)))
{
// 重置监控数据
dirty.Clear();
expected.Clear();
// 有概率出现 Unity Editor 资源导入失败:Could not create asset from Assets/xxxx: File could not be read 或出现导入两次的情况
// 原因1:FairyGUI Editor 未完全 Flush 文件,解决方案:延迟处理导入,XLoom.SetTimeout(() => Import(assetPath), 2000);
// 原因2:外部的操作(Git 更新)引起文件变更导致 dirty 监控不正确,引起提前导入,解决方案:用户重新发布/导入即可
Import(assetPath);
}
}
}
watcher.Created += (sender, args) => XLoom.RunInMain(() => proc(args.FullPath));
watcher.Changed += (sender, args) => XLoom.RunInMain(() => proc(args.FullPath));
watchers.Add(rawPath, watcher);
}
}
///
/// Create 创建 UIManifest 的菜单项处理方法。
///
[MenuItem("Assets/Create/FairyGUI/UI Manifest")]
internal static void Create()
{
if (Selection.assetGUIDs.Length == 0) XLog.Error("UIManifestEditor.Create: non selection.");
else
{
var path = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]);
if (string.IsNullOrEmpty(path)) XLog.Error("UIManifestEditor.Create: selection path is empty.");
else
{
var rawPath = EditorUtility.SaveFolderPanel("Select Raw Path", XEnv.ProjectPath, null);
if (string.IsNullOrEmpty(rawPath)) XLog.Warn("UIManifestEditor.Create: raw path is empty.");
else
{
rawPath = XFile.NormalizePath(Path.GetRelativePath(XEnv.ProjectPath, rawPath));
if (!XFile.HasDirectory(rawPath)) XLog.Error("UIManifestEditor.Create: raw path doesn't exist: {0}", rawPath);
else
{
var fs = Directory.GetFiles(rawPath);
if (!Directory.GetFiles(rawPath).Any(file => Path.GetFileName(file).EndsWith("_fui.bytes")))
{
XLog.Error("UIManifestEditor.Create: mani desc like xxx_fui.bytes was not found: {0}", rawPath);
}
else
{
var maniPath = AssetDatabase.CreateFolder(path, Path.GetFileName(rawPath));
if (string.IsNullOrEmpty(maniPath)) XLog.Error("UIManifestEditor.Create: create manifest folder error: {0}", maniPath);
else
{
maniPath = AssetDatabase.GUIDToAssetPath(maniPath);
var asset = Create(XFile.PathJoin(maniPath, Path.GetFileName(maniPath) + ".prefab"), rawPath);
if (asset) Selection.activeObject = asset;
}
}
}
}
}
}
}
///
/// Create 创建 UIManifest 预制体资源。
///
/// UIManifest 预制体的保存路径
/// FairyGUI 导出素材的路径
/// 创建的 UIManifest 资源对象
public static Object Create(string maniPath, string rawPath)
{
var go = new GameObject();
go.AddComponent().RawPath = rawPath;
var asset = PrefabUtility.SaveAsPrefabAsset(go, maniPath);
Object.DestroyImmediate(go);
AssetDatabase.Refresh();
Watch(rawPath, maniPath);
return asset;
}
///
/// Import 导入 UIManifest 资源,处理其依赖关系。
///
/// UIManifest 资源路径
/// 已访问的 UIManifest 路径列表,用于检测循环依赖
/// 导入是否成功
public static bool Import(string path, List visited = null)
{
path = XFile.NormalizePath(path);
var mani = AssetDatabase.LoadAssetAtPath(path);
if (mani == null)
{
XLog.Error("UIManifestEditor.Import: null manifest at: {0}", path);
return false;
}
if (!XFile.HasDirectory(mani.RawPath))
{
XLog.Error("UIManifestEditor.Import: raw path doesn't exist: {0}", mani.RawPath);
return false;
}
Collect(path: path);
visited ??= new List();
if (visited.Contains(path)) return true;
visited.Add(path);
var maniPath = Path.GetDirectoryName(path);
var maniName = Path.GetFileNameWithoutExtension(path);
var ofiles = Directory.GetFiles(maniPath);
foreach (var of in ofiles)
{
var nm = Path.GetFileNameWithoutExtension(of);
var ext = Path.GetExtension(of);
if (!nm.StartsWith(maniName + "_") || ext == ".meta") continue;
XFile.DeleteFile(of);
}
XFile.CopyDirectory(Path.GetFullPath(mani.RawPath), maniPath, ".meta");
AssetDatabase.Refresh();
var nfiles = Directory.GetFiles(maniPath);
var oldPackageName = mani.PackageName;
var oldPackagePath = mani.PackagePath;
var oldDependency = mani.Dependency;
mani.PackageName = Path.GetFileNameWithoutExtension(path);
mani.PackagePath = path.Replace(".prefab", "");
mani.Dependency = new List