553 lines
20 KiB
C#
553 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.Build.Reporting;
|
|
using UnityEditor.IMGUI.Controls;
|
|
using UnityEngine;
|
|
|
|
namespace VRWorldToolkit.Editor
|
|
{
|
|
public class BuildReportTreeView : TreeView
|
|
{
|
|
private BuildReport report;
|
|
public bool HasReport { get; private set; }
|
|
public bool BuildSucceeded { get; private set; }
|
|
|
|
private enum TreeColumns
|
|
{
|
|
Type,
|
|
Size,
|
|
Name,
|
|
Extension,
|
|
Percentage,
|
|
}
|
|
|
|
private readonly MultiColumnHeader header;
|
|
|
|
public BuildReportTreeView(TreeViewState state, MultiColumnHeader header, BuildReport report) : base(state, header)
|
|
{
|
|
this.header = header;
|
|
showBorder = true;
|
|
showAlternatingRowBackgrounds = true;
|
|
this.header.sortingChanged += OnSortingChanged;
|
|
|
|
SetReport(report);
|
|
}
|
|
|
|
private class BuildListAsset
|
|
{
|
|
public string AssetType { get; set; }
|
|
public string FullPath { get; set; }
|
|
public ulong Size { get; set; }
|
|
public double Percentage { get; set; }
|
|
|
|
public BuildListAsset()
|
|
{
|
|
}
|
|
|
|
public BuildListAsset(Type assetType, string fullPath, ulong size)
|
|
{
|
|
AssetType = assetType.Name;
|
|
FullPath = fullPath;
|
|
Size = size;
|
|
}
|
|
}
|
|
|
|
private sealed class BuildReportItem : TreeViewItem
|
|
{
|
|
public Texture previewIcon { get; set; }
|
|
public string assetType { get; set; }
|
|
public string path { get; set; }
|
|
public string extension { get; set; }
|
|
public ulong size { get; set; }
|
|
public double percentage { get; set; }
|
|
|
|
public BuildReportItem(int id, int depth, Texture previewIcon, string assetType, string displayName, string path, string extension, ulong size, double percentage) : base(id, depth, displayName)
|
|
{
|
|
this.previewIcon = previewIcon;
|
|
this.assetType = assetType;
|
|
this.displayName = displayName;
|
|
this.path = path;
|
|
this.extension = extension;
|
|
this.size = size;
|
|
this.percentage = percentage;
|
|
}
|
|
}
|
|
|
|
protected override TreeViewItem BuildRoot()
|
|
{
|
|
var root = new TreeViewItem {id = -1, depth = -1};
|
|
|
|
var packedAssets = report.packedAssets;
|
|
|
|
var bl = new List<BuildListAsset>();
|
|
|
|
for (var i = 0; i < packedAssets.Length; i++)
|
|
{
|
|
var packedAssetInfos = packedAssets[i].contents;
|
|
for (int j = 0; j < packedAssetInfos.Length; j++)
|
|
{
|
|
var packedAssetInfo = packedAssetInfos[j];
|
|
|
|
var asset = new BuildListAsset(packedAssetInfo.type, packedAssetInfo.sourceAssetPath, packedAssetInfo.packedSize);
|
|
|
|
bl.Add(asset);
|
|
}
|
|
}
|
|
|
|
var results = bl
|
|
.GroupBy(x => x.FullPath)
|
|
.Select(cx => new BuildListAsset()
|
|
{
|
|
AssetType = cx.First().AssetType,
|
|
FullPath = cx.First().FullPath,
|
|
Size = cx.Aggregate(0UL, (total, x) => total + x.Size),
|
|
})
|
|
.OrderByDescending(x => x.Size)
|
|
.ToList();
|
|
|
|
var totalSize = results.Sum(x => (long) x.Size);
|
|
|
|
for (var i = 0; i < results.Count; i++)
|
|
{
|
|
results[i].Percentage = (double) results[i].Size / totalSize;
|
|
}
|
|
|
|
for (var i = 0; i < results.Count; i++)
|
|
{
|
|
var asset = results[i];
|
|
|
|
root.AddChild(new BuildReportItem(i,
|
|
0,
|
|
AssetDatabase.GetCachedIcon(asset.FullPath),
|
|
asset.AssetType,
|
|
GetFileName(asset.FullPath),
|
|
asset.FullPath,
|
|
GetFileExtension(asset.FullPath),
|
|
asset.Size,
|
|
asset.Percentage)
|
|
);
|
|
}
|
|
|
|
return root;
|
|
|
|
static string GetFileName(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
{
|
|
return "Unknown";
|
|
}
|
|
|
|
if (path.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
|
|
{
|
|
return path;
|
|
}
|
|
|
|
return Path.GetFileName(path);
|
|
}
|
|
|
|
static string GetFileExtension(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path) || path.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
|
|
{
|
|
return "Unknown";
|
|
}
|
|
|
|
return Path.GetExtension(path);
|
|
}
|
|
}
|
|
|
|
public void SetReport(BuildReport newReport)
|
|
{
|
|
report = newReport;
|
|
HasReport = report != null;
|
|
BuildSucceeded = HasReport && report.summary.result == BuildResult.Succeeded;
|
|
|
|
if (HasReport && BuildSucceeded)
|
|
{
|
|
Reload();
|
|
}
|
|
}
|
|
|
|
private bool HasMessages()
|
|
{
|
|
return report.summary.totalErrors > 0 || report.summary.totalWarnings > 0;
|
|
}
|
|
|
|
private struct CategoryStats
|
|
{
|
|
public string Name;
|
|
public ulong Size;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw overall stats view of the current build report
|
|
/// </summary>
|
|
public void DrawOverallStats()
|
|
{
|
|
if (HasReport && BuildSucceeded)
|
|
{
|
|
var stats = base.GetRows().Cast<BuildReportItem>().ToList();
|
|
|
|
var totalSize = stats.Aggregate(0UL, (total, x) => total + x.size);
|
|
|
|
var grouped = stats
|
|
.GroupBy(x => x.assetType)
|
|
.Select(cx => new CategoryStats()
|
|
{
|
|
Name = cx.First().assetType,
|
|
Size = cx.Aggregate(0UL, (total, x) => total + x.size),
|
|
}).OrderByDescending(x => x.Size)
|
|
.ToArray();
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
|
|
for (var i = 0; i < grouped.Length; i++)
|
|
{
|
|
var item = grouped[i];
|
|
|
|
string name;
|
|
|
|
switch (item.Name)
|
|
{
|
|
case "Mono":
|
|
name = "Scripts";
|
|
break;
|
|
case "Model":
|
|
case "Texture":
|
|
case "Shader":
|
|
case "Asset":
|
|
case "TrueTypeFont":
|
|
case "Plugin":
|
|
case "Prefab":
|
|
name = item.Name + "s";
|
|
break;
|
|
default:
|
|
name = item.Name;
|
|
break;
|
|
}
|
|
|
|
var rect = GUILayoutUtility.GetRect(GUIContent.none, EditorStyles.label);
|
|
var barGraphRect = rect;
|
|
|
|
barGraphRect.width *= (float)((double)item.Size / totalSize);
|
|
EditorGUI.DrawRect(barGraphRect, new Color(0.28f, 0.37f, 0.51f, 0.6f));
|
|
EditorGUIUtility.AddCursorRect(rect, MouseCursor.Link);
|
|
|
|
GUI.Label(rect , name + " - " + EditorUtility.FormatBytes((long)item.Size), EditorStyles.label);
|
|
GUI.Label(rect, ((double)item.Size / totalSize).ToString("P"), Styles.BuildReportStatsLabel);
|
|
|
|
if (GUI.Button(rect, GUIContent.none, GUIStyle.none))
|
|
{
|
|
searchString = item.Name;
|
|
}
|
|
}
|
|
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
}
|
|
|
|
private Vector2 scrollPosMessages;
|
|
|
|
public void DrawMessages()
|
|
{
|
|
if (HasReport && HasMessages())
|
|
{
|
|
EditorGUILayout.BeginVertical();
|
|
scrollPosMessages = EditorGUILayout.BeginScrollView(scrollPosMessages);
|
|
|
|
var steps = report.steps;
|
|
|
|
for (var i = 0; i < steps.Length; i++)
|
|
{
|
|
var step = steps[i];
|
|
|
|
if (step.messages.Length > 0)
|
|
{
|
|
GUILayout.Label(step.name, Styles.BoldWrap);
|
|
|
|
for (var j = 0; j < step.messages.Length; j++)
|
|
{
|
|
var message = step.messages[j];
|
|
|
|
var messageType = MessageType.Info;
|
|
|
|
switch (message.type)
|
|
{
|
|
case LogType.Error:
|
|
case LogType.Exception:
|
|
messageType = MessageType.Error;
|
|
break;
|
|
case LogType.Assert:
|
|
case LogType.Warning:
|
|
messageType = MessageType.Warning;
|
|
break;
|
|
}
|
|
|
|
EditorGUILayout.HelpBox(message.content, messageType);
|
|
}
|
|
|
|
EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
|
|
}
|
|
}
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
else
|
|
{
|
|
EditorGUILayout.HelpBox("No messages to show.", MessageType.Info);
|
|
}
|
|
}
|
|
|
|
public static MultiColumnHeaderState CreateDefaultMultiColumnHeaderState(float treeViewWidth)
|
|
{
|
|
var columns = new[]
|
|
{
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
headerContent = EditorGUIUtility.IconContent("FilterByType"),
|
|
contextMenuText = "Preview",
|
|
headerTextAlignment = TextAlignment.Center,
|
|
canSort = false,
|
|
width = 20,
|
|
minWidth = 20,
|
|
maxWidth = 20,
|
|
autoResize = false,
|
|
allowToggleVisibility = true
|
|
},
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
headerContent = new GUIContent("USize", "Uncompressed size of the asset. Unity compresses assets during the build process, so this value will not match the size of the asset within the build."),
|
|
contextMenuText = "USize",
|
|
headerTextAlignment = TextAlignment.Left,
|
|
sortedAscending = true,
|
|
sortingArrowAlignment = TextAlignment.Right,
|
|
width = 60,
|
|
minWidth = 60,
|
|
maxWidth = 75,
|
|
autoResize = false,
|
|
allowToggleVisibility = true
|
|
},
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
headerContent = new GUIContent("Name"),
|
|
headerTextAlignment = TextAlignment.Left,
|
|
sortedAscending = true,
|
|
sortingArrowAlignment = TextAlignment.Center,
|
|
width = 250,
|
|
minWidth = 60,
|
|
autoResize = true,
|
|
allowToggleVisibility = false
|
|
},
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
headerContent = new GUIContent("Type", "File type"),
|
|
contextMenuText = "Type",
|
|
headerTextAlignment = TextAlignment.Left,
|
|
sortedAscending = true,
|
|
sortingArrowAlignment = TextAlignment.Right,
|
|
width = 60,
|
|
minWidth = 60,
|
|
maxWidth = 100,
|
|
autoResize = true,
|
|
allowToggleVisibility = true
|
|
},
|
|
new MultiColumnHeaderState.Column
|
|
{
|
|
headerContent = new GUIContent("%", "Percentage out of all assets"),
|
|
contextMenuText = "Percentage",
|
|
headerTextAlignment = TextAlignment.Left,
|
|
sortedAscending = true,
|
|
sortingArrowAlignment = TextAlignment.Right,
|
|
width = 60,
|
|
minWidth = 60,
|
|
maxWidth = 70,
|
|
autoResize = false,
|
|
allowToggleVisibility = true
|
|
}
|
|
};
|
|
|
|
var state = new MultiColumnHeaderState(columns);
|
|
return state;
|
|
}
|
|
|
|
protected override void RowGUI(RowGUIArgs args)
|
|
{
|
|
var buildReportItem = (BuildReportItem) args.item;
|
|
|
|
for (var visibleColumnIndex = 0; visibleColumnIndex < args.GetNumVisibleColumns(); visibleColumnIndex++)
|
|
{
|
|
Rect rect;
|
|
// Get the current cell rect and index
|
|
if (visibleColumnIndex == 2)
|
|
{
|
|
var rectOne = args.GetCellRect(visibleColumnIndex);
|
|
var rectTwo = args.GetCellRect(3);
|
|
|
|
rect = new Rect(rectOne.position, new Vector2(rectOne.width + rectTwo.width, rectOne.height));
|
|
}
|
|
else
|
|
{
|
|
rect = args.GetCellRect(visibleColumnIndex);
|
|
}
|
|
|
|
var columnIndex = (TreeColumns) args.GetColumn(visibleColumnIndex);
|
|
|
|
//Set label style to white if cell is selected otherwise to normal
|
|
var labelStyle = args.selected ? Styles.TreeViewLabelSelected : Styles.TreeViewLabel;
|
|
|
|
//Handle drawing of the columns
|
|
switch (columnIndex)
|
|
{
|
|
case TreeColumns.Type:
|
|
GUI.Label(rect, buildReportItem.previewIcon, Styles.TreeViewLabelCenter);
|
|
break;
|
|
case TreeColumns.Name:
|
|
if (args.selected && buildReportItem.path != "")
|
|
{
|
|
EditorGUI.LabelField(rect, buildReportItem.path, labelStyle);
|
|
}
|
|
else
|
|
{
|
|
EditorGUI.LabelField(rect, buildReportItem.displayName, labelStyle);
|
|
}
|
|
|
|
break;
|
|
case TreeColumns.Extension:
|
|
//EditorGUI.LabelField(rect, buildReportItem.extension, labelStyle);
|
|
break;
|
|
case TreeColumns.Size:
|
|
EditorGUI.LabelField(rect, EditorUtility.FormatBytes((long)buildReportItem.size), labelStyle);
|
|
break;
|
|
case TreeColumns.Percentage:
|
|
EditorGUI.LabelField(rect, buildReportItem.percentage.ToString("P"), labelStyle);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(columnIndex), columnIndex, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle double clicks inside the TreeView
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
protected override void DoubleClickedItem(int id)
|
|
{
|
|
base.DoubleClickedItem(id);
|
|
|
|
// Get the clicked item
|
|
var clickedItem = (BuildReportItem) FindItem(id, rootItem);
|
|
|
|
//Ping clicked asset in project window
|
|
EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(clickedItem.path));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle context clicks inside the TreeView
|
|
/// </summary>
|
|
/// <param name="id">ID of the clicked TreeView item</param>
|
|
protected override void ContextClickedItem(int id)
|
|
{
|
|
base.ContextClickedItem(id);
|
|
|
|
// Get the clicked item
|
|
var clickedItem = (BuildReportItem) FindItem(id, rootItem);
|
|
|
|
//base.SetSelection(new IList<int>());
|
|
|
|
// Create new
|
|
var menu = new GenericMenu();
|
|
|
|
// Create the menu items
|
|
menu.AddItem(new GUIContent("Copy Name"), false, ReplaceClipboard, Path.GetFileName(clickedItem.path));
|
|
menu.AddItem(new GUIContent("Copy Path"), false, ReplaceClipboard, clickedItem.path);
|
|
menu.AddItem(new GUIContent("Reveal in Explorer"), false, () => EditorUtility.RevealInFinder(clickedItem.path));
|
|
menu.AddItem(new GUIContent("Select in Assets"), false, SelectAssetsInProjectWindow);
|
|
|
|
// Show the menu
|
|
menu.ShowAsContext();
|
|
|
|
// Function to replace clipboard contents
|
|
void ReplaceClipboard(object input)
|
|
{
|
|
EditorGUIUtility.systemCopyBuffer = (string) input;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects assets in the Project window based on the currently selected BuildReportItems.
|
|
/// This is useful for quickly selecting a batch of assets to modify their import settings or other properties in bulk.
|
|
/// </summary>
|
|
private void SelectAssetsInProjectWindow()
|
|
{
|
|
// Retrieve the IDs of currently selected items
|
|
var selectedItems = GetSelection();
|
|
var assetPaths = new List<string>();
|
|
|
|
// Iterate over each selected item and collect their asset paths
|
|
foreach (var itemId in selectedItems)
|
|
{
|
|
var item = FindItem(itemId, rootItem) as BuildReportItem;
|
|
if (item != null && !string.IsNullOrEmpty(item.path))
|
|
{
|
|
assetPaths.Add(item.path);
|
|
}
|
|
}
|
|
|
|
// Load and select the assets in the Project window
|
|
var assets = assetPaths.Select(AssetDatabase.LoadAssetAtPath<UnityEngine.Object>).ToArray();
|
|
Selection.objects = assets;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if current item matches the search string
|
|
/// </summary>
|
|
/// <param name="item">Item to match</param>
|
|
/// <param name="search">Search string</param>
|
|
/// <returns>Returns true if the search term matches name or asset type</returns>
|
|
protected override bool DoesItemMatchSearch(TreeViewItem item, string search)
|
|
{
|
|
// Cast match item for parameter access
|
|
var textureTreeViewItem = (BuildReportItem) item;
|
|
|
|
// Try to match the search string to item name or asset type and return true if it does
|
|
return textureTreeViewItem.displayName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
|
textureTreeViewItem.assetType.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
|
|
protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
|
|
{
|
|
var rows = base.BuildRows(root);
|
|
|
|
var sortedColumn = header.sortedColumnIndex;
|
|
if (sortedColumn < 0) return rows;
|
|
|
|
var isSortedAscending = header.IsSortedAscending(sortedColumn);
|
|
|
|
var items = rows.Cast<BuildReportItem>();
|
|
|
|
Func<BuildReportItem, object> selector = sortedColumn switch
|
|
{
|
|
1 => i => i.size,
|
|
2 => i => i.displayName,
|
|
3 => i => i.extension,
|
|
4 => i => i.size,
|
|
_ => i => i.displayName
|
|
};
|
|
|
|
items = isSortedAscending ? items.OrderBy(selector) : items.OrderByDescending(selector);
|
|
|
|
return items.Cast<TreeViewItem>().ToList();
|
|
}
|
|
|
|
private void OnSortingChanged(MultiColumnHeader _)
|
|
{
|
|
Reload();
|
|
}
|
|
}
|
|
}
|