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 || !m_Model.hasSelectedLabels)
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;
}
}
}