using System; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.U2D.Animation; namespace UnityEditor.U2D.Animation.SpriteLibraryEditor { internal enum ActionType { SelectCategory, SelectLabels, RenameCategory, RenameLabel, CreateCategory, CreateLabel, DeleteCategories, DeleteLabels, ReorderCategories, ReorderLabels, ModifiedCategories, ModifiedLabels, SetMainLibrary, SetLabelSprite, None } internal enum ViewType { List, Grid } internal struct ViewData { /// /// View type (List / Grid). /// public ViewType viewType; /// /// Relative size for selected View Type in range 0-1. /// public float relativeSize; /// /// Absolute size based on the slider position in range 0-1. /// public float absoluteSize; } internal enum SearchType { CategoryAndLabel, Category, Label } internal class WindowController { ISpriteLibraryEditorWindow m_Window; SpriteLibraryEditorModel m_Model; List selectedCategories => m_Model.GetSelectedCategories(); List selectedLabels => m_Model.GetSelectedLabels(); public SpriteLibraryAsset GetSelectedAsset() => m_Model.selectedAsset; ControllerEvents m_ControllerEvents; ViewEvents m_ViewEvents; bool hasSelectedLibrary => m_Model.selectedAsset != null; const float k_ViewSizeDivision = 0.1f; float m_ViewSize; ViewType m_ViewType = ViewType.List; string m_FilterString = ""; SearchType m_FilterType = SearchType.CategoryAndLabel; string m_SelectedAssetPath; const string k_DefaultLabelName = "New Label"; const string k_DefaultCategoryName = "New Category"; bool m_AutoSave; public WindowController(ISpriteLibraryEditorWindow window, SpriteLibraryEditorModel model, ControllerEvents controllerEvents, ViewEvents viewEvents) { m_Window = window; m_ControllerEvents = controllerEvents; m_ViewEvents = viewEvents; m_Model = model; m_SelectedAssetPath = AssetDatabase.GetAssetPath(SpriteLibrarySourceAssetImporter.GetAssetFromSelection()); AddAssetPostprocessorListeners(); AddViewEventListeners(); Selection.selectionChanged += SelectionChanged; Undo.undoRedoPerformed += PropagateLastAction; m_AutoSave = SpriteLibraryEditorWindow.Settings.autoSave; } public void Destroy() { RemoveAssetPostprocessorListeners(); Selection.selectionChanged -= SelectionChanged; Undo.undoRedoPerformed -= PropagateLastAction; if (m_Model != null) m_Model.Destroy(); } public void SaveChanges() { m_Model.SaveLibrary(m_SelectedAssetPath); m_ControllerEvents.onSelectedLibrary?.Invoke(m_Model.selectedAsset); } public void RevertChanges() { m_Model.SelectLabels(new List()); m_Model.SelectCategories(new List()); m_Model.SelectAsset(m_Model.selectedAsset); m_ControllerEvents.onSelectedLibrary?.Invoke(m_Model.selectedAsset); RefreshView(); RefreshSelection(); } public void SelectAsset(SpriteLibraryAsset asset, bool modifiedExternally = false) { if (!modifiedExternally) { if (asset == null || asset == m_Model.selectedAsset) return; if (m_Window.hasUnsavedChanges) m_Window.HandleUnsavedChanges(); } m_SelectedAssetPath = asset != null ? AssetDatabase.GetAssetPath(asset) : null; m_Model.SelectAsset(asset); m_ControllerEvents.onSelectedLibrary?.Invoke(asset); RefreshView(); RefreshSelection(); } void AddViewEventListeners() { m_ViewEvents.onCreateNewSpriteLibraryAsset.AddListener(CreateNewSpriteLibraryAsset); m_ViewEvents.onSave.AddListener(OnSave); m_ViewEvents.onRevert.AddListener(OnRevert); m_ViewEvents.onToggleAutoSave.AddListener(ToggleAutoSave); m_ViewEvents.onViewSizeUpdate.AddListener(ChangeViewSize); m_ViewEvents.onViewTypeUpdate.AddListener(ChangeViewType); m_ViewEvents.onSelectedFilter.AddListener(SelectedFilter); m_ViewEvents.onSelectedFilterType.AddListener(SelectedFilterType); m_ViewEvents.onSetMainAsset.AddListener(SetMainAsset); m_ViewEvents.onSelectCategories.AddListener(SelectCategories); m_ViewEvents.onSelectLabels.AddListener(SelectLabels); m_ViewEvents.onCreateNewCategory.AddListener(CreateNewCategory); m_ViewEvents.onRenameCategory.AddListener(RenameSelectedCategory); m_ViewEvents.onReorderCategories.AddListener(ReorderCategories); m_ViewEvents.onDeleteCategories.AddListener(DeleteSelectedCategories); m_ViewEvents.onCreateNewLabel.AddListener(CreateNewLabel); m_ViewEvents.onRenameLabel.AddListener(RenameSelectedLabel); m_ViewEvents.onReorderLabels.AddListener(ReorderLabels); m_ViewEvents.onDeleteLabels.AddListener(DeleteSelectedLabels); m_ViewEvents.onSetLabelSprite.AddListener(SetLabelSprite); m_ViewEvents.onAddDataToCategories.AddListener(AddDataToCategories); m_ViewEvents.onAddDataToLabels.AddListener(AddDataToLabels); m_ViewEvents.onRevertOverridenLabels.AddListener(RevertOverridenLabels); } void AddAssetPostprocessorListeners() { SpriteLibraryAssetPostprocessor.OnImported += OnAssetModified; SpriteLibraryAssetPostprocessor.OnDeleted += OnAssetModified; SpriteLibraryAssetPostprocessor.OnMovedAssetFromTo += OnAssetMoved; } void RemoveAssetPostprocessorListeners() { SpriteLibraryAssetPostprocessor.OnImported -= OnAssetModified; SpriteLibraryAssetPostprocessor.OnDeleted -= OnAssetModified; SpriteLibraryAssetPostprocessor.OnMovedAssetFromTo -= OnAssetMoved; } void CreateNewSpriteLibraryAsset(string newAssetPath) { if (string.IsNullOrEmpty(newAssetPath) || !string.Equals(Path.GetExtension(newAssetPath), SpriteLibrarySourceAsset.extension, StringComparison.OrdinalIgnoreCase)) return; // Make sure that the extension is exactly the same if (Path.GetExtension(newAssetPath) != SpriteLibrarySourceAsset.extension) newAssetPath = newAssetPath.Replace(Path.GetExtension(newAssetPath), SpriteLibrarySourceAsset.extension); var assetToSave = ScriptableObject.CreateInstance(); SpriteLibrarySourceAssetImporter.SaveSpriteLibrarySourceAsset(assetToSave, newAssetPath); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); var newAsset = AssetDatabase.LoadAssetAtPath(newAssetPath); Selection.objects = new UnityEngine.Object[] { newAsset }; } void ChangeViewSize(float newSize) { m_ViewSize = newSize; if (m_ViewSize > k_ViewSizeDivision && m_ViewType == ViewType.List) m_ViewType = ViewType.Grid; if (m_ViewSize < k_ViewSizeDivision && m_ViewType == ViewType.Grid) m_ViewType = ViewType.List; m_ControllerEvents.onViewChanged?.Invoke(new ViewData { viewType = m_ViewType, relativeSize = GetAdjustedViewSize(m_ViewSize, m_ViewType), absoluteSize = m_ViewSize }); } void ChangeViewType(ViewType viewType) { if (m_ViewType == viewType) return; m_ViewType = viewType; m_ViewSize = m_ViewType == ViewType.List ? 0.0f : k_ViewSizeDivision; m_ControllerEvents.onViewChanged?.Invoke(new ViewData { viewType = m_ViewType, relativeSize = GetAdjustedViewSize(m_ViewSize, m_ViewType), absoluteSize = m_ViewSize }); } static float GetAdjustedViewSize(float size, ViewType viewType) { if (viewType == ViewType.List) return size / k_ViewSizeDivision; return (size - k_ViewSizeDivision) / (1 - k_ViewSizeDivision); } void SelectedFilterType(SearchType newFilterType) { if (m_FilterType == newFilterType) return; m_FilterType = newFilterType; RefreshView(); } void SelectedFilter(string newFilterString) { if (string.Equals(m_FilterString, newFilterString, StringComparison.OrdinalIgnoreCase)) return; m_FilterString = newFilterString; RefreshView(); } void OnAssetModified(string modifiedAssetPath) { var isModifiedExternally = !(m_SelectedAssetPath != modifiedAssetPath || m_Model.isSaving); if (!isModifiedExternally) return; SelectAsset(AssetDatabase.LoadAssetAtPath(m_SelectedAssetPath), true); } void OnAssetMoved(string sourcePath, string destinationPath) { if (sourcePath == m_SelectedAssetPath) { if (string.IsNullOrEmpty(destinationPath)) { SelectAsset(null, true); } else { m_Model.SetAssetPath(destinationPath); m_SelectedAssetPath = destinationPath; } } } void SelectionChanged() { SelectAsset(SpriteLibrarySourceAssetImporter.GetAssetFromSelection()); } void RefreshView() { var areCategoriesFiltered = m_FilterType is SearchType.CategoryAndLabel or SearchType.Category && !string.IsNullOrEmpty(m_FilterString); var filteredCategories = m_Model.GetFilteredCategories(m_FilterString, m_FilterType); m_ControllerEvents.onModifiedCategories?.Invoke(filteredCategories, areCategoriesFiltered); var areLabelsFiltered = m_FilterType is SearchType.CategoryAndLabel or SearchType.Label && !string.IsNullOrEmpty(m_FilterString); var filteredLabels = m_Model.GetFilteredLabels(m_FilterString, m_FilterType); m_ControllerEvents.onModifiedLabels?.Invoke(filteredLabels, areLabelsFiltered); m_ControllerEvents.onLibraryDataChanged?.Invoke(m_Model.isModified); } void RefreshSelection() { m_ControllerEvents.onSelectedCategories?.Invoke(selectedCategories); m_ControllerEvents.onSelectedLabels?.Invoke(selectedLabels); } void PropagateLastAction() { if (SpriteLibraryEditorModel.IsActionModifyingAssets(m_Model.lastActionType)) AutoSave(); if (m_Model.lastActionType == ActionType.SetMainLibrary) m_ControllerEvents.onMainLibraryChanged?.Invoke(m_Model.GetMainLibrary()); RefreshView(); RefreshSelection(); m_Model.lastActionType = ActionType.None; } void SetMainAsset(SpriteLibraryAsset libraryAsset) { if (!hasSelectedLibrary) return; var validAsset = true; if (libraryAsset != null) { if (libraryAsset == m_Model.selectedAsset || SpriteLibrarySourceAssetImporter.GetAssetParentChain(libraryAsset).Contains(GetSelectedAsset())) { Debug.LogWarning(TextContent.spriteLibraryCircularDependency); validAsset = false; } } if (validAsset) { m_Model.BeginUndo(ActionType.SetMainLibrary, TextContent.spriteLibrarySetMainLibrary); m_Model.SetMainLibrary(libraryAsset); m_Model.SelectCategories(new List()); m_Model.SelectLabels(new List()); m_Model.EndUndo(); AutoSave(); m_ControllerEvents.onMainLibraryChanged?.Invoke(libraryAsset); RefreshView(); RefreshSelection(); } else { m_ControllerEvents.onMainLibraryChanged?.Invoke(m_Model.GetMainLibrary()); } } void SelectCategories(IList newSelection) { if (!hasSelectedLibrary) return; newSelection ??= new List(); if (Equals(m_Model.GetSelectedCategories(), newSelection)) return; m_Model.BeginUndo(ActionType.SelectCategory, TextContent.spriteLibrarySelectCategories); m_Model.SelectCategories(newSelection); m_Model.SelectLabels(new List()); m_Model.EndUndo(); RefreshView(); RefreshSelection(); } void SelectLabels(IList newSelection) { newSelection ??= new List(); if (AreSequencesEqual(m_Model.GetSelectedLabels(), newSelection)) return; m_Model.BeginUndo(ActionType.SelectLabels, TextContent.spriteLibrarySelectLabels); m_Model.SelectLabels(newSelection); m_Model.EndUndo(); RefreshSelection(); } void CreateNewCategory(string categoryName = null, IList sprites = null) { if (!hasSelectedLibrary) return; if (string.IsNullOrEmpty(categoryName)) categoryName = k_DefaultCategoryName; m_Model.BeginUndo(ActionType.CreateCategory, TextContent.spriteLibraryCreateCategory); m_Model.CreateNewCategory(categoryName, sprites); m_Model.SelectCategories(new List { m_Model.GetAllCategories()[^1].name }); m_Model.SelectLabels(new List()); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void RenameSelectedCategory(string newName) { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories) return; newName = newName?.Trim(); if (string.IsNullOrEmpty(newName)) return; var categoryData = GetCategoryData(selectedCategories[0]); if (categoryData == null || categoryData.fromMain) return; m_Model.BeginUndo(ActionType.RenameCategory, TextContent.spriteLibraryRenameCategory); m_Model.RenameSelectedCategory(newName); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void ReorderCategories(IList reorderedCategories) { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories) return; if (reorderedCategories == null || reorderedCategories.Count == 0) return; var categoriesToReorder = selectedCategories; m_Model.BeginUndo(ActionType.ReorderCategories, TextContent.spriteLibraryReorderCategories); m_Model.ReorderCategories(reorderedCategories); m_Model.SelectCategories(categoriesToReorder); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void DeleteSelectedCategories() { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories) return; var validCategories = false; foreach (var category in selectedCategories) { var categoryData = GetCategoryData(category); if (categoryData != null && !categoryData.fromMain) { validCategories = true; break; } } if (!validCategories) return; m_Model.BeginUndo(ActionType.DeleteCategories, TextContent.spriteLibraryDeleteCategories); m_Model.DeleteSelectedCategories(); m_Model.SelectCategories(new List()); m_Model.SelectLabels(new List()); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void CreateNewLabel(string labelName = null) { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories) return; if (string.IsNullOrEmpty(labelName)) labelName = k_DefaultLabelName; m_Model.BeginUndo(ActionType.CreateLabel, TextContent.spriteLibraryCreateLabel); m_Model.CreateNewLabel(labelName); m_Model.SelectLabels(new List { m_Model.GetAllLabels()[^1].name }); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void RenameSelectedLabel(string newName) { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories || !m_Model.hasSelectedLabels) return; var labelData = GetLabelData(selectedLabels[0]); if (labelData == null || labelData.fromMain) return; newName = newName?.Trim(); if (newName == string.Empty) return; m_Model.BeginUndo(ActionType.RenameLabel, TextContent.spriteLibraryRenameLabel); m_Model.RenameSelectedLabel(newName); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void ReorderLabels(IList reorderedLabels) { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories) return; if (reorderedLabels == null || reorderedLabels.Count == 0) return; var labelsToReorder = selectedLabels; m_Model.BeginUndo(ActionType.ReorderLabels, TextContent.spriteLibraryReorderLabels); m_Model.ReorderLabels(reorderedLabels); m_Model.SelectLabels(labelsToReorder); m_Model.EndUndo(); AutoSave(); RefreshView(); } void DeleteSelectedLabels() { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories || !m_Model.hasSelectedLabels) return; var canAnyLabelBeDeleted = false; foreach (var label in selectedLabels) { if (!GetLabelData(label).fromMain) { canAnyLabelBeDeleted = true; break; } } if (!canAnyLabelBeDeleted) return; m_Model.BeginUndo(ActionType.DeleteLabels, TextContent.spriteLibraryDeleteLabels); m_Model.DeleteSelectedLabels(); m_Model.SelectLabels(new List()); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void SetLabelSprite(string labelName, Sprite newSprite) { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories) return; var labelData = GetLabelData(labelName); if (labelData == null || labelData.sprite == newSprite) return; m_Model.BeginUndo(ActionType.SetLabelSprite, TextContent.spriteLibrarySetLabelSprite); m_Model.SetLabelSprite(labelName, newSprite); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void ToggleAutoSave(bool newAutoSaveValue) { m_AutoSave = newAutoSaveValue; if (m_AutoSave) OnSave(); } void OnSave() { m_Window.SaveChanges(); } void OnRevert() { if (m_Window.hasUnsavedChanges) m_Window.HandleRevertChanges(); } void AutoSave() { if (m_AutoSave) m_Window.SaveChanges(); } void AddDataToCategories(IList spritesData, bool alt, string category) { if (!hasSelectedLibrary) return; if (spritesData == null || spritesData.Count == 0) return; m_Model.BeginUndo(ActionType.ModifiedCategories, TextContent.spriteLibraryAddDataToCategories); foreach (var data in spritesData) { var isDroppedIntoEmptyArea = string.IsNullOrEmpty(category); if (isDroppedIntoEmptyArea) { if (data.spriteSourceType == SpriteSourceType.Psb) HandlePsdData(data); else if (data.spriteSourceType == SpriteSourceType.Sprite) m_Model.CreateNewCategory(data.name, data.sprites); } else m_Model.AddLabelsToCategory(category, data.sprites, true); } m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void AddDataToLabels(IList spritesData, bool alt, string label) { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories) return; var sprites = new List(); foreach (var data in spritesData) sprites.AddRange(data.sprites); if (sprites.Count == 0) return; m_Model.BeginUndo(ActionType.ModifiedLabels, TextContent.spriteLibraryAddDataToLabels); if (!string.IsNullOrEmpty(label)) m_Model.SetLabelSprite(label, sprites[0]); else // empty area m_Model.AddLabelsToCategory(selectedCategories[0], sprites, false); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } void HandlePsdData(DragAndDropData data) { var psdFilePath = AssetDatabase.GetAssetPath(data.sprites[0]); var characterObj = AssetDatabase.LoadAssetAtPath(psdFilePath); var categoryDictionary = new Dictionary>(); var objectList = new Queue(); objectList.Enqueue(characterObj.transform); while (objectList.Count > 0) { var goTransform = objectList.Dequeue(); var spriteList = new List(); for (var i = 0; i < goTransform.childCount; i++) { var childTransform = goTransform.GetChild(i); var spriteRenderer = childTransform.GetComponent(); if (spriteRenderer != null) spriteList.Add(spriteRenderer.sprite); else objectList.Enqueue(childTransform); } if (spriteList.Count > 0) { if (goTransform == characterObj.transform) { foreach (var sprite in spriteList) categoryDictionary[sprite.name] = new List { sprite }; } else { categoryDictionary[goTransform.name] = spriteList; } } } foreach (var (categoryName, sprites) in categoryDictionary) { var addedToCategory = false; foreach (var cat in m_Model.GetAllCategories()) { if (cat.name == categoryName) { m_Model.AddLabelsToCategory(categoryName, sprites, true); addedToCategory = true; break; } } if (!addedToCategory) m_Model.CreateNewCategory(categoryName, sprites); } } void RevertOverridenLabels(IList labels) { if (!hasSelectedLibrary || !m_Model.hasSelectedCategories) return; var canRevertChanges = false; foreach (var label in labels) { if (string.IsNullOrEmpty(label)) continue; var data = GetLabelData(label); if (data != null && (data.spriteOverride || data.categoryFromMain && !data.fromMain)) { canRevertChanges = true; break; } } if (!canRevertChanges) return; m_Model.BeginUndo(ActionType.SetLabelSprite, TextContent.spriteLibrarySetLabelSprite); m_Model.RevertLabels(labels); m_Model.SelectLabels(new List()); m_Model.EndUndo(); AutoSave(); RefreshView(); RefreshSelection(); } CategoryData GetCategoryData(string categoryName) { if (string.IsNullOrEmpty(categoryName)) return null; foreach (var category in m_Model.GetAllCategories()) { if (category.name == categoryName) return category; } return null; } LabelData GetLabelData(string labelName) { if (string.IsNullOrEmpty(labelName)) return null; foreach (var label in m_Model.GetAllLabels()) { if (label.name == labelName) return label; } return null; } static bool AreSequencesEqual(IList first, IList second) { if (first == null || second == null || first.Count != second.Count) return false; for (var i = 0; i < first.Count; i++) { if (first[i] != second[i]) return false; } return true; } } }