using System; using System.Linq; using UnityEditor; using UnityEngine; namespace AibisDream.FrameAnimation.Editor { internal sealed class FrameAnimationTextInputWindow : EditorWindow { private string label; private string value; private string warning; private Func validate; private Action confirm; private Vector2 scroll; public static void Show( string title, string label, string initialValue, string warning, Func validate, Action confirm) { var window = CreateInstance(); window.titleContent = new GUIContent(title); window.label = label; window.value = initialValue ?? string.Empty; window.warning = warning ?? string.Empty; window.validate = validate; window.confirm = confirm; window.minSize = new Vector2(460f, 220f); window.maxSize = new Vector2(700f, 420f); window.ShowUtility(); } private void OnGUI() { scroll = EditorGUILayout.BeginScrollView(scroll); EditorGUILayout.LabelField(label, EditorStyles.boldLabel); GUI.SetNextControlName("FrameAnimationTextInput"); value = EditorGUILayout.TextField(value); if (!string.IsNullOrEmpty(warning)) { EditorGUILayout.HelpBox(warning, MessageType.Warning); } var error = validate?.Invoke(value) ?? string.Empty; if (!string.IsNullOrEmpty(error)) { EditorGUILayout.HelpBox(error, MessageType.Error); } GUILayout.FlexibleSpace(); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("取消", GUILayout.Width(90f))) { Close(); } using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error))) { if (GUILayout.Button("确认", GUILayout.Width(90f))) { var callback = confirm; var result = value; Close(); callback?.Invoke(result); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndScrollView(); } private void OnEnable() { EditorApplication.delayCall += FocusInput; } private void FocusInput() { if (this != null) { Focus(); EditorGUI.FocusTextInControl("FrameAnimationTextInput"); } } } internal sealed class FrameAnimationManualClipWindow : EditorWindow { private FrameAnimationGraph graph; private FrameClip source; private string id = "NewClip"; private string displayName = "New Clip"; private bool external; private Action completed; public static void Show( FrameAnimationGraph graph, FrameClip source, Action completed) { var window = CreateInstance(); window.titleContent = new GUIContent(source == null ? "Create Manual Clip" : "Copy As Manual Clip"); window.graph = graph; window.source = source; window.completed = completed; var baseId = source != null ? source.Id + "_Manual" : "NewClip"; window.id = MakeUniqueId(graph, baseId); window.displayName = source != null ? source.DisplayName + " Manual" : window.id; window.minSize = new Vector2(460f, 260f); window.maxSize = new Vector2(700f, 420f); window.ShowUtility(); } private void OnGUI() { EditorGUILayout.LabelField(source == null ? "创建 Manual Clip" : "复制为 Manual Clip", EditorStyles.boldLabel); id = EditorGUILayout.TextField("ID", id); displayName = EditorGUILayout.TextField("Display Name", displayName); external = EditorGUILayout.ToggleLeft("保存为外部独立 .asset(默认保存为 Graph sub-asset)", external); var error = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id) ? string.Empty : "ID 为空或与当前 Graph 的 Clip / Flow 冲突。"; if (!string.IsNullOrEmpty(error)) { EditorGUILayout.HelpBox(error, MessageType.Error); } if (source != null) { EditorGUILayout.HelpBox( "帧表将深拷贝,但 Sprite 仍复用原资源;ImportSource 关联会被清除。", MessageType.Info); } GUILayout.FlexibleSpace(); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("取消", GUILayout.Width(90f))) { Close(); } using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(error))) { if (GUILayout.Button("创建", GUILayout.Width(90f))) { Create(); } } EditorGUILayout.EndHorizontal(); } private void Create() { var path = string.Empty; if (external) { path = EditorUtility.SaveFilePanelInProject( "保存外部 Manual Clip", id, "asset", "请选择 Assets 下的保存位置。"); if (string.IsNullOrEmpty(path)) { return; } } var succeeded = source == null ? FrameAnimationAssetOperations.CreateManualClip(graph, id, displayName, path, out var result, out var error) : FrameAnimationAssetOperations.CopyToManual(graph, source, id, displayName, path, out result, out error); if (!succeeded) { EditorUtility.DisplayDialog("操作失败", error, "确定"); return; } var callback = completed; Close(); callback?.Invoke(result); } private static string MakeUniqueId(FrameAnimationGraph graph, string baseId) { var candidate = baseId; var suffix = 2; while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate)) { candidate = baseId + suffix++; } return candidate; } } internal sealed class FrameAnimationExistingClipWindow : EditorWindow { private FrameAnimationGraph graph; private FrameClip clip; private Action completed; public static void Show(FrameAnimationGraph graph, Action completed) { var window = CreateInstance(); window.titleContent = new GUIContent("Add Existing Manual Clip"); window.graph = graph; window.completed = completed; window.minSize = new Vector2(460f, 180f); window.maxSize = new Vector2(640f, 260f); window.ShowUtility(); } private void OnGUI() { EditorGUILayout.LabelField("添加外部 Manual Clip", EditorStyles.boldLabel); clip = (FrameClip)EditorGUILayout.ObjectField("FrameClip", clip, typeof(FrameClip), false); GUILayout.FlexibleSpace(); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("取消", GUILayout.Width(90f))) { Close(); } using (new EditorGUI.DisabledScope(clip == null)) { if (GUILayout.Button("添加", GUILayout.Width(90f))) { if (!FrameAnimationAssetOperations.AddExistingManualClip(graph, clip, out var error)) { EditorUtility.DisplayDialog("添加失败", error, "确定"); return; } var callback = completed; var result = clip; Close(); callback?.Invoke(result); } } EditorGUILayout.EndHorizontal(); } } internal sealed class FrameAnimationFlowWindow : EditorWindow { private FrameAnimationGraph graph; private AnimationNode entry; private string id; private string displayName; private Action completed; internal static void Show( FrameAnimationGraph graph, AnimationNode entry, Action completed) { var window = CreateInstance(); window.titleContent = new GUIContent("Create Animation Flow"); window.graph = graph; window.entry = entry; window.id = FrameAnimationGraphMutationService.MakeUniqueFlowId(graph, entry); window.displayName = window.id; window.completed = completed; window.minSize = new Vector2(460f, 230f); window.maxSize = new Vector2(700f, 360f); window.ShowUtility(); } private void OnGUI() { EditorGUILayout.LabelField("从选中节点创建 Flow", EditorStyles.boldLabel); using (new EditorGUI.DisabledScope(true)) { EditorGUILayout.TextField("Entry Node", entry?.DisplayName ?? string.Empty); } id = EditorGUILayout.TextField("ID", id); displayName = EditorGUILayout.TextField("Display Name", displayName); var idError = FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id) ? string.Empty : "ID 为空或与当前 Graph 中的 Clip / Flow 冲突。"; var owner = graph?.Flows.FirstOrDefault(flow => flow != null && flow.EntryNodeId == entry?.InternalId); if (owner != null) { idError = $"该节点已是 Flow '{owner.Id}' 的入口。"; } if (!string.IsNullOrEmpty(idError)) { EditorGUILayout.HelpBox(idError, MessageType.Error); } GUILayout.FlexibleSpace(); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("取消", GUILayout.Width(90f))) Close(); using (new EditorGUI.DisabledScope(!string.IsNullOrEmpty(idError))) { if (GUILayout.Button("创建", GUILayout.Width(90f))) { if (!FrameAnimationGraphMutationService.CreateFlow( graph, entry, id, displayName, out var flow, out var error)) { EditorUtility.DisplayDialog("创建 Flow 失败", error, "确定"); return; } var callback = completed; Close(); callback?.Invoke(flow); } } EditorGUILayout.EndHorizontal(); } } }