1352 lines
46 KiB
C#
1352 lines
46 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.IMGUI.Controls;
|
|
using UnityEngine;
|
|
|
|
namespace FMODUnity
|
|
{
|
|
public class FileReorganizer : EditorWindow, ISerializationCallbackReceiver
|
|
{
|
|
public const string ReorganizerMenuItemPath = "FMOD/Reorganize Plugin Files";
|
|
|
|
[SerializeField]
|
|
private List<Task> tasks = new List<Task>();
|
|
|
|
[SerializeField]
|
|
private int taskCount;
|
|
|
|
[SerializeField]
|
|
private int currentTask;
|
|
|
|
private TaskView taskView;
|
|
|
|
[SerializeField]
|
|
private TreeViewState taskViewState = new TreeViewState();
|
|
|
|
[SerializeField]
|
|
private MultiColumnHeaderState taskHeaderState;
|
|
|
|
[SerializeField]
|
|
private bool reloadingFromSerializedState = false;
|
|
|
|
[NonSerialized]
|
|
private GUIContent statusContent = GUIContent.none;
|
|
|
|
private IEnumerator<string> processingState;
|
|
|
|
[MenuItem(ReorganizerMenuItemPath)]
|
|
public static void ShowWindow()
|
|
{
|
|
FileReorganizer reorganizer = GetWindow<FileReorganizer>("FMOD File Reorganizer");
|
|
reorganizer.minSize = new Vector2(850, 600);
|
|
|
|
reorganizer.PopulateTasks();
|
|
|
|
reorganizer.Show();
|
|
}
|
|
|
|
[Serializable]
|
|
private class Task
|
|
{
|
|
public int step = int.MaxValue;
|
|
|
|
private Task()
|
|
{
|
|
}
|
|
|
|
public static Task Move(string source, string destination, Platform platform)
|
|
{
|
|
return new Task() {
|
|
type = Type.Move,
|
|
status = Status.Pending,
|
|
platform = platform,
|
|
source = source,
|
|
destination = destination,
|
|
statusText = string.Format("{0} will be moved to\n{1}", source, destination),
|
|
};
|
|
}
|
|
|
|
public static Task RemoveFolder(string path)
|
|
{
|
|
return new Task() {
|
|
type = Type.RemoveFolder,
|
|
status = Status.Pending,
|
|
source = path,
|
|
statusText = string.Format("{0} will be removed if it is empty", path),
|
|
};
|
|
}
|
|
|
|
public static Task Missing(string path, Platform platform)
|
|
{
|
|
return new Task() {
|
|
type = Type.Missing,
|
|
status = Status.Missing,
|
|
platform = platform,
|
|
source = path,
|
|
statusText = string.Format(
|
|
"{0} is missing.\nYou may need to reinstall the {1} support package from {2}.",
|
|
path, platform.DisplayName, EditorSettings.DownloadURL),
|
|
};
|
|
}
|
|
|
|
public static Task RemoveAsset(string path, Platform platform)
|
|
{
|
|
return new Task() {
|
|
type = Type.RemoveAsset,
|
|
status = Status.Pending,
|
|
platform = platform,
|
|
source = path,
|
|
statusText = string.Format("{0} will be removed", path),
|
|
};
|
|
}
|
|
|
|
public Platform platform { get; private set; }
|
|
public string source { get; private set; }
|
|
public string destination { get; private set; }
|
|
|
|
public enum Status
|
|
{
|
|
Pending,
|
|
Succeeded,
|
|
Failed,
|
|
Missing,
|
|
}
|
|
|
|
public Status status { get; private set; }
|
|
public string statusText { get; private set; }
|
|
|
|
public void SetSucceeded(string message)
|
|
{
|
|
status = Status.Succeeded;
|
|
statusText = message;
|
|
}
|
|
|
|
public void SetFailed(string message)
|
|
{
|
|
status = Status.Failed;
|
|
statusText = message;
|
|
}
|
|
|
|
public enum Type
|
|
{
|
|
Move,
|
|
RemoveFolder,
|
|
RemoveAsset,
|
|
Missing,
|
|
}
|
|
|
|
public Type type { get; private set; }
|
|
|
|
public string platformName { get { return (platform != null) ? platform.DisplayName : string.Empty; } }
|
|
}
|
|
|
|
public void OnBeforeSerialize()
|
|
{
|
|
taskViewState = taskView.state;
|
|
taskHeaderState = taskView.multiColumnHeader.state;
|
|
}
|
|
|
|
public void OnAfterDeserialize()
|
|
{
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
{
|
|
MultiColumnHeaderState newHeaderState = TaskView.CreateHeaderState();
|
|
|
|
if (MultiColumnHeaderState.CanOverwriteSerializedFields(taskHeaderState, newHeaderState))
|
|
{
|
|
MultiColumnHeaderState.OverwriteSerializedFields(taskHeaderState, newHeaderState);
|
|
}
|
|
|
|
taskHeaderState = newHeaderState;
|
|
}
|
|
|
|
MultiColumnHeader taskHeader = new MultiColumnHeader(taskHeaderState);
|
|
|
|
taskView = new TaskView(taskViewState, taskHeader, tasks);
|
|
taskView.taskSelected += OnTaskSelected;
|
|
|
|
taskView.Reload();
|
|
|
|
if (reloadingFromSerializedState)
|
|
{
|
|
taskView.SortRows();
|
|
}
|
|
else
|
|
{
|
|
taskHeader.ResizeToFit();
|
|
taskHeader.SetSorting((int)TaskView.Column.Step, true);
|
|
}
|
|
|
|
reloadingFromSerializedState = true;
|
|
|
|
EditorApplication.update += ProcessNextTask;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
EditorApplication.update -= ProcessNextTask;
|
|
|
|
StopProcessing();
|
|
}
|
|
|
|
private void PopulateTasks()
|
|
{
|
|
tasks.Clear();
|
|
|
|
TaskGenerator.Generate(tasks);
|
|
|
|
SetTaskSequence();
|
|
UpdateTaskCount();
|
|
SetDefaultStatus();
|
|
|
|
taskView.Reload();
|
|
taskView.SortRows();
|
|
}
|
|
|
|
public static bool IsUpToDate()
|
|
{
|
|
List<Task> tasks = new List<Task>();
|
|
|
|
TaskGenerator.Generate(tasks);
|
|
|
|
return !tasks.Any(t => t.type != Task.Type.Missing);
|
|
}
|
|
|
|
private void SetDefaultStatus()
|
|
{
|
|
int missingCount = tasks.Count(t => t.type == Task.Type.Missing);
|
|
|
|
if (missingCount > 0)
|
|
{
|
|
string message;
|
|
|
|
if (missingCount == 1)
|
|
{
|
|
message = "There is a file missing. Select it above for more information.";
|
|
}
|
|
else
|
|
{
|
|
message = string.Format(
|
|
"There are {0} files missing. Select them above for more information.", missingCount);
|
|
}
|
|
|
|
statusContent = new GUIContent(message, Resources.StatusIcon[Task.Status.Missing]);
|
|
}
|
|
else
|
|
{
|
|
statusContent = GUIContent.none;
|
|
}
|
|
}
|
|
|
|
private void SetTaskSequence()
|
|
{
|
|
int step = 1;
|
|
|
|
foreach (Task task in tasks.Where(t => t.type == Task.Type.Move))
|
|
{
|
|
task.step = step;
|
|
++step;
|
|
}
|
|
|
|
foreach (Task task in tasks.Where(t => t.type == Task.Type.RemoveAsset))
|
|
{
|
|
task.step = step;
|
|
++step;
|
|
}
|
|
|
|
// Sort folder tasks in reverse path order, so subfolders are processed before their parents
|
|
foreach (Task task in tasks.Where(t => t.type == Task.Type.RemoveFolder).OrderByDescending(t => t.source))
|
|
{
|
|
task.step = step;
|
|
++step;
|
|
}
|
|
|
|
tasks.Sort((a, b) => a.step.CompareTo(b.step));
|
|
}
|
|
|
|
private void UpdateTaskCount()
|
|
{
|
|
taskCount = tasks.Count(t => t.status == Task.Status.Pending);
|
|
}
|
|
|
|
private class TaskView : TreeView
|
|
{
|
|
private List<Task> tasks;
|
|
|
|
public delegate void TaskSelectedHandler(Task task);
|
|
|
|
public event TaskSelectedHandler taskSelected;
|
|
|
|
public TaskView(TreeViewState state, MultiColumnHeader header, List<Task> tasks)
|
|
: base(state, header)
|
|
{
|
|
this.tasks = tasks;
|
|
|
|
showAlternatingRowBackgrounds = true;
|
|
|
|
header.sortingChanged += SortRows;
|
|
}
|
|
|
|
public static MultiColumnHeaderState CreateHeaderState()
|
|
{
|
|
MultiColumnHeaderState.Column[] columns = new MultiColumnHeaderState.Column[] {
|
|
new MultiColumnHeaderState.Column()
|
|
{
|
|
headerContent = new GUIContent("Task #"),
|
|
width = 50,
|
|
autoResize = false,
|
|
allowToggleVisibility = false,
|
|
},
|
|
new MultiColumnHeaderState.Column()
|
|
{
|
|
headerContent = new GUIContent("Status"),
|
|
width = 100,
|
|
autoResize = false,
|
|
allowToggleVisibility = false,
|
|
},
|
|
new MultiColumnHeaderState.Column() {
|
|
headerContent = new GUIContent("Platform"),
|
|
width = 150,
|
|
autoResize = false,
|
|
allowToggleVisibility = false,
|
|
},
|
|
new MultiColumnHeaderState.Column()
|
|
{
|
|
headerContent = new GUIContent("Description"),
|
|
minWidth = 500,
|
|
allowToggleVisibility = false,
|
|
},
|
|
};
|
|
|
|
return new MultiColumnHeaderState(columns);
|
|
}
|
|
|
|
public enum Column
|
|
{
|
|
Step,
|
|
Status,
|
|
Platform,
|
|
Description,
|
|
}
|
|
|
|
private class TaskItem : TreeViewItem
|
|
{
|
|
public Task task;
|
|
}
|
|
|
|
protected override TreeViewItem BuildRoot()
|
|
{
|
|
TreeViewItem root = new TreeViewItem(-1, -1);
|
|
|
|
if (tasks.Count > 0)
|
|
{
|
|
int index = 0;
|
|
|
|
foreach (Task task in tasks)
|
|
{
|
|
TreeViewItem taskItem = new TaskItem() {
|
|
id = index++,
|
|
task = task,
|
|
};
|
|
|
|
root.AddChild(taskItem);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TreeViewItem item = new TreeViewItem(0);
|
|
item.displayName = "Nothing to do here.";
|
|
|
|
root.AddChild(item);
|
|
}
|
|
|
|
SetupDepthsFromParentsAndChildren(root);
|
|
|
|
return root;
|
|
}
|
|
|
|
protected override bool CanMultiSelect(TreeViewItem item)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
protected override void SelectionChanged(IList<int> selectedIds)
|
|
{
|
|
base.SelectionChanged(selectedIds);
|
|
|
|
if (taskSelected != null)
|
|
{
|
|
if (selectedIds.Count > 0)
|
|
{
|
|
TaskItem item = FindItem(selectedIds[0], rootItem) as TaskItem;
|
|
|
|
if (item != null)
|
|
{
|
|
taskSelected(item.task);
|
|
return;
|
|
}
|
|
}
|
|
|
|
taskSelected(null);
|
|
}
|
|
}
|
|
|
|
public void SortRows()
|
|
{
|
|
SortRows(multiColumnHeader);
|
|
}
|
|
|
|
private void SortRows(MultiColumnHeader header)
|
|
{
|
|
IList<TreeViewItem> rows = GetRows();
|
|
int[] sortedColumns = header.state.sortedColumns;
|
|
|
|
if (sortedColumns.Length > 0 && rows.Count > 1)
|
|
{
|
|
int firstColumn = sortedColumns[0];
|
|
|
|
IOrderedEnumerable<TreeViewItem> query =
|
|
InitialQuery(rows, (Column)firstColumn, header.IsSortedAscending(firstColumn));
|
|
|
|
for (int i = 1; i < sortedColumns.Length; ++i)
|
|
{
|
|
query = SubQuery(query, sortedColumns[i], header.IsSortedAscending(sortedColumns[i]));
|
|
}
|
|
|
|
// We need to execute the query before clearing rows, otherwise it returns nothing
|
|
List<TreeViewItem> newRows = query.ToList();
|
|
|
|
rows.Clear();
|
|
|
|
foreach (TreeViewItem item in newRows)
|
|
{
|
|
rows.Add(item);
|
|
}
|
|
}
|
|
|
|
RefreshCustomRowHeights();
|
|
}
|
|
|
|
private IOrderedEnumerable<TreeViewItem> InitialQuery(IList<TreeViewItem> rows, Column column, bool ascending)
|
|
{
|
|
switch (column)
|
|
{
|
|
case Column.Step:
|
|
return Sort(rows, r => (r as TaskItem).task.step, ascending);
|
|
case Column.Status:
|
|
return Sort(rows, r => (r as TaskItem).task.status, ascending);
|
|
case Column.Platform:
|
|
return Sort(rows, r => (r as TaskItem).task.platformName, ascending);
|
|
case Column.Description:
|
|
return Sort(rows, r => (r as TaskItem).task.source, ascending);
|
|
default:
|
|
throw new ArgumentException("Unrecognised column: " + column);
|
|
}
|
|
}
|
|
|
|
private static IOrderedEnumerable<TreeViewItem> SubQuery(
|
|
IOrderedEnumerable<TreeViewItem> query, int column, bool ascending)
|
|
{
|
|
switch ((Column)column)
|
|
{
|
|
case Column.Step:
|
|
return SubSort(query, r => (r as TaskItem).task.step, ascending);
|
|
case Column.Status:
|
|
return SubSort(query, r => (r as TaskItem).task.status, ascending);
|
|
case Column.Platform:
|
|
return SubSort(query, r => (r as TaskItem).task.platformName, ascending);
|
|
case Column.Description:
|
|
return SubSort(query, r => (r as TaskItem).task.source, ascending);
|
|
default:
|
|
throw new ArgumentException("Unrecognised column: " + column);
|
|
}
|
|
}
|
|
|
|
protected override float GetCustomRowHeight(int row, TreeViewItem item)
|
|
{
|
|
if (item is TaskItem)
|
|
{
|
|
Task task = (item as TaskItem).task;
|
|
|
|
if (task.type == Task.Type.Move)
|
|
{
|
|
return EditorGUIUtility.singleLineHeight * 2;
|
|
}
|
|
else
|
|
{
|
|
return Resources.StatusHeight();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return base.GetCustomRowHeight(row, item);
|
|
}
|
|
}
|
|
|
|
protected override void RowGUI(RowGUIArgs args)
|
|
{
|
|
if (args.item is TaskItem)
|
|
{
|
|
TaskItem taskItem = args.item as TaskItem;
|
|
|
|
for (int i = 0; i < args.GetNumVisibleColumns(); ++i)
|
|
{
|
|
CellGUI(args.GetCellRect(i), taskItem.task, args.GetColumn(i));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
base.RowGUI(args);
|
|
}
|
|
}
|
|
|
|
private void CellGUI(Rect rect, Task task, int columnIndex)
|
|
{
|
|
switch ((Column)columnIndex)
|
|
{
|
|
case Column.Step:
|
|
if (task.step != int.MaxValue)
|
|
{
|
|
GUI.Label(rect, task.step.ToString(), Resources.StepStyle());
|
|
}
|
|
break;
|
|
case Column.Status:
|
|
GUI.Label(rect, Resources.StatusContent[task.status], Resources.StatusColumnStyle());
|
|
break;
|
|
case Column.Platform:
|
|
GUI.Label(rect, task.platformName, Resources.PlatformStyle());
|
|
break;
|
|
case Column.Description:
|
|
DrawDescription(rect, task);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void DrawDescription(Rect rect, Task task)
|
|
{
|
|
switch (task.type)
|
|
{
|
|
case Task.Type.Move:
|
|
DrawMoveDescription(rect, task);
|
|
break;
|
|
case Task.Type.RemoveFolder:
|
|
DrawRemoveFolderDescription(rect, task);
|
|
break;
|
|
case Task.Type.RemoveAsset:
|
|
DrawRemoveAssetDescription(rect, task);
|
|
break;
|
|
case Task.Type.Missing:
|
|
DrawMissingDescription(rect, task);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void DrawMoveDescription(Rect rect, Task task)
|
|
{
|
|
Rect sourcePrefixRect = new Rect(rect.x, rect.y, Resources.PrefixSize().x, Resources.PrefixSize().y);
|
|
|
|
Rect destinationPrefixRect = sourcePrefixRect;
|
|
destinationPrefixRect.y = sourcePrefixRect.yMax;
|
|
|
|
Rect sourceRect = sourcePrefixRect;
|
|
sourceRect.x = sourcePrefixRect.xMax;
|
|
sourceRect.xMax = rect.xMax;
|
|
|
|
Rect destinationRect = destinationPrefixRect;
|
|
destinationRect.x = destinationPrefixRect.xMax;
|
|
destinationRect.xMax = rect.xMax;
|
|
|
|
EditorGUI.BeginDisabledGroup(true);
|
|
|
|
GUI.Label(sourcePrefixRect, Resources.SourcePrefix, Resources.PrefixStyle());
|
|
GUI.Label(destinationPrefixRect, Resources.DestinationPrefix, Resources.PrefixStyle());
|
|
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
DrawAssetPath(sourceRect, task.source);
|
|
DrawAssetPath(destinationRect, task.destination);
|
|
}
|
|
|
|
private void DrawRemoveFolderDescription(Rect rect, Task task)
|
|
{
|
|
Rect prefixRect = new Rect(rect.x, rect.y, Resources.PrefixSize().x, Resources.PrefixSize().y);
|
|
|
|
Rect pathRect = prefixRect;
|
|
pathRect.x = prefixRect.xMax;
|
|
pathRect.width = Resources.AssetPathStyle().CalcSize(new GUIContent(task.source)).x;
|
|
|
|
Rect suffixRect = prefixRect;
|
|
suffixRect.x = pathRect.xMax;
|
|
suffixRect.xMax = rect.xMax;
|
|
|
|
EditorGUI.BeginDisabledGroup(true);
|
|
|
|
GUI.Label(prefixRect, Resources.RemovePrefix, Resources.PrefixStyle());
|
|
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
DrawAssetPath(pathRect, task.source);
|
|
|
|
EditorGUI.BeginDisabledGroup(true);
|
|
|
|
GUI.Label(suffixRect, "if empty", Resources.SuffixStyle());
|
|
|
|
EditorGUI.EndDisabledGroup();
|
|
}
|
|
|
|
private void DrawRemoveAssetDescription(Rect rect, Task task)
|
|
{
|
|
Rect prefixRect = new Rect(rect.x, rect.y, Resources.PrefixSize().x, Resources.PrefixSize().y);
|
|
|
|
Rect pathRect = prefixRect;
|
|
pathRect.x = prefixRect.xMax;
|
|
pathRect.width = Resources.AssetPathStyle().CalcSize(new GUIContent(task.source)).x;
|
|
|
|
EditorGUI.BeginDisabledGroup(true);
|
|
|
|
GUI.Label(prefixRect, Resources.RemovePrefix, Resources.PrefixStyle());
|
|
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
DrawAssetPath(pathRect, task.source);
|
|
}
|
|
|
|
private void DrawMissingDescription(Rect rect, Task task)
|
|
{
|
|
Rect sourceRect = rect;
|
|
sourceRect.xMin += Resources.PrefixSize().x;
|
|
|
|
DrawAssetPath(sourceRect, task.source);
|
|
}
|
|
}
|
|
|
|
private static IOrderedEnumerable<T1> Sort<T1, T2>(IEnumerable<T1> enumerable,
|
|
Func<T1, T2> keySelector, bool ascending)
|
|
{
|
|
if (ascending)
|
|
{
|
|
return enumerable.OrderBy(keySelector);
|
|
}
|
|
else
|
|
{
|
|
return enumerable.OrderByDescending(keySelector);
|
|
}
|
|
}
|
|
|
|
private static IOrderedEnumerable<T1> SubSort<T1, T2>(IOrderedEnumerable<T1> enumerable,
|
|
Func<T1, T2> keySelector, bool ascending)
|
|
{
|
|
if (ascending)
|
|
{
|
|
return enumerable.ThenBy(keySelector);
|
|
}
|
|
else
|
|
{
|
|
return enumerable.ThenByDescending(keySelector);
|
|
}
|
|
}
|
|
|
|
private class Resources
|
|
{
|
|
private static GUIStyle statusColumnStyle;
|
|
|
|
private static GUIStyle statusBarStyle;
|
|
|
|
private static float statusHeight;
|
|
|
|
private static GUIStyle stepStyle;
|
|
|
|
public static readonly GUIContent SourcePrefix = new GUIContent("Move");
|
|
public static readonly GUIContent DestinationPrefix = new GUIContent("to");
|
|
public static readonly GUIContent RemovePrefix = new GUIContent("Remove");
|
|
|
|
private static Vector2 prefixSize;
|
|
|
|
private static GUIStyle prefixStyle;
|
|
|
|
private static GUIStyle suffixStyle;
|
|
|
|
private static GUIStyle assetPathStyle;
|
|
|
|
private static bool cacheInitialized = false;
|
|
|
|
public static readonly Dictionary<Task.Status, Texture> StatusIcon =
|
|
new Dictionary<Task.Status, Texture>() {
|
|
{ Task.Status.Pending, EditorGUIUtility.FindTexture("TestNormal") },
|
|
{ Task.Status.Succeeded, EditorGUIUtility.FindTexture("TestPassed") },
|
|
{ Task.Status.Failed, EditorGUIUtility.FindTexture("TestFailed") },
|
|
{ Task.Status.Missing, EditorGUIUtility.FindTexture("console.warnicon.sml") },
|
|
};
|
|
|
|
public static readonly Dictionary<Task.Status, GUIContent> StatusContent =
|
|
new Dictionary<Task.Status, GUIContent>() {
|
|
{ Task.Status.Pending, new GUIContent("Pending", StatusIcon[Task.Status.Pending]) },
|
|
{ Task.Status.Succeeded, new GUIContent("Succeeded", StatusIcon[Task.Status.Succeeded]) },
|
|
{ Task.Status.Failed, new GUIContent("Failed", StatusIcon[Task.Status.Failed]) },
|
|
{ Task.Status.Missing, new GUIContent("Missing", StatusIcon[Task.Status.Missing]) },
|
|
};
|
|
|
|
public static GUIStyle StatusColumnStyle()
|
|
{
|
|
AffirmCache();
|
|
return statusColumnStyle;
|
|
}
|
|
|
|
public static GUIStyle StatusBarStyle()
|
|
{
|
|
AffirmCache();
|
|
return statusBarStyle;
|
|
}
|
|
|
|
public static GUIStyle PlatformStyle()
|
|
{
|
|
return StatusColumnStyle();
|
|
}
|
|
|
|
public static float StatusHeight()
|
|
{
|
|
if (statusHeight == 0)
|
|
{
|
|
foreach (var current in StatusIcon)
|
|
{
|
|
statusHeight = Math.Max(statusHeight, current.Value.height + 4);
|
|
}
|
|
}
|
|
|
|
return statusHeight;
|
|
}
|
|
|
|
public static GUIStyle StepStyle()
|
|
{
|
|
AffirmCache();
|
|
return stepStyle;
|
|
}
|
|
|
|
public static Vector2 PrefixSize()
|
|
{
|
|
AffirmCache();
|
|
return prefixSize;
|
|
}
|
|
|
|
public static GUIStyle PrefixStyle()
|
|
{
|
|
AffirmCache();
|
|
return prefixStyle;
|
|
}
|
|
|
|
public static GUIStyle SuffixStyle()
|
|
{
|
|
AffirmCache();
|
|
return suffixStyle;
|
|
}
|
|
|
|
public static GUIStyle AssetPathStyle()
|
|
{
|
|
AffirmCache();
|
|
return assetPathStyle;
|
|
}
|
|
|
|
private static void AffirmCache()
|
|
{
|
|
if (!cacheInitialized)
|
|
{
|
|
cacheInitialized = true;
|
|
|
|
statusColumnStyle = new GUIStyle(GUI.skin.label) {
|
|
alignment = TextAnchor.MiddleLeft,
|
|
};
|
|
|
|
statusBarStyle = new GUIStyle(GUI.skin.label) {
|
|
alignment = TextAnchor.UpperLeft,
|
|
wordWrap = true,
|
|
};
|
|
|
|
stepStyle = new GUIStyle(GUI.skin.label) {
|
|
alignment = TextAnchor.MiddleRight,
|
|
};
|
|
|
|
prefixStyle = new GUIStyle(GUI.skin.label) {
|
|
alignment = TextAnchor.MiddleRight,
|
|
};
|
|
|
|
suffixStyle = new GUIStyle(GUI.skin.label) {
|
|
alignment = TextAnchor.MiddleLeft,
|
|
};
|
|
|
|
assetPathStyle = new GUIStyle(GUI.skin.label);
|
|
|
|
prefixSize = prefixStyle.CalcSize(SourcePrefix);
|
|
prefixSize = Vector2.Max(prefixSize, prefixStyle.CalcSize(DestinationPrefix));
|
|
prefixSize = Vector2.Max(prefixSize, prefixStyle.CalcSize(RemovePrefix));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnTaskSelected(Task task)
|
|
{
|
|
if (task != null)
|
|
{
|
|
statusContent = new GUIContent(task.statusText, Resources.StatusIcon[task.status]);
|
|
}
|
|
else
|
|
{
|
|
SetDefaultStatus();
|
|
}
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
if (focusedWindow == this
|
|
&& Event.current.type == EventType.KeyDown
|
|
&& Event.current.keyCode == KeyCode.Escape)
|
|
{
|
|
Cancel();
|
|
Event.current.Use();
|
|
}
|
|
|
|
// Task list
|
|
GUILayout.BeginVertical(GUI.skin.box);
|
|
|
|
Rect treeViewRect = GUILayoutUtility.GetRect(0, 0, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
|
|
|
|
taskView.OnGUI(treeViewRect);
|
|
|
|
GUILayout.EndVertical();
|
|
|
|
// Status bar
|
|
GUILayout.BeginHorizontal(GUI.skin.box, GUILayout.Height(EditorGUIUtility.singleLineHeight * 2));
|
|
|
|
GUILayout.Label(statusContent.image, GUILayout.ExpandWidth(false));
|
|
EditorGUILayout.SelectableLabel(statusContent.text, Resources.StatusBarStyle());
|
|
|
|
GUILayout.EndHorizontal();
|
|
|
|
// Buttons
|
|
float buttonHeight = EditorGUIUtility.singleLineHeight * 2;
|
|
|
|
GUILayout.BeginHorizontal();
|
|
|
|
if (GUILayout.Button("Cancel", GUILayout.Height(buttonHeight)))
|
|
{
|
|
Cancel();
|
|
}
|
|
|
|
EditorGUI.BeginDisabledGroup(IsProcessing());
|
|
|
|
if (GUILayout.Button("Refresh", GUILayout.Height(buttonHeight)))
|
|
{
|
|
PopulateTasks();
|
|
}
|
|
|
|
EditorGUI.EndDisabledGroup();
|
|
|
|
if (IsProcessing())
|
|
{
|
|
EditorGUI.BeginDisabledGroup(true);
|
|
|
|
GUILayout.Button(string.Format("Processing Task {0} of {1}", currentTask, taskCount), GUILayout.Height(buttonHeight));
|
|
|
|
EditorGUI.EndDisabledGroup();
|
|
}
|
|
else
|
|
{
|
|
if (GUILayout.Button(string.Format("Process {0} Tasks", taskCount), GUILayout.Height(buttonHeight)))
|
|
{
|
|
StartProcessing();
|
|
}
|
|
}
|
|
|
|
GUILayout.EndHorizontal();
|
|
}
|
|
|
|
private void Cancel()
|
|
{
|
|
if (IsProcessing())
|
|
{
|
|
StopProcessing();
|
|
}
|
|
else
|
|
{
|
|
Close();
|
|
}
|
|
}
|
|
|
|
private static void DrawAssetPath(Rect rect, string path)
|
|
{
|
|
GUIStyle pathStyle = Resources.AssetPathStyle();
|
|
GUIContent pathContent = new GUIContent(path);
|
|
|
|
Rect pathRect = rect;
|
|
pathRect.width = pathStyle.CalcSize(pathContent).x;
|
|
|
|
GUI.Label(pathRect, pathContent, pathStyle);
|
|
EditorGUIUtility.AddCursorRect(pathRect, MouseCursor.Link);
|
|
|
|
if (Event.current.type == EventType.MouseDown
|
|
&& pathRect.Contains(Event.current.mousePosition))
|
|
{
|
|
SelectAssetOrParentFolder(path);
|
|
Event.current.Use();
|
|
}
|
|
}
|
|
|
|
private static void SelectAssetOrParentFolder(string path)
|
|
{
|
|
while (!AssetExists(path))
|
|
{
|
|
path = EditorUtils.GetParentFolder(path);
|
|
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
Selection.activeObject = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
|
|
}
|
|
|
|
private void OnInspectorUpdate()
|
|
{
|
|
ProcessNextTask();
|
|
}
|
|
|
|
private struct TaskGenerator
|
|
{
|
|
private const string AssetsFolder = "Assets";
|
|
private const string FMODRoot = "Assets/Plugins/FMOD";
|
|
private const string FMODSource = FMODRoot + "/src";
|
|
|
|
private static readonly string[] BaseFolders = {
|
|
FMODSource,
|
|
FMODRoot,
|
|
"Assets/Plugins",
|
|
"Assets",
|
|
};
|
|
|
|
private static readonly MoveRecord[] looseAssets = {
|
|
// Release 1.10 layout
|
|
new MoveRecord() { source = FMODRoot + "/fmodplugins.cpp", destination = "obsolete" },
|
|
new MoveRecord() { source = "Assets/GoogleVR", destination = "addons" },
|
|
new MoveRecord() { source = "Assets/ResonanceAudio", destination = "addons" },
|
|
new MoveRecord() { source = "Assets/Resources/FMODStudioSettings.asset", destination = "Resources" },
|
|
new MoveRecord() { source = "Assets/FMODStudioCache.asset", destination = "Resources" },
|
|
|
|
// Release 2.0 layout
|
|
new MoveRecord() { source = FMODRoot + "/src/Runtime/fmodplugins.cpp", destination = "obsolete" },
|
|
|
|
// Release 2.1 layout
|
|
new MoveRecord() { source = FMODRoot + "/src/Runtime/fmod_static_plugin_support.h", destination = "obsolete" },
|
|
|
|
// Release 2.2 layout
|
|
new MoveRecord() { source = FMODRoot + "/src/fmodplugins.cpp", destination = "obsolete" },
|
|
new MoveRecord() { source = FMODRoot + "/src/fmod_static_plugin_support.h", destination = "obsolete" },
|
|
};
|
|
|
|
private static readonly string[] foldersToCleanUp = {
|
|
"Assets/Plugins/FMOD/Runtime",
|
|
"Assets/Plugins/Editor",
|
|
};
|
|
|
|
private List<Task> tasks;
|
|
|
|
public static void Generate(List<Task> tasks)
|
|
{
|
|
TaskGenerator generator = new TaskGenerator() { tasks = tasks };
|
|
|
|
Settings.Instance.ForEachPlatform(generator.GenerateTasksForPlatform);
|
|
generator.GenerateTasksForLooseAssets();
|
|
generator.GenerateTasksForCodeFolders();
|
|
generator.GenerateTasksForLegacyCodeFiles();
|
|
generator.GenerateTasksForFolderCleanup();
|
|
}
|
|
|
|
private void GenerateTasksForPlatform(Platform platform)
|
|
{
|
|
IEnumerable<Platform.FileInfo> files = platform.GetSourceFileInfo().Cast<Platform.FileInfo>();
|
|
|
|
foreach (BuildTarget buildTarget in platform.GetBuildTargets())
|
|
{
|
|
files = files.Concat(platform.GetBinaryFileInfo(buildTarget, Platform.BinaryType.All).Cast<Platform.FileInfo>());
|
|
}
|
|
|
|
foreach (Platform.FileInfo info in files)
|
|
{
|
|
string newPath = string.Format("{0}/{1}", AssetsFolder, info.LatestLocation());
|
|
|
|
if (!AssetExists(newPath))
|
|
{
|
|
bool foundPath = false;
|
|
string oldPath = null;
|
|
|
|
foreach (string path in info.OldLocations())
|
|
{
|
|
oldPath = string.Format("{0}/{1}", AssetsFolder, path);
|
|
|
|
if (tasks.Any(t => t.source == oldPath))
|
|
{
|
|
foundPath = true;
|
|
break;
|
|
}
|
|
|
|
if (AssetExists(oldPath))
|
|
{
|
|
tasks.Add(Task.Move(oldPath, newPath, platform));
|
|
|
|
foundPath = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (oldPath != null)
|
|
{
|
|
string oldFolder = EditorUtils.GetParentFolder(oldPath);
|
|
string newFolder = EditorUtils.GetParentFolder(newPath);
|
|
|
|
if (newFolder != oldFolder)
|
|
{
|
|
AddFolderTasks(oldFolder);
|
|
}
|
|
}
|
|
|
|
if (!foundPath && ((info.type & Platform.BinaryType.Optional) == 0)
|
|
&& !tasks.Any(t => t.source == newPath))
|
|
{
|
|
tasks.Add(Task.Missing(newPath, platform));
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (string path in platform.GetObsoleteAssetPaths())
|
|
{
|
|
if (AssetExists(path) && !tasks.Any(t => t.source == path))
|
|
{
|
|
tasks.Add(Task.RemoveAsset(path, platform));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AddFolderTasks(string path)
|
|
{
|
|
string baseFolder = BaseFolders.First(f => path.StartsWith(f));
|
|
|
|
string currentFolder = path;
|
|
|
|
// Find the last folder in the path that exists, without leaving the base folder
|
|
while (currentFolder.StartsWith(baseFolder) && !AssetDatabase.IsValidFolder(currentFolder))
|
|
{
|
|
currentFolder = EditorUtils.GetParentFolder(currentFolder);
|
|
}
|
|
|
|
while (currentFolder.StartsWith(baseFolder) && currentFolder != baseFolder)
|
|
{
|
|
AddFolderTask(currentFolder);
|
|
currentFolder = EditorUtils.GetParentFolder(currentFolder);
|
|
}
|
|
}
|
|
|
|
private void AddFolderTask(string path)
|
|
{
|
|
if (!tasks.Any(t => t.type == Task.Type.RemoveFolder && t.source == path))
|
|
{
|
|
tasks.Add(Task.RemoveFolder(path));
|
|
}
|
|
}
|
|
|
|
private struct MoveRecord
|
|
{
|
|
public string source;
|
|
public string destination;
|
|
}
|
|
|
|
private static readonly MoveRecord[] codeFolders = {
|
|
// Release 2.0 layout
|
|
new MoveRecord() { source = FMODSource + "/Runtime", destination = "src" },
|
|
new MoveRecord() { source = FMODSource + "/Runtime/Timeline", destination = "src" },
|
|
new MoveRecord() { source = FMODSource + "/Runtime/wrapper", destination = "src" },
|
|
new MoveRecord() { source = FMODSource + "/Editor/Timeline", destination = "src/Editor" },
|
|
|
|
// Release 1.10 layout
|
|
new MoveRecord() { source = FMODRoot + "/Timeline", destination = "src" },
|
|
new MoveRecord() { source = FMODRoot + "/Wrapper", destination = "src" },
|
|
new MoveRecord() { source = "Assets/Plugins/Editor/FMOD", destination = "src/Editor" },
|
|
new MoveRecord() { source = "Assets/Plugins/Editor/FMOD/Timeline", destination = "src/Editor" },
|
|
};
|
|
|
|
private void AddMoveTask(string source, string destination)
|
|
{
|
|
if (!tasks.Any(t => t.source == source))
|
|
{
|
|
tasks.Add(Task.Move(source, destination, null));
|
|
}
|
|
}
|
|
|
|
private void GenerateTasksForCodeFolders()
|
|
{
|
|
foreach (MoveRecord folder in codeFolders)
|
|
{
|
|
if (AssetDatabase.IsValidFolder(folder.source))
|
|
{
|
|
foreach (string sourcePath in FindFileAssets(folder.source))
|
|
{
|
|
string filename = Path.GetFileName(sourcePath);
|
|
|
|
AddMoveTask(
|
|
sourcePath, $"Assets/{RuntimeUtils.PluginBasePath}/{folder.destination}/{filename}");
|
|
|
|
}
|
|
|
|
AddFolderTask(folder.source);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void GenerateTasksForLooseAssets()
|
|
{
|
|
foreach (MoveRecord asset in looseAssets)
|
|
{
|
|
string filename = Path.GetFileName(asset.source);
|
|
string destinationPath = $"Assets/{RuntimeUtils.PluginBasePath}/{asset.destination}/{filename}";
|
|
|
|
if (AssetExists(asset.source) && !AssetExists(destinationPath))
|
|
{
|
|
AddMoveTask(asset.source, destinationPath);
|
|
AddFolderTasks(EditorUtils.GetParentFolder(asset.source));
|
|
}
|
|
else if (AssetDatabase.IsValidFolder(asset.source) && AssetDatabase.IsValidFolder(destinationPath))
|
|
{
|
|
GenerateFolderMergeTasks(asset.source, destinationPath);
|
|
AddFolderTasks(asset.source);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void GenerateFolderMergeTasks(string sourceFolder, string destinationFolder)
|
|
{
|
|
IEnumerable<string> assetPaths = AssetDatabase.FindAssets(string.Empty, new string[] { sourceFolder })
|
|
.Select(g => AssetDatabase.GUIDToAssetPath(g))
|
|
.Where(p => !AssetDatabase.IsValidFolder(p) || IsFolderEmpty(p));
|
|
|
|
foreach (string sourcePath in assetPaths)
|
|
{
|
|
int prefixLength = sourceFolder.Length;
|
|
|
|
if (!sourceFolder.EndsWith("/"))
|
|
{
|
|
++prefixLength;
|
|
}
|
|
|
|
string relativePath = sourcePath.Substring(prefixLength);
|
|
string destinationPath = string.Format("{0}/{1}", destinationFolder, relativePath);
|
|
|
|
if (!AssetExists(destinationPath))
|
|
{
|
|
AddMoveTask(sourcePath, destinationPath);
|
|
AddFolderTasks(EditorUtils.GetParentFolder(sourcePath));
|
|
}
|
|
else if (AssetDatabase.IsValidFolder(sourcePath))
|
|
{
|
|
AddFolderTasks(sourcePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void GenerateTasksForLegacyCodeFiles()
|
|
{
|
|
foreach (string path in FindFileAssets(FMODRoot).Where(p => p.EndsWith(".cs")))
|
|
{
|
|
string destinationPath = $"Assets/{RuntimeUtils.PluginBasePath}/src/{Path.GetFileName(path)}";
|
|
|
|
if (!AssetExists(destinationPath))
|
|
{
|
|
AddMoveTask(path, destinationPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void GenerateTasksForFolderCleanup()
|
|
{
|
|
foreach (string folder in foldersToCleanUp)
|
|
{
|
|
if (AssetDatabase.IsValidFolder(folder))
|
|
{
|
|
AddFolderTask(folder);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<string> FindFileAssets(string folder)
|
|
{
|
|
if (AssetDatabase.IsValidFolder(folder))
|
|
{
|
|
return AssetDatabase.FindAssets(string.Empty, new string[] { folder })
|
|
.Select(g => AssetDatabase.GUIDToAssetPath(g))
|
|
.Where(p => (EditorUtils.GetParentFolder(p) == folder) && !AssetDatabase.IsValidFolder(p));
|
|
}
|
|
else
|
|
{
|
|
return Enumerable.Empty<string>();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void StartProcessing()
|
|
{
|
|
if (!IsProcessing())
|
|
{
|
|
EditorApplication.LockReloadAssemblies();
|
|
|
|
currentTask = 0;
|
|
processingState = ProcessMoveTasks()
|
|
.Concat(ProcessRemoveAssetTasks())
|
|
.Concat(ProcessRemoveFolderTasks())
|
|
.GetEnumerator();
|
|
}
|
|
}
|
|
|
|
private void StopProcessing()
|
|
{
|
|
if (IsProcessing())
|
|
{
|
|
processingState = null;
|
|
UpdateTaskCount();
|
|
SetDefaultStatus();
|
|
|
|
EditorApplication.UnlockReloadAssemblies();
|
|
|
|
if (taskCount == 0)
|
|
{
|
|
SetupWizardWindow.SetUpdateTaskComplete(SetupWizardWindow.UpdateTaskType.ReorganizePluginFiles);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsProcessing()
|
|
{
|
|
return processingState != null;
|
|
}
|
|
|
|
private void ProcessNextTask()
|
|
{
|
|
if (processingState != null)
|
|
{
|
|
if (processingState.MoveNext())
|
|
{
|
|
statusContent = new GUIContent(processingState.Current);
|
|
Repaint();
|
|
}
|
|
else
|
|
{
|
|
StopProcessing();
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerable<string> ProcessMoveTasks()
|
|
{
|
|
foreach (Task task in tasks.Where(t => t.type == Task.Type.Move && t.status == Task.Status.Pending))
|
|
{
|
|
EditorUtils.EnsureFolderExists(EditorUtils.GetParentFolder(task.destination));
|
|
|
|
currentTask = task.step;
|
|
|
|
yield return string.Format("Moving {0} to {1}", task.source, task.destination);
|
|
|
|
string result = AssetDatabase.MoveAsset(task.source, task.destination);
|
|
|
|
if (string.IsNullOrEmpty(result))
|
|
{
|
|
task.SetSucceeded(string.Format("{0} was moved to\n{1}", task.source, task.destination));
|
|
}
|
|
else
|
|
{
|
|
task.SetFailed(string.Format("{0} could not be moved to\n{1}: '{2}'",
|
|
task.source, task.destination, result));
|
|
}
|
|
|
|
yield return task.statusText;
|
|
}
|
|
}
|
|
|
|
private static bool AssetExists(string path)
|
|
{
|
|
return EditorUtils.AssetExists(path);
|
|
}
|
|
|
|
private IEnumerable<string> ProcessRemoveAssetTasks()
|
|
{
|
|
foreach (Task task in tasks.Where(t => t.type == Task.Type.RemoveAsset && t.status == Task.Status.Pending))
|
|
{
|
|
currentTask = task.step;
|
|
|
|
if (AssetDatabase.MoveAssetToTrash(task.source))
|
|
{
|
|
task.SetSucceeded(string.Format("{0} was removed", task.source));
|
|
}
|
|
else
|
|
{
|
|
task.SetFailed(string.Format("{0} could not be removed", task.source));
|
|
}
|
|
|
|
yield return task.statusText;
|
|
}
|
|
}
|
|
|
|
private static bool IsFolderEmpty(string path)
|
|
{
|
|
return AssetDatabase.FindAssets(string.Empty, new string[] { path }).Length == 0;
|
|
}
|
|
|
|
private IEnumerable<string> ProcessRemoveFolderTasks()
|
|
{
|
|
foreach (Task task in tasks.Where(t => t.type == Task.Type.RemoveFolder && t.status == Task.Status.Pending))
|
|
{
|
|
currentTask = task.step;
|
|
|
|
foreach (string result in RemoveFolderIfEmpty(task))
|
|
{
|
|
yield return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<string> RemoveFolderIfEmpty(Task task)
|
|
{
|
|
if (!Directory.Exists(Application.dataPath + "/../" + task.source))
|
|
{
|
|
task.SetSucceeded(string.Format("{0} has already been removed", task.source));
|
|
yield break;
|
|
}
|
|
|
|
if (!AssetDatabase.IsValidFolder(task.source))
|
|
{
|
|
task.SetFailed(string.Format("{0} is not a valid folder", task.source));
|
|
yield break;
|
|
}
|
|
|
|
if (!IsFolderEmpty(task.source))
|
|
{
|
|
task.SetFailed(string.Format("{0} is not empty", task.source));
|
|
yield break;
|
|
}
|
|
|
|
yield return string.Format("Removing empty folder {0}", task.source);
|
|
|
|
if (AssetDatabase.MoveAssetToTrash(task.source))
|
|
{
|
|
task.SetSucceeded(string.Format("{0} was removed", task.source));
|
|
}
|
|
else
|
|
{
|
|
task.SetFailed(string.Format("{0} could not be removed", task.source));
|
|
}
|
|
|
|
yield return task.statusText;
|
|
}
|
|
}
|
|
}
|