diff --git a/Editor/Art_Tools/ArtResourceConfigEditor.cs b/Editor/Art_Tools/ArtResourceConfigEditor.cs index 8d6490fd..fbfcd8a3 100644 --- a/Editor/Art_Tools/ArtResourceConfigEditor.cs +++ b/Editor/Art_Tools/ArtResourceConfigEditor.cs @@ -22,6 +22,7 @@ namespace EditorArt_Tools private const string SO_ROOT_PATH = "Assets/Art_SubModule/Art_SO"; private const string JSON_ROOT_PATH = "Assets/Art_SubModule/Art_Json"; private const string BYTES_ROOT_PATH = "Assets/Art_SubModule/Art_Bytes"; + private const string FOLDER_FOLDOUT_PREFS_KEY_PREFIX = "ArtResourceConfigEditor.FolderFoldout."; // ===== 数据 ===== private List allTables = new List(); @@ -84,6 +85,7 @@ namespace EditorArt_Tools private void OnEnable() { + LoadFolderFoldoutStates(); RefreshTableList(); InitializeSpinePreview(); @@ -290,16 +292,23 @@ namespace EditorArt_Tools string folderName = folderGroup.Key; if (!folderFoldouts.ContainsKey(folderName)) { - folderFoldouts[folderName] = true; + folderFoldouts[folderName] = GetFolderFoldoutState(folderName); } // 绘制文件夹 EditorGUILayout.BeginHorizontal(); - folderFoldouts[folderName] = EditorGUILayout.Foldout( - folderFoldouts[folderName], + bool previousFoldoutState = folderFoldouts[folderName]; + bool newFoldoutState = EditorGUILayout.Foldout( + previousFoldoutState, $"📁 {folderName} ({folderGroup.Count()})", true, EditorStyles.foldoutHeader); + + if (newFoldoutState != previousFoldoutState) + { + folderFoldouts[folderName] = newFoldoutState; + SaveFolderFoldoutState(folderName, newFoldoutState); + } EditorGUILayout.EndHorizontal(); // 绘制该文件夹下的配置表 @@ -355,6 +364,26 @@ namespace EditorArt_Tools return "根目录"; } + private void LoadFolderFoldoutStates() + { + folderFoldouts.Clear(); + } + + private bool GetFolderFoldoutState(string folderName) + { + return EditorPrefs.GetBool(GetFolderFoldoutPrefsKey(folderName), false); + } + + private void SaveFolderFoldoutState(string folderName, bool isExpanded) + { + EditorPrefs.SetBool(GetFolderFoldoutPrefsKey(folderName), isExpanded); + } + + private string GetFolderFoldoutPrefsKey(string folderName) + { + return FOLDER_FOLDOUT_PREFS_KEY_PREFIX + folderName; + } + #endregion #region 第二栏:Item导航 @@ -377,10 +406,14 @@ namespace EditorArt_Tools { AddNewItem(); } - if (GUILayout.Button("📁 批量导入Sprite", GUILayout.Height(25))) + if (GUILayout.Button("📁 按文件夹导入", GUILayout.Height(25))) { BatchImportSprites(); } + if (GUILayout.Button("🖼 批量导入图片", GUILayout.Height(25))) + { + BatchImportSpritesByFiles(); + } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); @@ -885,12 +918,25 @@ namespace EditorArt_Tools // 如果有未保存的更改,提示用户 if (hasUnsavedChanges && selectedTable != null && selectedTable != table) { - if (!EditorUtility.DisplayDialog("切换配置表", - "当前有未保存的更改,切换将丢失这些更改。\n确定要切换吗?", - "切换", "取消")) + // 0=保存并切换, 1=取消, 2=不保存直接切换 + int result = EditorUtility.DisplayDialogComplex("未保存的更改", + $"当前配置表「{selectedTable.TableName}」有未保存的更改。", + "保存并切换", "取消", "不保存,直接切换"); + + if (result == 0) { + // 保存当前表后再切换 + SaveCurrentTable(); + // 如果保存失败(仍有未保存标记),中止切换 + if (hasUnsavedChanges) + return; + } + else if (result == 1) + { + // 取消切换 return; } + // result == 2: 不保存,继续切换 } selectedTable = table; @@ -1240,6 +1286,102 @@ namespace EditorArt_Tools } } + private void BatchImportSpritesByFiles() + { + if (selectedTable == null) + { + EditorUtility.DisplayDialog("错误", "请先选择一个配置表", "确定"); + return; + } + + BatchImportByDragWindow.Show(this); + } + + /// + /// 通过文件路径列表导入Sprite(供拖拽导入窗口调用) + /// + public void ImportSpritesByAssetPaths(string[] assetPaths) + { + if (selectedTable == null || assetPaths == null || assetPaths.Length == 0) + return; + + var existingSpritePaths = new HashSet( + tempItemsList + .Select(GetSpriteReferencePath) + .Where(path => !string.IsNullOrEmpty(path))); + + var spritesToImport = new List(); + var spritePathsToImport = new List(); + int skippedExistingCount = 0; + int notSpriteCount = 0; + + foreach (string assetPath in assetPaths) + { + if (existingSpritePaths.Contains(assetPath)) + { + skippedExistingCount++; + continue; + } + + // 尝试加载为Sprite + Sprite sprite = AssetDatabase.LoadAssetAtPath(assetPath); + if (sprite == null) + { + notSpriteCount++; + continue; + } + + spritesToImport.Add(sprite); + spritePathsToImport.Add(assetPath); + existingSpritePaths.Add(assetPath); + } + + if (spritesToImport.Count == 0) + { + string msg = "没有可导入的Sprite资源。"; + if (notSpriteCount > 0) + msg += $"\n{notSpriteCount} 个文件不是Sprite类型(请检查图片导入设置)。"; + if (skippedExistingCount > 0) + msg += $"\n{skippedExistingCount} 个已存在于当前表格中,已跳过。"; + EditorUtility.DisplayDialog("提示", msg, "确定"); + return; + } + + int startId = tempItemsList.Count > 0 ? tempItemsList.Max(i => i.Id) + 1 : 1; + int importCount = 0; + + for (int i = 0; i < spritesToImport.Count; i++) + { + Sprite sprite = spritesToImport[i]; + string assetPath = spritePathsToImport[i]; + + var newItem = new ArtItemData + { + Id = startId + importCount, + Name = sprite.name, + Desc = "", + Sprite = sprite, + SpritePath = assetPath + }; + + tempItemsList.Add(newItem); + importCount++; + } + + if (importCount > 0) + { + hasUnsavedChanges = true; + string msg = $"成功导入 {importCount} 个Sprite资源"; + if (skippedExistingCount > 0) + msg += $"\n跳过已存在引用 {skippedExistingCount} 个"; + if (notSpriteCount > 0) + msg += $"\n跳过非Sprite文件 {notSpriteCount} 个"; + msg += "\n记得点击保存按钮!"; + EditorUtility.DisplayDialog("导入成功", msg, "确定"); + Repaint(); + } + } + private void SaveCurrentTable() { if (selectedTable == null) @@ -1248,6 +1390,21 @@ namespace EditorArt_Tools return; } + selectedTable.TableName = (selectedTable.TableName ?? string.Empty).Trim(); + if (string.IsNullOrEmpty(selectedTable.TableName)) + { + EditorUtility.DisplayDialog("保存失败", "表格名称不能为空。", "确定"); + return; + } + + if (selectedTable.TableName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) + { + EditorUtility.DisplayDialog("保存失败", "表格名称包含非法文件名字符,请修改后重试。", "确定"); + return; + } + + string originalSoPath = AssetDatabase.GetAssetPath(selectedTable); + // 检查表格ID和名称的唯一性 foreach (var table in allTables) { @@ -1323,6 +1480,13 @@ namespace EditorArt_Tools selectedTable.Items.Add(newItem); } + selectedTable.name = selectedTable.TableName; + + if (!TrySyncTableAssetNames(selectedTable, originalSoPath)) + { + return; + } + // 保存SO EditorUtility.SetDirty(selectedTable); AssetDatabase.SaveAssets(); @@ -1341,10 +1505,53 @@ namespace EditorArt_Tools hasUnsavedChanges = false; + var savedTable = selectedTable; + RefreshTableList(); + SelectTable(savedTable); + Debug.Log($"配置表保存成功: {selectedTable.TableName}"); EditorUtility.DisplayDialog("保存成功", $"配置表「{selectedTable.TableName}」已保存\n共 {selectedTable.Items.Count} 项", "确定"); } + private bool TrySyncTableAssetNames(ArtTableSO table, string originalSoPath) + { + if (table == null || string.IsNullOrEmpty(originalSoPath)) + { + return false; + } + + string originalAssetName = Path.GetFileNameWithoutExtension(originalSoPath); + if (string.Equals(originalAssetName, table.TableName, System.StringComparison.Ordinal)) + { + return true; + } + + string originalJsonPath = GetJsonPathFromSoPath(originalSoPath); + string renameError = AssetDatabase.RenameAsset(originalSoPath, table.TableName); + if (!string.IsNullOrEmpty(renameError)) + { + EditorUtility.DisplayDialog("保存失败", + $"重命名配置表文件失败:{renameError}", + "确定"); + return false; + } + + string newSoPath = AssetDatabase.GetAssetPath(table); + string newJsonPath = GetJsonPathFromSoPath(newSoPath); + if (!string.Equals(originalJsonPath, newJsonPath, System.StringComparison.OrdinalIgnoreCase) + && AssetDatabase.LoadAssetAtPath(originalJsonPath) != null) + { + AssetDatabase.DeleteAsset(originalJsonPath); + } + + return true; + } + + private string GetJsonPathFromSoPath(string soPath) + { + return soPath.Replace(SO_ROOT_PATH, JSON_ROOT_PATH).Replace(".asset", ".json"); + } + #endregion #region JSON同步 @@ -2302,4 +2509,158 @@ namespace EditorArt_Tools #endregion } + + /// + /// 批量导入图片的拖拽窗口 + /// 支持从Project窗口拖拽多个png图片进来 + /// + public class BatchImportByDragWindow : EditorWindow + { + private ArtResourceConfigEditor parentEditor; + private List draggedObjects = new List(); + private Vector2 scrollPos; + + public static void Show(ArtResourceConfigEditor editor) + { + var window = GetWindow("批量导入图片"); + window.parentEditor = editor; + window.draggedObjects.Clear(); + window.minSize = new Vector2(400, 500); + window.ShowUtility(); + } + + private void OnGUI() + { + EditorGUILayout.LabelField("批量导入图片", EditorStyles.boldLabel); + EditorGUILayout.HelpBox( + "从 Project 窗口中将 png 图片拖拽到下方区域,\n支持多选拖入。只有 Sprite 类型的图片会被导入。", + MessageType.Info); + + EditorGUILayout.Space(5); + + // 拖拽区域 + Rect dropArea = GUILayoutUtility.GetRect(0, 100, GUILayout.ExpandWidth(true)); + GUI.Box(dropArea, "🖼 将图片拖拽到这里", new GUIStyle(GUI.skin.box) + { + alignment = TextAnchor.MiddleCenter, + fontSize = 16, + fontStyle = FontStyle.Bold + }); + + HandleDragAndDrop(dropArea); + + EditorGUILayout.Space(5); + + // 已添加的列表 + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"已选择: {draggedObjects.Count} 个文件", EditorStyles.boldLabel); + if (draggedObjects.Count > 0) + { + if (GUILayout.Button("清空", GUILayout.Width(60))) + { + draggedObjects.Clear(); + } + } + EditorGUILayout.EndHorizontal(); + + scrollPos = EditorGUILayout.BeginScrollView(scrollPos); + for (int i = draggedObjects.Count - 1; i >= 0; i--) + { + EditorGUILayout.BeginHorizontal("box"); + EditorGUILayout.ObjectField(draggedObjects[i], typeof(Texture2D), false); + if (GUILayout.Button("✕", GUILayout.Width(25))) + { + draggedObjects.RemoveAt(i); + } + EditorGUILayout.EndHorizontal(); + } + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(10); + + // 导入按钮 + EditorGUI.BeginDisabledGroup(draggedObjects.Count == 0 || parentEditor == null); + GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f); + if (GUILayout.Button($"确认导入 ({draggedObjects.Count} 个)", GUILayout.Height(35))) + { + DoImport(); + } + GUI.backgroundColor = Color.white; + EditorGUI.EndDisabledGroup(); + } + + private void HandleDragAndDrop(Rect dropArea) + { + Event evt = Event.current; + switch (evt.type) + { + case EventType.DragUpdated: + case EventType.DragPerform: + if (!dropArea.Contains(evt.mousePosition)) + return; + + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + + if (evt.type == EventType.DragPerform) + { + DragAndDrop.AcceptDrag(); + + var existingPaths = new HashSet( + draggedObjects.Select(o => AssetDatabase.GetAssetPath(o))); + + foreach (var obj in DragAndDrop.objectReferences) + { + string path = AssetDatabase.GetAssetPath(obj); + if (string.IsNullOrEmpty(path)) + continue; + + // 如果拖入的是文件夹,递归查找png + if (AssetDatabase.IsValidFolder(path)) + { + string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { path }); + foreach (string guid in guids) + { + string texPath = AssetDatabase.GUIDToAssetPath(guid); + if (!texPath.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase)) + continue; + if (existingPaths.Contains(texPath)) + continue; + + var texObj = AssetDatabase.LoadAssetAtPath(texPath); + if (texObj != null) + { + draggedObjects.Add(texObj); + existingPaths.Add(texPath); + } + } + } + else + { + // 只接受png文件 + if (!path.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase)) + continue; + if (existingPaths.Contains(path)) + continue; + + draggedObjects.Add(obj); + existingPaths.Add(path); + } + } + } + evt.Use(); + break; + } + } + + private void DoImport() + { + var assetPaths = draggedObjects + .Select(o => AssetDatabase.GetAssetPath(o)) + .Where(p => !string.IsNullOrEmpty(p)) + .ToArray(); + + parentEditor.ImportSpritesByAssetPaths(assetPaths); + Close(); + } + } } diff --git a/GameMain/UI/UISprites/Area/merge_pic_s33.png b/GameMain/UI/UISprites/Area/merge_pic_s33.png index 1243f480..472177ec 100644 Binary files a/GameMain/UI/UISprites/Area/merge_pic_s33.png and b/GameMain/UI/UISprites/Area/merge_pic_s33.png differ diff --git a/GameMain/UI/UISprites/Area/merge_pic_s37 .png b/GameMain/UI/UISprites/Area/merge_pic_s37 .png deleted file mode 100644 index d275a2e8..00000000 Binary files a/GameMain/UI/UISprites/Area/merge_pic_s37 .png and /dev/null differ diff --git a/GameMain/UI/UISprites/Area/merge_pic_s37 .png.meta b/GameMain/UI/UISprites/Area/merge_pic_s37 .png.meta deleted file mode 100644 index 83db236c..00000000 --- a/GameMain/UI/UISprites/Area/merge_pic_s37 .png.meta +++ /dev/null @@ -1,140 +0,0 @@ -fileFormatVersion: 2 -guid: 538da807bccdbfe4a927761b4bc37384 -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 13 - mipmaps: - mipMapMode: 0 - enableMipMap: 0 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - flipGreenChannel: 0 - isReadable: 0 - streamingMipmaps: 0 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMipmapLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 1 - mipBias: 0 - wrapU: 1 - wrapV: 1 - wrapW: 0 - nPOTScale: 0 - lightmap: 0 - compressionQuality: 50 - spriteMode: 1 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 1 - spriteTessellationDetail: -1 - textureType: 8 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - swizzle: 50462976 - cookieLightType: 0 - platformSettings: - - serializedVersion: 3 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 3 - buildTarget: Standalone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 3 - buildTarget: Android - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: 52 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 1 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 3 - buildTarget: iPhone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - physicsShape: [] - bones: [] - spriteID: 5e97eb03825dee720800000000000000 - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - nameFileIdTable: {} - mipmapLimitGroupName: - pSDRemoveMatte: 0 - userData: - assetBundleName: - assetBundleVariant: