2022-01-16 05:40:49 +03:00

599 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.Assertions;
namespace HeurekaGames.AssetHunterPRO.BaseTreeviewImpl.AssetTreeView
{
class AH_TreeViewWithTreeModel : TreeViewWithTreeModel<AH_TreeviewElement>
{
const float kRowHeights = 20f;
const float kToggleWidth = 18f;
AH_TreeViewSelectionInfo treeviewSelectionInfo = new AH_TreeViewSelectionInfo();
GUIContent[] guiContents_toolbarShowSelection = new GUIContent[3]
{
new GUIContent(AH_MultiColumnHeader.AssetShowMode.Unused.ToString(),"Show only assets that was NOT included in build"),
new GUIContent(AH_MultiColumnHeader.AssetShowMode.Used.ToString(),"Show only assets that WAS included in build"),
new GUIContent(AH_MultiColumnHeader.AssetShowMode.All.ToString(),"Show all assets in project")
};
// All columns
enum MyColumns
{
//Dummy,
Icon,
Name,
AssetSize,
FileSize,
UsedInBuild,
LevelUsage
}
public enum SortOption
{
AssetType,
Name,
AssetSize,
FileSize,
Used,
LevelRefs,
}
// Sort options per column
SortOption[] m_SortOptions =
{
//SortOption.Value1,
SortOption.AssetType,
SortOption.Name,
SortOption.AssetSize,
SortOption.FileSize,
SortOption.Used,
SortOption.LevelRefs
};
public static void TreeToList(TreeViewItem root, IList<TreeViewItem> result)
{
if (root == null)
throw new NullReferenceException("root");
if (result == null)
throw new NullReferenceException("result");
result.Clear();
if (root.children == null)
return;
Stack<TreeViewItem> stack = new Stack<TreeViewItem>();
for (int i = root.children.Count - 1; i >= 0; i--)
stack.Push(root.children[i]);
while (stack.Count > 0)
{
TreeViewItem current = stack.Pop();
result.Add(current);
if (current.hasChildren && current.children[0] != null)
{
for (int i = current.children.Count - 1; i >= 0; i--)
{
stack.Push(current.children[i]);
}
}
}
}
public AH_TreeViewWithTreeModel(TreeViewState state, MultiColumnHeader multicolumnHeader, TreeModel<AH_TreeviewElement> model) : base(state, multicolumnHeader, model)
{
Assert.AreEqual(m_SortOptions.Length, Enum.GetValues(typeof(MyColumns)).Length, "Ensure number of sort options are in sync with number of MyColumns enum values");
// Custom setup
rowHeight = kRowHeights;
columnIndexForTreeFoldouts = 1;
showAlternatingRowBackgrounds = true;
showBorder = true;
customFoldoutYOffset = (kRowHeights - EditorGUIUtility.singleLineHeight) * 0.5f; // center foldout in the row since we also center content. See RowGUI
extraSpaceBeforeIconAndLabel = kToggleWidth;
multicolumnHeader.sortingChanged += OnSortingChanged;
//Make sure we show the correct columns
updateColumns();
//IF we want to start expanded one level
if (model.root.hasChildren)
SetExpanded(model.root.children[0].id, true);
Reload();
}
protected override bool RequiresSorting()
{
//Show as list if base requires sorting OR if we chose sortedList
return base.RequiresSorting() || ((AH_MultiColumnHeader)multiColumnHeader).mode == AH_MultiColumnHeader.Mode.SortedList;
}
protected override void AddChildrenRecursive(AH_TreeviewElement parent, int depth, IList<TreeViewItem> newRows)
{
AH_MultiColumnHeader.AssetShowMode showMode = ((AH_MultiColumnHeader)multiColumnHeader).ShowMode;
foreach (AH_TreeviewElement child in parent.children)
{
bool isFolder = child.hasChildren;
bool isFolderWithValidChildren = isFolder && (showMode == AH_MultiColumnHeader.AssetShowMode.All || child.HasChildrenThatMatchesState(showMode));
//TODO EXTRACT THIS TO BE ABLE TO Ignore TYPES ETC OR MAYBE THAT SHOULD BE DONE WHEN POPULATING THE FIRST TIME...maybe better
//If its a folder or an asset that matches showmode
if (isFolderWithValidChildren || child.AssetMatchesState(showMode))
{
//Add new row
var item = new TreeViewItem<AH_TreeviewElement>(child.id, depth, child.Name, child);
newRows.Add(item);
if (isFolder)
{
if (IsExpanded(child.id))
{
AddChildrenRecursive(child, depth + 1, newRows);
}
else
{
item.children = CreateChildListForCollapsedParent();
}
}
}
}
}
public override void OnGUI(Rect rect)
{
Rect treeViewRect = rect;
//If we have selection, we want to make room in the rect to show that
if (treeviewSelectionInfo.HasSelection)
treeViewRect.height -= AH_TreeViewSelectionInfo.Height;
base.OnGUI(treeViewRect);
Rect SelectionRect = new Rect(treeViewRect.x, treeViewRect.yMax, treeViewRect.width, AH_TreeViewSelectionInfo.Height);
if (treeviewSelectionInfo.HasSelection/* && ((AH_MultiColumnHeader)multiColumnHeader).ShowMode == AH_MultiColumnHeader.AssetShowMode.Unused*/)
treeviewSelectionInfo.OnGUISelectionInfo(SelectionRect);
}
// Note we We only build the visible rows, only the backend has the full tree information.
// The treeview only creates info for the row list.
protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
{
var rows = base.BuildRows(root);
SortIfNeeded(root, rows);
return rows;
}
void GetAllChildren(TreeViewItem root, ref List<TreeViewItem<AH_TreeviewElement>> children)
{
if (root != null && root.hasChildren && root.children != null)
{
foreach (var child in root.children)
{
children.AddRange(root.children.Cast<TreeViewItem<AH_TreeviewElement>>());
GetAllChildren(child, ref children);
}
}
}
void OnSortingChanged(MultiColumnHeader multiColumnHeader)
{
ModelChanged();
SortIfNeeded(rootItem, GetRows());
}
void SortIfNeeded(TreeViewItem root, IList<TreeViewItem> rows)
{
if (rows.Count <= 1)
return;
if (multiColumnHeader.sortedColumnIndex == -1)
{
return; // No column to sort for (just use the order the data are in)
}
SortByMultipleColumns();
TreeToList(root, rows);
Repaint();
}
private void deselectItems()
{
treeviewSelectionInfo.Reset();
IList<int> emptyList = new List<int>();
this.SetSelection(emptyList);
}
void SortByMultipleColumns()
{
var sortedColumns = multiColumnHeader.state.sortedColumns;
if (sortedColumns.Length == 0)
return;
var myTypes = rootItem.children.Cast<TreeViewItem<AH_TreeviewElement>>();
//Try to visualize to user that we are getting asset sizes
if (AH_SettingsManager.Instance.EstimateAssetSize)
{
int counter = 0;
int typeCount = myTypes.Count();
foreach (var item in myTypes)
{
counter++;
if (item == null)
continue;
EditorUtility.DisplayProgressBar($"Getting asset sizes", $"{ counter} / {typeCount} : {item.data.Name} : {item.data.AssetSizeStringRepresentation}", (float)counter / (float)typeCount);
}
EditorUtility.ClearProgressBar();
}
var orderedQuery = InitialOrder(myTypes, sortedColumns);
for (int i = 1; i < sortedColumns.Length; i++)
{
SortOption sortOption = m_SortOptions[sortedColumns[i]];
bool ascending = multiColumnHeader.IsSortedAscending(sortedColumns[i]);
switch (sortOption)
{
case SortOption.Name:
orderedQuery = orderedQuery.ThenBy(l => l.data.Name, ascending);
break;
case SortOption.AssetSize:
orderedQuery = orderedQuery.ThenBy(l => l.data.AssetSize, ascending);
break;
case SortOption.FileSize:
orderedQuery = orderedQuery.ThenBy(l => l.data.FileSize, ascending);
break;
case SortOption.AssetType:
//Make sure that type is not null, if it is null then its a folder and we sort by path
orderedQuery = orderedQuery.ThenBy(l => ((l.data.AssetType != null) ? l.data.AssetType.ToString() : l.data.RelativePath), ascending);//.ThenBy(x => x.Pet != null ? x.Pet.Name : "");
break;
case SortOption.LevelRefs:
orderedQuery = orderedQuery.ThenBy(l => l.data.SceneRefCount, ascending);
break;
case SortOption.Used:
orderedQuery = orderedQuery.ThenBy(l => l.data.UsedInBuild, ascending);
break;
default:
Assert.IsTrue(false, "Unhandled enum");
break;
}
}
rootItem.children = orderedQuery.Cast<TreeViewItem>().ToList();
}
internal long GetCombinedUnusedSize()
{
return treeModel.root.GetFileSizeRecursively(AH_MultiColumnHeader.AssetShowMode.Unused);
}
internal void DrawDeleteAllButton(GUIContent content, GUIStyle style, params GUILayoutOption[] layout)
{
treeviewSelectionInfo.DrawDeleteFolderButton(content, treeModel.root, style, content.tooltip, "This will delete ALL assets not used in last build. Its recommended to review the unused asset list and to backup before proceding", layout);
}
internal void ShowTreeMode()
{
searchString = "";
multiColumnHeader.sortedColumnIndex = -1;
((AH_MultiColumnHeader)multiColumnHeader).mode = AH_MultiColumnHeader.Mode.Treeview;
//Make sure we dont cause an endless loop
ModelChanged();
}
//Check if it matches state and searchstring
protected override bool IsValidElement(TreeElement element, string searchString)
{
AH_TreeviewElement ahElement = (element as AH_TreeviewElement);
AH_MultiColumnHeader.AssetShowMode hShowMode = ((AH_MultiColumnHeader)multiColumnHeader).ShowMode;
bool validAsset = !ahElement.IsFolder && ahElement.AssetMatchesState(hShowMode);
//Only show folders if we are in treeview mode and the folder has children that matches state
bool validFolder = searchString == "" && (ahElement.IsFolder && (((AH_MultiColumnHeader)multiColumnHeader).mode == AH_MultiColumnHeader.Mode.Treeview) && ahElement.HasChildrenThatMatchesState(hShowMode));
return (validAsset || validFolder) && base.IsValidElement(element, searchString);
}
internal void AssetSelectionToolBarGUI()
{
EditorGUI.BeginChangeCheck();
((AH_MultiColumnHeader)multiColumnHeader).ShowMode = (AH_MultiColumnHeader.AssetShowMode)GUILayout.Toolbar((int)((AH_MultiColumnHeader)multiColumnHeader).ShowMode, guiContents_toolbarShowSelection/*, GUILayout.MaxWidth(AH_SettingsManager.Instance.ShowMenuText ? 290 : 128)*/);
if (EditorGUI.EndChangeCheck())
{
showModeChanged();
}
}
private void showModeChanged()
{
updateColumns();
deselectItems();
ModelChanged();
}
private void updateColumns()
{
#region Add or remove the "asset size" column as required
bool currentlyShowsAssetSize = (multiColumnHeader.state.visibleColumns.ToList().Contains((int)MyColumns.AssetSize));
bool couldHaveAssetSize = ((AH_MultiColumnHeader)multiColumnHeader).ShowMode != AH_MultiColumnHeader.AssetShowMode.Unused;
if (AH_SettingsManager.Instance.EstimateAssetSize)
{
if (!currentlyShowsAssetSize && couldHaveAssetSize)
{
var tmpList = multiColumnHeader.state.visibleColumns.ToList();
tmpList.Add((int)MyColumns.AssetSize);
multiColumnHeader.state.visibleColumns = tmpList.ToArray();
Array.Sort(multiColumnHeader.state.visibleColumns);
}
}
else
{
if (currentlyShowsAssetSize)
{
var tmpList = multiColumnHeader.state.visibleColumns.ToList();
tmpList.Remove((int)MyColumns.AssetSize);
multiColumnHeader.state.visibleColumns = tmpList.ToArray();
}
}
#endregion
}
IOrderedEnumerable<TreeViewItem<AH_TreeviewElement>> InitialOrder(IEnumerable<TreeViewItem<AH_TreeviewElement>> myTypes, int[] history)
{
SortOption sortOption = m_SortOptions[history[0]];
bool ascending = multiColumnHeader.IsSortedAscending(history[0]);
switch (sortOption)
{
case SortOption.Name:
return myTypes.Order(l => l.data.Name, ascending);
case SortOption.AssetSize:
return myTypes.Order(l => l.data.AssetSize, ascending);
case SortOption.FileSize:
return myTypes.Order(l => l.data.FileSize, ascending);
case SortOption.AssetType:
return myTypes.Order(l => l.data.AssetTypeSerialized, ascending);
case SortOption.LevelRefs:
return myTypes.Order(l => l.data.ScenesReferencingAsset?.Count, ascending);
case SortOption.Used:
return myTypes.Order(l => l.data.UsedInBuild, ascending);
default:
Assert.IsTrue(false, "Unhandled enum");
break;
}
// default
return myTypes.Order(l => l.data.Name, ascending);
}
protected override void RowGUI(RowGUIArgs args)
{
var item = (TreeViewItem<AH_TreeviewElement>)args.item;
for (int i = 0; i < args.GetNumVisibleColumns(); ++i)
{
CellGUI(args.GetCellRect(i), item, (MyColumns)args.GetColumn(i), ref args);
}
}
void CellGUI(Rect cellRect, TreeViewItem<AH_TreeviewElement> item, MyColumns column, ref RowGUIArgs args)
{
// Center cell rect vertically (makes it easier to place controls, icons etc in the cells)
CenterRectUsingSingleLineHeight(ref cellRect);
AH_TreeviewElement element = (AH_TreeviewElement)item.data;
switch (column)
{
case MyColumns.Icon:
{
if (item.data.AssetType != null)
GUI.DrawTexture(cellRect, AH_TreeviewElement.GetIcon(item.data.AssetType), ScaleMode.ScaleToFit);
}
break;
case MyColumns.Name:
{
Rect nameRect = cellRect;
nameRect.x += GetContentIndent(item);
DefaultGUI.Label(nameRect, item.data.m_Name, args.selected, args.focused);
}
break;
case MyColumns.AssetSize:
case MyColumns.FileSize:
{
string value = "";
if (column == MyColumns.AssetSize && element.AssetSize > 0)
value = element.AssetSizeStringRepresentation;
if (column == MyColumns.FileSize && element.FileSize > 0)
value = element.FileSizeStringRepresentation;
if (element.IsFolder && column == MyColumns.FileSize/*&& !IsExpanded(element.id)*/)
{
value = "{"+AH_Utils.BytesToString(element.GetFileSizeRecursively(((AH_MultiColumnHeader)multiColumnHeader).ShowMode))+"}";
DefaultGUI.Label(cellRect, value, args.selected, args.focused);
}
else
DefaultGUI.LabelRightAligned(cellRect, value, args.selected, args.focused);
}
break;
case MyColumns.UsedInBuild:
{
if (item.data.UsedInBuild)
DefaultGUI.LabelRightAligned(cellRect, "\u2713", args.selected, args.focused);
}
break;
case MyColumns.LevelUsage:
{
if (item.data.UsedInBuild && item.data.ScenesReferencingAsset != null)
{
if (item.data.ScenesReferencingAsset.Count > 0)
{
string cellString = String.Format("Usage: {0}", item.data.ScenesReferencingAsset.Count.ToString());
if (args.selected && args.focused)
{
if (GUI.Button(cellRect, cellString))
{
UnityEngine.Object[] sceneAssets = new UnityEngine.Object[item.data.ScenesReferencingAsset.Count];
string message = "";
string path = "";
for (int i = 0; i < item.data.ScenesReferencingAsset.Count; i++)
{
path = AssetDatabase.GUIDToAssetPath(item.data.ScenesReferencingAsset[i]);
message += (path + Environment.NewLine);
sceneAssets[i] = AssetDatabase.LoadMainAssetAtPath(path);
}
Selection.objects = sceneAssets;
EditorUtility.DisplayDialog("Scenes referencing " + path, message, "OK");
}
}
else
DefaultGUI.LabelRightAligned(cellRect, cellString, args.selected, args.focused);
}
}
}
break;
}
}
protected override void SelectionChanged(IList<int> selectedIds)
{
base.SelectionChanged(selectedIds);
//Only set selection if we are in "Unused Mode"
//if (((AH_MultiColumnHeader)multiColumnHeader).ShowMode == AH_MultiColumnHeader.AssetShowMode.Unused)
treeviewSelectionInfo.SetSelection(this, selectedIds);
}
protected override bool CanMultiSelect(TreeViewItem item)
{
return true;
}
public static MultiColumnHeaderState CreateDefaultMultiColumnHeaderState(float treeViewWidth)
{
var columns = new[]
{
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent(EditorGUIUtility.FindTexture("FilterByType"), "Type of asset"),
contextMenuText = "Type",
headerTextAlignment = TextAlignment.Center,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Right,
width = 30,
minWidth = 30,
maxWidth = 30,
autoResize = false,
allowToggleVisibility = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Name"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Center,
width = 280,
minWidth = 175,
autoResize = true,
allowToggleVisibility = false
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Asset size", "Size of the asset in project"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Center,
width = 80,
minWidth = 80,
maxWidth = 130,
autoResize = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Disc size", "Size of the file on disc"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Center,
width = 80,
minWidth = 80,
maxWidth = 130,
autoResize = true,
allowToggleVisibility = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Used", "If this asset is used in build"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Center,
width = 45,
minWidth = 30,
maxWidth = 45,
autoResize = true
},
new MultiColumnHeaderState.Column
{
headerContent = new GUIContent("Level refs", "How many scenes are using this asset"),
headerTextAlignment = TextAlignment.Left,
sortedAscending = true,
sortingArrowAlignment = TextAlignment.Center,
width = 70,
minWidth = 70,
maxWidth = 100,
autoResize = true
}
};
Assert.AreEqual(columns.Length, Enum.GetValues(typeof(MyColumns)).Length, "Number of columns should match number of enum values: You probably forgot to update one of them.");
var state = new MultiColumnHeaderState(columns);
return state;
}
}
static class MyExtensionMethods
{
public static IOrderedEnumerable<T> Order<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector, bool ascending)
{
if (ascending)
{
return source.OrderBy(selector);
}
else
{
return source.OrderByDescending(selector);
}
}
public static IOrderedEnumerable<T> ThenBy<T, TKey>(this IOrderedEnumerable<T> source, Func<T, TKey> selector, bool ascending)
{
if (ascending)
{
return source.ThenBy(selector);
}
else
{
return source.ThenByDescending(selector);
}
}
}
}