Files
Silly-Home/Packages/dev.onevr.vrworldtoolkit/Editor/BuildReportTreeView.cs
2026-06-07 16:58:24 +01:00

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();
}
}
}