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(); 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; } /// /// Draw overall stats view of the current build report /// public void DrawOverallStats() { if (HasReport && BuildSucceeded) { var stats = base.GetRows().Cast().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); } } } /// /// Handle double clicks inside the TreeView /// /// 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(clickedItem.path)); } /// /// Handle context clicks inside the TreeView /// /// ID of the clicked TreeView item protected override void ContextClickedItem(int id) { base.ContextClickedItem(id); // Get the clicked item var clickedItem = (BuildReportItem) FindItem(id, rootItem); //base.SetSelection(new IList()); // 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; } } /// /// 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. /// private void SelectAssetsInProjectWindow() { // Retrieve the IDs of currently selected items var selectedItems = GetSelection(); var assetPaths = new List(); // 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).ToArray(); Selection.objects = assets; } /// /// Check if current item matches the search string /// /// Item to match /// Search string /// Returns true if the search term matches name or asset type 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 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(); Func 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().ToList(); } private void OnSortingChanged(MultiColumnHeader _) { Reload(); } } }