#if UNITY_EDITOR using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using UnityEditor.ShortcutManagement; using System.Reflection; using System.Linq; using System.IO; using UnityEngine.UIElements; using UnityEngine.SceneManagement; using UnityEditor.SceneManagement; using UnityEditor.IMGUI.Controls; using UnityEditorInternal; using Type = System.Type; using static VFolders.VFoldersData; using static VFolders.VFoldersCache; using static VFolders.Libs.VUtils; using static VFolders.Libs.VGUI; namespace VFolders { public static class VFolders { static void RowGUI(string guid, Rect rowRect) { var fullRowRect = rowRect.AddWidthFromRight(rowRect.x); var isRowHovered = fullRowRect.IsHovered(); var isListArea = rowRect.x == 14; var isFolder = AssetDatabase.IsValidFolder(guid.ToPath()); var isAsset = !isFolder && !guid.IsNullOrEmpty(); var isFavorite = !isFolder && !isAsset && rowRect.x != 16; var isFavoritesRoot = rowRect.x == 16 && !isFolder && rowRect.y == 0; var isAssetsRoot = rowRect.x == 16 && isFolder && guid.ToPath() == "Assets"; var isPackagesRoot = rowRect.x == 16 && !isFavoritesRoot && !isAssetsRoot && guid.IsNullOrEmpty(); var useClearerRows = VFoldersMenu.clearerRowsEnabled && !isListArea; var useMinimalMode = VFoldersMenu.minimalModeEnabled && !isListArea; var useContentMinimap = VFoldersMenu.contentMinimapEnabled && !isListArea; var useHierarchyLines = VFoldersMenu.hierarchyLinesEnabled && !isListArea; var useZebraStriping = VFoldersMenu.zebraStripingEnabled && !isListArea; EditorWindow browser = null; Tree tree = null; TreeViewItem treeItem = null; var isTreeFocused = false; var isRowSelected = false; var isRowBeingRenamed = false; void setObjects() { if (isListArea) return; if (!curEvent.isRepaint) // only needed for drawing, if (!curEvent.isMouseUp) // altClick and if (!isRowHovered) // setting last hovered tree item return; void set_browser() { var pointInsideWindow = EditorGUIUtility.GUIToScreenPoint(rowRect.center); browser = allProjectBrowsers.FirstOrDefault(r => r.position.AddHeight(30).Contains(pointInsideWindow) && r.hasFocus); } void set_tree() { if (browser == null) return; tree = GetTree(browser); } void set_treeItem_byRect() { if (tree == null) return; if (tree.animatingExpansion) return; var offest = tree.isTwoColumns ? -15 : -4; if ((rowRect.y + offest) % 16 != 0) return; var rowIndex = ((rowRect.y + offest) / 16).ToInt(); if (rowIndex < 0) return; if (tree.data == null) return; treeItem = (TreeViewItem)Tree.mi_data_GetItem.Invoke(tree.data, new object[] { rowIndex }); } void set_treeItem_byInstanceId() { if (tree == null) return; if (treeItem != null) return; if (isFavorite || isFavoritesRoot) return; var instanceId = typeof(AssetDatabase).InvokeMethod("GetMainAssetOrInProgressProxyInstanceID", guid.ToPath()); treeItem = tree.treeViewController.InvokeMethod("FindItem", instanceId); } set_browser(); set_tree(); set_treeItem_byRect(); set_treeItem_byInstanceId(); } void setState() { void set_isTreeFocused() { if (!curEvent.isRepaint) return; if (tree?.treeViewController == null) return; if (isListArea) return; isTreeFocused = EditorWindow.focusedWindow == browser && GUIUtility.keyboardControl == tree.treeViewController.GetFieldValue("m_KeyboardControlID"); } void set_isRowSelected_oneColumn() { if (!curEvent.isRepaint) return; if (isListArea) return; if (tree == null) return; if (treeItem == null) return; if (tree.isTwoColumns) return; #if UNITY_2021_1_OR_NEWER var dragSelectionList = tree.treeViewController?.GetFieldValue("m_DragSelection")?.GetFieldValue>("m_List"); #else var dragSelectionList = tree.treeViewController?.GetFieldValue>("m_DragSelection"); #endif var dragging = dragSelectionList != null && dragSelectionList.Any(); isRowSelected = dragging ? (dragSelectionList.Contains(treeItem.id)) : Selection.Contains(treeItem.id); } void set_isRowSelected_TwoColumns() { if (!curEvent.isRepaint) return; if (isListArea) return; if (tree == null) return; if (treeItem == null) return; if (!tree.isTwoColumns) return; #if UNITY_2021_1_OR_NEWER var dragSelectionList = tree.treeViewController?.GetFieldValue("m_DragSelection")?.GetFieldValue>("m_List"); var normalSelectionList = tree.treeViewController?.GetFieldValue("m_CachedSelection")?.GetFieldValue>("m_List"); #else var dragSelectionList = tree.treeViewController?.GetFieldValue>("m_DragSelection"); var normalSelectionList = tree.treeViewController?.GetMemberValue("state").GetMemberValue>("selectedIDs"); #endif var dragging = dragSelectionList != null && dragSelectionList.Any(); isRowSelected = dragging ? (dragSelectionList.Contains(treeItem.id)) : normalSelectionList != null && normalSelectionList.Contains(treeItem.id); } void set_isRowBeingRenamed() { if (!curEvent.isRepaint) return; if (isListArea) return; isRowBeingRenamed = EditorGUIUtility.editingTextField && isRowSelected && tree?.treeViewController?.GetMemberValue("state")?.GetMemberValue("renameOverlay")?.InvokeMethod("IsRenaming") == true; } void set_lastHovered() { if (isListArea) return; if (!curEvent.isRepaint) return; if (!isRowHovered) return; lastHoveredRowRect_screenSpace = EditorGUIUtility.GUIToScreenRect(fullRowRect); lastHoveredTreeItem = treeItem; } set_isTreeFocused(); set_isRowSelected_oneColumn(); set_isRowSelected_TwoColumns(); set_isRowBeingRenamed(); set_lastHovered(); lastKnownMousePosition_screenSpace = curEvent.mousePosition_screenSpace; } void drawing() { if (!curEvent.isRepaint) { hierarchyLines_isFirstRowDrawn = false; return; } var folderInfo = GetFolderInfo(guid, createDataIfDoesntExist: false); var showBackgroundColor = folderInfo.hasColor && useClearerRows;// && !(isRowSelected && isTreeFocused); var showCustomIcon = useClearerRows ? folderInfo.hasIcon : (folderInfo.hasIcon || folderInfo.hasColor); var showDefaultIcon = !showCustomIcon && (!useMinimalMode || isRowBeingRenamed); var makeTriangleBrighter = showBackgroundColor && isDarkTheme; var makeNameBrighter = showBackgroundColor && isDarkTheme; var makeIconBrighter = showBackgroundColor; var hideProperly = showBackgroundColor || (!showCustomIcon && !showDefaultIcon); Color defaultBackground = default; void calcDefaultBackground() { if (!isFolder && !isFavoritesRoot && !isPackagesRoot) return; if (!hideProperly) return; var selectedFocused = GUIColors.selectedBackground; var selectedUnfocused = isDarkTheme ? Greyscale(.3f) : Greyscale(.68f); var normal = isListArea ? Greyscale(.2f) : GUIColors.windowBackground; if (isRowSelected && !isRowBeingRenamed) defaultBackground = isTreeFocused ? selectedFocused : selectedUnfocused; else defaultBackground = normal; } void hideName_proper() { if (!showBackgroundColor && (showCustomIcon || showDefaultIcon)) return; if (!isFolder && !isFavoritesRoot && !isPackagesRoot) return; if (!hideProperly) return; var name = isListArea ? guid.ToPath().GetFilename() : treeItem != null ? treeItem.displayName : "Favorites"; var nameRect = rowRect.SetWidth(name.GetLabelWidth() + 3).MoveX(16).MoveX(isListArea ? 3 : 0); nameRect.Draw(defaultBackground); } void hideDefaultIcon_proper() { if (showDefaultIcon) return; if (!isFolder && !isFavoritesRoot && !isPackagesRoot) return; if (!hideProperly) return; var iconRect = rowRect.SetWidth(16).MoveX(isListArea ? 3 : 0); iconRect.Draw(defaultBackground); } void hideDefaultIcon_fast() { if (showDefaultIcon) return; if (hideProperly) return; var iconRect = rowRect.SetWidth(16).MoveX(isListArea ? 3 : 0); SetGUIColor(EditorGUIUtility.isProSkin ? Greyscale(.28f) : Color.white); GUI.DrawTexture(iconRect, EditorGUIUtility.FindTexture("d_Folder Icon")); GUI.DrawTexture(iconRect, EditorGUIUtility.FindTexture("d_FolderOpened Icon")); ResetGUIColor(); } void backgroundColor() { if (!showBackgroundColor) return; if (!isFolder && !isFavoritesRoot && !isPackagesRoot) return; var hasLeftGradient = rowRect.x > 32; var colorRect = rowRect.AddWidthFromRight(30).AddWidth(16); if (!isRowSelected) colorRect = colorRect.AddHeightFromMid(EditorGUIUtility.pixelsPerPoint >= 2 ? -.5f : -1); if (hasLeftGradient) colorRect = colorRect.AddWidthFromRight(2); var leftGradientWith = hasLeftGradient ? 22 : 0; var rightGradientWidth = (fullRowRect.width * .77f).Min(colorRect.width); var leftGradientRect = colorRect.SetWidth(leftGradientWith); var rightGradientRect = colorRect.SetWidthFromRight(rightGradientWidth - leftGradientWith); var flatColorRect = colorRect.SetX(leftGradientRect.xMax).SetXMax(rightGradientRect.x); var colorWithFlatness = folderInfo.color; var flatness = colorWithFlatness.a; var color = (isDarkTheme ? colorWithFlatness * .64f : Color.Lerp(colorWithFlatness, Greyscale(.8f), .5f)).SetAlpha(1); if (isRowHovered) color *= 1.1f; if (isRowSelected) color *= 1.2f; leftGradientRect.Draw(color.SetAlpha((flatness - .1f) / .9f)); leftGradientRect.DrawCurtainLeft(color); flatColorRect.AddWidth(1).Draw(color); rightGradientRect.Draw(color.MultiplyAlpha(flatness)); rightGradientRect.DrawCurtainRight(color); } void triangle() { if (!showBackgroundColor) return; if (treeItem == null) return; if (!treeItem.hasChildren) return; if (!isFolder && !isFavoritesRoot && !isPackagesRoot) return; var triangleRect = rowRect.MoveX(-15).SetWidth(16).Resize(-1); GUI.Label(triangleRect, EditorGUIUtility.IconContent(tree.IsExpanded(treeItem) ? "IN_foldout_on" : "IN_foldout")); if (!makeTriangleBrighter) return; GUI.Label(triangleRect, EditorGUIUtility.IconContent(tree.IsExpanded(treeItem) ? "IN_foldout_on" : "IN_foldout")); } void name() { if (!showBackgroundColor && (showCustomIcon || showDefaultIcon)) return; if (!isFolder && !isFavoritesRoot && !isPackagesRoot) return; if (isRowBeingRenamed) return; var nameRect = rowRect.MoveX(18); if (isListArea) nameRect = nameRect.MoveX(3); if (useMinimalMode && !showCustomIcon && !showDefaultIcon) nameRect = nameRect.MoveX(-17); if (makeNameBrighter) nameRect = nameRect.MoveX(-2).MoveY(-.5f); var styleName = makeNameBrighter ? "WhiteLabel" : "TV Line"; if (isFavoritesRoot || isAssetsRoot || isPackagesRoot) styleName = "BoldLabel"; var name = isFavoritesRoot ? "Favorites" : isPackagesRoot ? "Packages" : isListArea || treeItem == null ? guid.ToPath().GetFilename() : treeItem.displayName; if (makeNameBrighter) SetGUIColor(Greyscale(isRowSelected ? 1 : .93f)); GUI.skin.GetStyle(styleName).Draw(nameRect, name, false, false, isRowSelected, isTreeFocused && styleName != "BoldLabel"); if (makeNameBrighter) ResetGUIColor(); } void defaultIcon() { if (!isFolder) return; if (!showBackgroundColor) return; if (!showDefaultIcon) return; var iconRect = rowRect.SetWidth(16).MoveX(isListArea ? 3 : 0); SetLabelAlignmentCenter(); if (makeNameBrighter) SetGUIColor(Greyscale(.88f)); if (makeNameBrighter) GUI.Label(iconRect.Resize(-2), EditorGUIUtility.IconContent(folderInfo.isEmpty ? "FolderEmpty On Icon" : "Folder On Icon")); else GUI.Label(iconRect.Resize(-2), EditorGUIUtility.IconContent(folderInfo.isEmpty ? "FolderEmpty Icon" : "Folder Icon")); if (makeNameBrighter) ResetGUIColor(); ResetLabelStyle(); } void customIcon() { if (!isFolder) return; if (!showCustomIcon) return; var icon = useClearerRows ? EditorIcons.GetIcon(folderInfo.iconNameOrPath) : GetSmallFolderIcon(folderInfo); if (!icon) return; var iconRect = rowRect.SetWidth(16).MoveX(isListArea ? 3 : 0); iconRect = iconRect.SetWidth(iconRect.height / icon.height * icon.width); GUI.DrawTexture(iconRect, icon); } void hierarchyLines() { if (!useHierarchyLines) return; if (isListArea) return; if (treeItem == null) return; var lineThickness = 1f; var lineContrast = isDarkTheme ? .35f : .55f; if (isRowSelected) if (isTreeFocused) lineContrast += isDarkTheme ? .1f : -.25f; else lineContrast += isDarkTheme ? .05f : -.05f; var depth = ((rowRect.x - 16) / 14).RoundToInt(); bool isLastChild(TreeViewItem item) => item.parent?.children?.LastOrDefault() == item; bool hasChilren(TreeViewItem item) => item.children != null && item.children.Count > 0; void calcVerticalGaps_beforeFirstRowDrawn() { if (hierarchyLines_isFirstRowDrawn) return; hierarchyLines_verticalGaps.Clear(); var curItem = treeItem.parent; var curDepth = depth - 1; while (curItem != null && curItem.parent != null) { if (isLastChild(curItem)) hierarchyLines_verticalGaps.Add(curDepth - 1); curItem = curItem.parent; curDepth--; } } void updateVerticalGaps_beforeNextRowDrawn() { if (isLastChild(treeItem)) hierarchyLines_verticalGaps.Add(depth - 1); if (depth < hierarchyLines_prevRowDepth) hierarchyLines_verticalGaps.RemoveAll(r => r >= depth); } void drawVerticals() { for (int i = 1; i < depth; i++) if (!hierarchyLines_verticalGaps.Contains(i)) rowRect.SetX(9 + i * 14 - lineThickness / 2) .SetWidth(lineThickness) .SetHeight(isLastChild(treeItem) && i == depth - 1 ? 8 + lineThickness / 2 : 16) .Draw(Greyscale(lineContrast)); } void drawHorizontals() { if (depth == 0) return; if (depth == 1) return; rowRect.MoveX(-21) .SetHeightFromMid(lineThickness) .SetWidth(hasChilren(treeItem) ? 7 : 17) .Draw(Greyscale(lineContrast)); } calcVerticalGaps_beforeFirstRowDrawn(); drawVerticals(); drawHorizontals(); updateVerticalGaps_beforeNextRowDrawn(); hierarchyLines_prevRowDepth = depth; hierarchyLines_isFirstRowDrawn = true; } void zebraStriping() { if (!useZebraStriping) return; var contrast = isDarkTheme ? .033f : .05f; var t = 1 - rowRect.y.PingPong(16f) / 16f; fullRowRect.Draw(Greyscale(isDarkTheme ? 1 : 0, contrast * t)); } void hoveredRow() { if (!isRowHovered) return; if (isListArea) return; if (EditorWindow.mouseOverWindow == VFoldersPaletteWindow.instance) return; fullRowRect.Draw(Greyscale(isDarkTheme ? 1 : 0, .06f)); } void contentMinimap() { if (!isFolder) return; if (!useContentMinimap) return; if (guid.IsNullOrEmpty()) return; void icon(Rect rect, string name) { var icon = EditorIcons.GetIcon(name); if (!icon) return; SetGUIColor(Greyscale(1, isDarkTheme ? .49f : .7f)); GUI.DrawTexture(rect, icon); ResetGUIColor(); } var iconDistance = 13; var minButtonX = rowRect.x + guid.ToPath().GetFilename().GetLabelWidth() + iconDistance + 2; var iconRect = fullRowRect.SetWidthFromRight(iconDistance).SetSizeFromMid(12, 12).MoveX(-1.5f); foreach (var iconName in folderInfo.folderState.contentMinimapIconNames) { if (iconRect.x < minButtonX) continue; icon(iconRect, iconName); iconRect = iconRect.MoveX(-iconDistance); } } fullRowRect.MarkInteractive(); calcDefaultBackground(); hideName_proper(); hideDefaultIcon_proper(); hideDefaultIcon_fast(); hierarchyLines(); backgroundColor(); triangle(); defaultIcon(); customIcon(); name(); zebraStriping(); hoveredRow(); contentMinimap(); } void altDrag() { if (!curEvent.holdingAlt) return; void mouseDown() { if (!curEvent.isMouseDown) return; if (!isRowHovered) return; mouseDownPos = curEvent.mousePosition; } void mouseDrag() { if (!curEvent.isMouseDrag) return; if ((curEvent.mousePosition - mouseDownPos).magnitude < 5) return; if (!rowRect.Contains(mouseDownPos)) return; if (!rowRect.Contains(curEvent.mousePosition - curEvent.mouseDelta)) return; if (DragAndDrop.objectReferences.Any()) return; DragAndDrop.PrepareStartDrag(); DragAndDrop.objectReferences = new[] { AssetDatabase.LoadAssetAtPath(guid.ToPath()) }; DragAndDrop.StartDrag(guid.ToPath().GetFilename()); } mouseDown(); mouseDrag(); // altdrag has to be set up manually before altClick because altClick will use() mouseDown event to prevent selection change } void altClick() { if (!isRowHovered) return; if (!curEvent.holdingAlt) return; if (!isFolder) return; void mouseDown() { if (!curEvent.isMouseDown) return; curEvent.Use(); } void mouseUp() { if (!curEvent.isMouseUp) return; var selectedGuids = isListArea ? Selection.objects.Where(r => r is DefaultAsset).Select(r => r.GetPath().ToGuid()) : #if UNITY_2021_1_OR_NEWER tree.treeViewController.GetFieldValue("m_CachedSelection").GetFieldValue>("m_List") #else tree.treeViewController?.GetMemberValue("state").GetMemberValue>("selectedIDs") #endif .Select(id => tree.treeViewController.InvokeMethod("FindItem", id) // todo to somethinf else (finditem reveals selected item for some reason) .GetPropertyValue("Guid", exceptionIfNotFound: false)) .Where(r => r != null); var editMultiSelection = selectedGuids.Count() > 1 && selectedGuids.Contains(guid); var guidsToEdit = (editMultiSelection ? selectedGuids.Where(r => AssetDatabase.IsValidFolder(r.ToPath())) : new[] { guid }).ToList(); if (VFoldersPaletteWindow.instance && VFoldersPaletteWindow.instance.guids.SequenceEqual(guidsToEdit)) { VFoldersPaletteWindow.instance.Close(); return; } var openNearRect = rowRect;// editMultiSelection ? lastVisibleSelectedCellRect : cellRect; var position = EditorGUIUtility.GUIToScreenPoint(new Vector2(curEvent.mousePosition.x + 20, openNearRect.y - 13)); if (!VFoldersPaletteWindow.instance) VFoldersPaletteWindow.CreateInstance(position); VFoldersPaletteWindow.instance.Init(guidsToEdit); VFoldersPaletteWindow.instance.Focus(); VFoldersPaletteWindow.instance.targetPosition = position; if (editMultiSelection) Selection.objects = null; } mouseDown(); mouseUp(); } setObjects(); setState(); drawing(); altDrag(); altClick(); } static List hierarchyLines_verticalGaps = new List(); static bool hierarchyLines_isFirstRowDrawn; static int hierarchyLines_prevRowDepth; static Vector2 lastKnownMousePosition_screenSpace; static Rect lastHoveredRowRect_screenSpace; static TreeViewItem lastHoveredTreeItem; static void CellGUI(string guid, Rect cellRect) { if (!AssetDatabase.IsValidFolder(guid.ToPath())) return; var folderInfo = GetFolderInfo(guid, createDataIfDoesntExist: false); void setLastVisibleSelectedForAltClick() { if (!curEvent.isRepaint) return; if (!Selection.objects.Contains(AssetDatabase.LoadAssetAtPath(guid.ToPath()))) return; lastVisibleSelectedCellRect = cellRect; } void hideIcon() { if (!curEvent.isRepaint) return; if (!folderInfo.hasColor) return; cellRect.SetHeight(cellRect.width).Resize(4).Draw(EditorGUIUtility.isProSkin ? Greyscale(.2f) : Greyscale(.75f)); } void icon() { if (!curEvent.isRepaint) return; if (!folderInfo.hasColor && !folderInfo.hasIcon) return; DrawBigFolderIcon(cellRect, folderInfo); } void altDrag() { if (!curEvent.holdingAlt) return; void mouseDown() { if (!curEvent.isMouseDown) return; if (!cellRect.IsHovered()) return; mouseDownPos = curEvent.mousePosition; } void mouseDrag() { if (!curEvent.isMouseDrag) return; if ((curEvent.mousePosition - mouseDownPos).magnitude < 5) return; if (!cellRect.Contains(mouseDownPos)) return; if (!cellRect.Contains(curEvent.mousePosition - curEvent.mouseDelta)) return; if (DragAndDrop.objectReferences.Any()) return; DragAndDrop.PrepareStartDrag(); DragAndDrop.objectReferences = new[] { AssetDatabase.LoadAssetAtPath(guid.ToPath()) }; DragAndDrop.StartDrag(guid.ToPath().GetFilename()); } mouseDown(); mouseDrag(); } void altClick() { if (!cellRect.IsHovered()) return; if (!curEvent.holdingAlt) return; void mouseDown() { if (!curEvent.isMouseDown) return; curEvent.Use(); } void mouseUp() { if (!curEvent.isMouseUp) return; var selectedFoldersGuids = Selection.objects.Where(r => r is DefaultAsset).Select(r => r.GetPath().ToGuid()); var editMultiSelection = selectedFoldersGuids.Count() > 1 && selectedFoldersGuids.Contains(guid); var guidsToEdit = (editMultiSelection ? selectedFoldersGuids : new[] { guid }).ToList(); if (VFoldersPaletteWindow.instance && VFoldersPaletteWindow.instance.guids.SequenceEqual(guidsToEdit)) { VFoldersPaletteWindow.instance.Close(); return; } var openNearRect = editMultiSelection ? lastVisibleSelectedCellRect : cellRect; var position = EditorGUIUtility.GUIToScreenPoint(new Vector2(openNearRect.xMax + 8, openNearRect.y - 5)); if (!VFoldersPaletteWindow.instance) VFoldersPaletteWindow.CreateInstance(position); VFoldersPaletteWindow.instance.Init(guidsToEdit); VFoldersPaletteWindow.instance.Focus(); VFoldersPaletteWindow.instance.targetPosition = position; if (editMultiSelection) Selection.objects = null; } mouseDown(); mouseUp(); } setLastVisibleSelectedForAltClick(); hideIcon(); icon(); altDrag(); altClick(); } static Rect lastVisibleSelectedCellRect; static void ProjectBrowserItemGUI(string guid, Rect itemRect) { void callRowGUI() { if (itemRect.height != 16) return; RowGUI(guid, itemRect); } void callCellGUI() { if (itemRect.height == 16) return; CellGUI(guid, itemRect); } void updateExpandQueues() { if (!curEvent.isLayout) return; foreach (var tree in trees_byBrowser.Values) tree.UpdateExpandQueue(); } void createFoldersFirstUpdater() { if (!curEvent.isLayout) return; if (!VFoldersMenu.foldersFirstEnabled) return; var pointInsideWindow = EditorGUIUtility.GUIToScreenPoint(itemRect.center); var browser = allProjectBrowsers.FirstOrDefault(r => r.position.AddHeight(30).Contains(pointInsideWindow) && r.hasFocus); if (browser == null) return; if (foldersFirstUpdater_byBrowser.ContainsKey(browser)) return; var updater = new FoldersFirstUpdater(browser); foldersFirstUpdater_byBrowser[browser] = updater; updater.Update(); } callRowGUI(); callCellGUI(); updateExpandQueues(); createFoldersFirstUpdater(); } static Vector2 mouseDownPos; public static Texture2D GetSmallFolderIcon(FolderInfo folderInfo) { var key = new object[] { folderInfo.iconNameOrPath, folderInfo.color, folderInfo.isEmpty, isDarkTheme }.Aggregate(0, (hash, r) => (hash * 2) ^ r.GetHashCode()); Texture2D icon = null; void getCached() { if (!cache.HasIcon(key)) return; icon = cache.GetIcon(key); } void generateAndCache() { if (icon != null) return; if (Event.current != null) return; // interactions with gpu in OnGUI may interfere with gui rendering var iconSizeX = folderInfo.hasIcon ? 36 : 32; var iconSizeY = 32; var assetIconSize = 20; // 20 21 var assetIconOffsetX = 16; var assetIconOffsetY = -2; // -2 -3 var folderIconSize = iconSizeY; var folderIconOffsetY_ifHasAssetIcon = 1; Color[] iconPixels; Texture2D folderIcon; Color[] folderIconPixels; void createIcon() { icon = new Texture2D(iconSizeX, iconSizeY, TextureFormat.RGBA32, 1, false); icon.hideFlags = HideFlags.DontSave; icon.SetPropertyValue("pixelsPerPoint", 2); iconPixels = new Color[iconSizeX * iconSizeY]; } void createFolderIcon() { // var folderIconName = folderInfo.isEmpty ? "FolderEmpty Icon" : "Project@2x"; var folderIconName = folderInfo.hasColor ? (folderInfo.isEmpty ? "FolderEmpty On Icon" : "Folder On Icon") : (folderInfo.isEmpty ? "FolderEmpty Icon" : "Folder Icon"); folderIcon = EditorGUIUtility.FindTexture(folderIconName); if (folderIcon.width != folderIconSize) folderIcon = folderIcon.CreateResizedCopy(folderIconSize, folderIconSize); else folderIcon = folderIcon.CreateCopy(); folderIconPixels = folderIcon.GetPixels(0); } void copyFolderIcon() { if (!folderInfo.hasIcon) { iconPixels = folderIconPixels; return; } for (int x = 0; x < folderIcon.width; x++) for (int y = 0; y < folderIcon.height - folderIconOffsetY_ifHasAssetIcon; y++) iconPixels[x + (y + folderIconOffsetY_ifHasAssetIcon) * icon.width] = folderIconPixels[x + y * folderIcon.width]; } void applyColor() { if (!folderInfo.hasColor) return; for (int i = 0; i < iconPixels.Length; i++) iconPixels[i] *= folderInfo.color.SetAlpha(1); } void insertAssetIcon() { if (!folderInfo.hasIcon) return; var assetIconOriginal = EditorIcons.GetIcon(folderInfo.iconNameOrPath); if (!assetIconOriginal) return; var prevFilter = assetIconOriginal.filterMode; assetIconOriginal.filterMode = FilterMode.Bilinear; var assetIconPixels_bilinear = assetIconOriginal.CreateResizedCopy(assetIconSize, assetIconSize).GetPixels(); assetIconOriginal.filterMode = FilterMode.Point; var assetIconPixels_point = assetIconOriginal.CreateResizedCopy(assetIconSize, assetIconSize).GetPixels(); assetIconOriginal.filterMode = prevFilter; for (int x = 0; x < iconSizeX; x++) for (int y = 0; y < iconSizeY; y++) { var xAssetIcon = x - assetIconOffsetX; var yAssetIcon = y - assetIconOffsetY - folderIconOffsetY_ifHasAssetIcon; if (!xAssetIcon.IsInRange(0, assetIconSize - 1)) continue; if (!yAssetIcon.IsInRange(0, assetIconSize - 1)) continue; var innerRadius = (folderInfo.iconNameOrPath == "AudioClip Icon" ? .2f : .4f); var isInnerPixel = (new Vector2(xAssetIcon, yAssetIcon) / (assetIconSize - 1) - Vector2.one * .5f).magnitude < innerRadius; var isOutlinePixel = false; var outlineRadius = isInnerPixel ? 2 : 1; for (int xx = xAssetIcon - outlineRadius; xx <= xAssetIcon + outlineRadius; xx++) if (!isOutlinePixel) for (int yy = yAssetIcon - outlineRadius; yy <= yAssetIcon + outlineRadius; yy++) if (!isOutlinePixel) if (xx.IsInRange(0, assetIconSize - 1) && yy.IsInRange(0, assetIconSize - 1)) if (assetIconPixels_bilinear[xx + yy * assetIconSize].a > .2f) isOutlinePixel = true; var pxBilinear = assetIconPixels_bilinear[xAssetIcon + yAssetIcon * assetIconSize]; var pxPoint = assetIconPixels_point[xAssetIcon + yAssetIcon * assetIconSize]; var pxCombined = new Color(pxPoint.r, pxPoint.g, pxPoint.b, pxBilinear.a); if (pxCombined.a == 0 && !isOutlinePixel) continue; iconPixels[x + y * iconSizeX] = pxCombined; } } createIcon(); createFolderIcon(); copyFolderIcon(); applyColor(); insertAssetIcon(); icon.SetPixels(iconPixels); icon.Apply(); cache.AddIcon(key, icon); } void queueGeneration() { if (icon != null) return; toCallInUpdate.Add(generateAndCache); } getCached(); generateAndCache(); queueGeneration(); return icon ?? EditorGUIUtility.FindTexture(folderInfo.isEmpty ? "FolderEmpty Icon" : "Project@2x"); } public static Texture2D GetSmallFolderIcon_forVTabs(string folderGuid) { var folderInfo = GetFolderInfo(folderGuid, false); if (folderInfo.hasColor || folderInfo.hasIcon) return GetSmallFolderIcon(folderInfo); return null; } static void Update() { foreach (var r in toCallInUpdate) r.Invoke(); toCallInUpdate.Clear(); } static List toCallInUpdate = new List(); public static void DrawBigFolderIcon(Rect rect, FolderInfo folderInfo) { Rect folderIconRect; Rect assetIconRect; void calcRects() { folderIconRect = rect.SetHeight(rect.width); #if UNITY_2022_3_OR_NEWER #else if (Application.platform == RuntimePlatform.OSXEditor) if (folderIconRect.width > 64) folderIconRect = folderIconRect.SetSizeFromMid(64, 64); #endif var assetIconOffsetMin = new Vector2(4.5f, 3.5f); var assetIconSizeMin = 10; var assetIconOffsetMax = new Vector2(19, 15); var assetIconSizeMax = 24.5f; // 25 var t = ((folderIconRect.width - 16) / (64 - 16)); #if UNITY_2022_3_OR_NEWER #else if (Application.platform == RuntimePlatform.OSXEditor) t = t.Clamp01(); #endif var assetIconOffset = Lerp(assetIconOffsetMin, assetIconOffsetMax, t); var assetIconSize = Lerp(assetIconSizeMin, assetIconSizeMax, t); assetIconRect = folderIconRect.Move(assetIconOffset).SetSizeFromMid(assetIconSize, assetIconSize).AlignToPixelGrid(); } void color() { if (!folderInfo.hasColor) return; SetGUIColor(folderInfo.color.SetAlpha(1)); GUI.DrawTexture(folderIconRect, EditorGUIUtility.FindTexture(folderInfo.isEmpty ? "FolderEmpty On Icon" : "Folder On Icon")); ResetGUIColor(); } void assetIcon() { if (!folderInfo.hasIcon) return; var texture = EditorIcons.GetIcon(folderInfo.iconNameOrPath); if (!texture) return; void material() { if (outlineMaterial) return; outlineMaterial = new Material(Shader.Find("Hidden/VFoldersOutline")); outlineMaterial.SetColor("_Color", EditorGUIUtility.isProSkin ? Greyscale(.2f, .6f) : Greyscale(.75f)); } void shadow() { var contrast = isDarkTheme ? .6f : .2f; // was .65 then .6 assetIconRect.SetSizeFromMid(assetIconRect.width * .8f).DrawBlurred(Greyscale(.2f, contrast), assetIconRect.width * .55f); } void outline() { var outlineRect = assetIconRect.Resize(rect.height >= 70 && EditorGUIUtility.pixelsPerPoint >= 2 ? -1f / EditorGUIUtility.pixelsPerPoint : 0).AlignToPixelGrid(); EditorGUI.DrawPreviewTexture(outlineRect.Move(-1, -1), texture, outlineMaterial); EditorGUI.DrawPreviewTexture(outlineRect.Move(-1, 1), texture, outlineMaterial); EditorGUI.DrawPreviewTexture(outlineRect.Move(1, 1), texture, outlineMaterial); EditorGUI.DrawPreviewTexture(outlineRect.Move(1, -1), texture, outlineMaterial); } void background() { for (int i = 0; i < assetIconRect.size.x; i++) EditorGUI.DrawPreviewTexture(assetIconRect.Resize(i * .5f + 1), texture, outlineMaterial); } void icon() { GUI.DrawTexture(assetIconRect, texture); } material(); shadow(); outline(); background(); icon(); } calcRects(); color(); assetIcon(); } public static void DrawBigFolderIcon_forVFavorites(Rect rect, string folderGuid) { DrawBigFolderIcon(rect, GetFolderInfo(folderGuid, false)); } static Material outlineMaterial; static void CheckShortcuts() // called from globalEventHandler { if (!hoveredProjectBrowser) return; if (!curEvent.isKeyDown) return; if (curEvent.keyCode == KeyCode.None) return; if (EditorWindow.mouseOverWindow.GetType() != t_ProjectBrowser) return; void toggleExpanded() { if (!curEvent.isKeyDown) return; if (curEvent.keyCode != KeyCode.E) return; if (curEvent.holdingAnyModifierKey) return; if (!VFoldersMenu.toggleExpandedEnabled) return; if (lastHoveredTreeItem == null) return; if (!lastHoveredRowRect_screenSpace.Contains(lastKnownMousePosition_screenSpace)) return; curEvent.Use(); if (lastHoveredTreeItem.children == null) return; if (lastHoveredTreeItem.children.Count == 0) return; var tree = GetTree(hoveredProjectBrowser); tree.SetExpandedWithAnimation(lastHoveredTreeItem.id, !tree.expandedIds.Contains(lastHoveredTreeItem.id)); EditorApplication.RepaintProjectWindow(); } void collapseEverything() { if (!curEvent.isKeyDown) return; if (curEvent.keyCode != KeyCode.E) return; if (curEvent.modifiers != (EventModifiers.Shift | EventModifiers.Command) && curEvent.modifiers != (EventModifiers.Shift | EventModifiers.Control)) return; if (!VFoldersMenu.collapseEverythingEnabled) return; curEvent.Use(); var tree = GetTree(hoveredProjectBrowser); var idsToCollapse_roots = tree.expandedIds.Where(id => EditorUtility.InstanceIDToObject(id).GetPath() is string path && path.HasParentPath() && (path.GetParentPath() == "Assets" || path.GetParentPath() == "Packages")); var idsToCollapse_children = tree.expandedIds.Where(id => EditorUtility.InstanceIDToObject(id).GetPath() is string path && !path.IsNullOrEmpty() && path != "Assets" && path != "Packages" && !idsToCollapse_roots.Contains(id)); tree.expandQueue_toCollapseAfterAnimation = idsToCollapse_children.ToList(); tree.expandQueue_toAnimate = idsToCollapse_roots.Select(id => new Tree.ExpandQueueEntry { id = id, expand = false }) .OrderBy(row => tree.VisibleRowIndex(row.id)).ToList(); EditorApplication.RepaintProjectWindow(); } void collapseEverythingElse() { if (!curEvent.isKeyDown) return; if (curEvent.keyCode != KeyCode.E) return; if (curEvent.modifiers != EventModifiers.Shift) return; if (!VFoldersMenu.collapseEverythingElseEnabled) return; if (lastHoveredTreeItem == null) return; if (!lastHoveredRowRect_screenSpace.Contains(lastKnownMousePosition_screenSpace)) return; curEvent.Use(); if (lastHoveredTreeItem.children == null) return; if (lastHoveredTreeItem.children.Count == 0) return; var tree = GetTree(hoveredProjectBrowser); var parents = new List(); for (var cur = lastHoveredTreeItem.parent; cur != null; cur = cur.parent) parents.Add(cur); var idsToCollapse = new List(); foreach (var id in tree.expandedIds.ToList()) if (!parents.Any(r => r.id == id)) if (id != lastHoveredTreeItem.id) idsToCollapse.Add(id); tree.expandQueue_toAnimate = idsToCollapse.Select(id => new Tree.ExpandQueueEntry { id = id, expand = false }) .Append(new Tree.ExpandQueueEntry { id = lastHoveredTreeItem.id, expand = true }) .OrderBy(r => tree.VisibleRowIndex(r.id)).ToList(); EditorApplication.RepaintProjectWindow(); } toggleExpanded(); collapseEverything(); collapseEverythingElse(); } class Tree { public void UpdateExpandQueue() // called from gui because reflected methods rely on event.current { if (animatingExpansion) return; if (!expandQueue_toAnimate.Any()) { if (!expandQueue_toCollapseAfterAnimation.Any()) return; foreach (var id in expandQueue_toCollapseAfterAnimation) SetExpanded(id, false); expandQueue_toCollapseAfterAnimation.Clear(); return; } var iid = expandQueue_toAnimate.First().id; var expand = expandQueue_toAnimate.First().expand; if (expandedIds.Contains(iid) != expand) SetExpandedWithAnimation(iid, expand); expandQueue_toAnimate.RemoveAt(0); } public void SetExpandedWithAnimation(int instanceId, bool expanded) => treeViewController.InvokeMethod("ChangeFoldingForSingleItem", instanceId, expanded); public void SetExpanded(int instanceId, bool expanded) => treeViewController.GetPropertyValue("data").InvokeMethod("SetExpanded", instanceId, expanded); public int VisibleRowIndex(int instanceId) => treeViewController.GetPropertyValue("data").InvokeMethod("GetRow", instanceId); public bool animatingExpansion => treeViewController.GetPropertyValue("animatingExpansion"); public bool IsExpanded(TreeViewItem treeItem) => expandedIds.Contains(treeItem.id); public List expandQueue_toAnimate = new List(); public List expandQueue_toCollapseAfterAnimation = new List(); public struct ExpandQueueEntry { public int id; public bool expand; } public void UpdateState() // delayCall loop { isTwoColumns = browser.GetFieldValue("m_ViewMode") == 1; treeViewController = browser.GetFieldValue(isTwoColumns ? "m_FolderTree" : "m_AssetTree"); data = treeViewController?.GetPropertyValue("data"); expandedIds = treeViewController?.GetPropertyValue("state")?.GetPropertyValue>("expandedIDs") ?? new List(); EditorApplication.delayCall -= UpdateState; EditorApplication.delayCall += UpdateState; } public bool isTwoColumns; public object treeViewController; public object data; public List expandedIds = new List(); public Tree(EditorWindow browser) => this.browser = browser; public EditorWindow browser; public static MethodInfo mi_data_GetItem = typeof(Editor).Assembly.GetType("UnityEditor.IMGUI.Controls.ITreeViewDataSource").GetMethod("GetItem", maxBindingFlags); } static Tree GetTree(EditorWindow browser) { if (trees_byBrowser.TryGetValue(browser, out var existingState)) return existingState; var tree = new Tree(browser); tree.UpdateState(); trees_byBrowser[browser] = tree; return tree; } static Dictionary trees_byBrowser = new Dictionary(); class FoldersFirstUpdater { public void Update() // delayCall loop { if (browser == null) { foldersFirstUpdater_byBrowser.Remove(browser); return; } var isTwoColumns = browser.GetFieldValue("m_ViewMode") == 1; void oneColumn() { if (isTwoColumns) return; if (initedForOneColumn) return; var m_AssetTree = browser.GetFieldValue("m_AssetTree"); if (m_AssetTree == null) return; m_AssetTree.GetPropertyValue("data").SetPropertyValue("foldersFirst", true); m_AssetTree.InvokeMethod("ReloadData"); initedForOneColumn = true; initedForTwoColumns = false; } void twoColumns() { if (!isTwoColumns) return; if (initedForTwoColumns) return; var m_ListArea = browser.GetFieldValue("m_ListArea"); if (m_ListArea == null) return; m_ListArea.SetPropertyValue("foldersFirst", true); browser.InvokeMethod("InitListArea"); initedForOneColumn = false; initedForTwoColumns = true; } oneColumn(); twoColumns(); EditorApplication.delayCall -= Update; EditorApplication.delayCall += Update; } bool initedForOneColumn; bool initedForTwoColumns; public FoldersFirstUpdater(EditorWindow browser) => this.browser = browser; public EditorWindow browser; } static Dictionary foldersFirstUpdater_byBrowser = new Dictionary(); public static FolderInfo GetFolderInfo(string guid, bool createDataIfDoesntExist) { var folderInfo = new FolderInfo(); folderInfo.folderData = GetFolderData(guid, createDataIfDoesntExist); folderInfo.folderState = GetFolderState(guid); return folderInfo; } public class FolderInfo { public string iconNameOrGuid { get { var iconNameOrGuid = folderData?.iconNameOrGuid; if (iconNameOrGuid == "" || iconNameOrGuid == null) iconNameOrGuid = VFoldersMenu.autoIconsEnabled ? folderState.autoIconName : ""; if (iconNameOrGuid == "none") iconNameOrGuid = ""; return iconNameOrGuid; } set { if (!VFoldersMenu.autoIconsEnabled) { folderData.iconNameOrGuid = value; return; } if (value == folderState.autoIconName) folderData.iconNameOrGuid = ""; else if (value == "") folderData.iconNameOrGuid = "none"; else folderData.iconNameOrGuid = value; // if (value == folderState.autoIconName && VFoldersMenu.autoIconsEnabled) // folderData.iconNameOrGuid = ""; // else if (value == "") // folderData.iconNameOrGuid = "none"; // else // folderData.iconNameOrGuid = value; } } public string iconNameOrPath => iconNameOrGuid.Length == 32 ? iconNameOrGuid.ToPath() : iconNameOrGuid; public Color color { get { if (!hasColor) return default; // return VFoldersPalette.GetDefaultColor(folderData.colorIndex - 1); if (palette) return palette.colors[folderData.colorIndex - 1]; else return VFoldersPalette.GetDefaultColor(folderData.colorIndex - 1); } } public bool hasIcon => (folderData != null && folderData.iconNameOrGuid != "" && folderData.iconNameOrGuid != "none") || (VFoldersMenu.autoIconsEnabled && folderState.autoIconName != "" && (folderData == null || folderData.iconNameOrGuid != "none")); public bool hasColor => folderData != null && folderData.colorIndex.IsInRange(1, VFoldersPalette.colorsCount); public bool isEmpty => folderState.isEmpty; public FolderData folderData; public FolderState folderState; } public static FolderData GetFolderData(string guid, bool createDataIfDoesntExist) { if (!data) return null; FolderData folderData = null; void fromScripableObject() { if (VFoldersData.storeDataInMetaFiles) return; data.folderDatas_byGuid.TryGetValue(guid, out folderData); if (folderData != null || !createDataIfDoesntExist) return; folderData = new FolderData(); data.folderDatas_byGuid[guid] = folderData; } void fromMetaFile() { if (!VFoldersData.storeDataInMetaFiles) return; folderDatasFromMetaFiles_byGuid.TryGetValue(guid, out folderData); if (folderData != null) return; var importer = AssetImporter.GetAtPath(guid.ToPath()); try { folderData = JsonUtility.FromJson(importer.userData); } catch { } folderDatasFromMetaFiles_byGuid[guid] = folderData; if (folderData != null || !createDataIfDoesntExist) return; folderData = new FolderData(); folderDatasFromMetaFiles_byGuid[guid] = folderData; } fromScripableObject(); fromMetaFile(); return folderData; } public static Dictionary folderDatasFromMetaFiles_byGuid = new Dictionary(); public static FolderState GetFolderState(string guid) { FolderState folderState = null; void getFromCache() { cache.folderStates_byGuid.TryGetValue(guid, out folderState); } void create() { if (folderState != null) return; folderState = new FolderState(); folderState.needsUpdate = true; cache.folderStates_byGuid[guid] = folderState; } void update() { if (!folderState.needsUpdate) return; if (!Directory.Exists(guid.ToPath())) { folderState.needsUpdate = false; return; } void isEmpty() { folderState.isEmpty = !Directory.EnumerateFileSystemEntries(guid.ToPath()).Any(); } void contentTypeNames() { var types = Directory.GetFiles(guid.ToPath(), "*.*") .Select(r => AssetDatabase.GetMainAssetTypeAtPath(r)) .Where(r => r != null); var iconNames = new List(); foreach (var type in types) if (type == typeof(Texture2D)) iconNames.Add("Texture Icon"); else if (type == typeof(GameObject)) iconNames.Add("Prefab Icon"); else if (type.BaseType == typeof(ScriptableObject) || type.BaseType?.BaseType == typeof(ScriptableObject)) iconNames.Add("ScriptableObject Icon"); // else if (type == typeof(MonoScript) || type == typeof(AssemblyDefinitionAsset) || type == typeof(AssemblyDefinitionReferenceAsset)) else if (type == typeof(MonoScript))// || type == typeof(AssemblyDefinitionAsset) || type == typeof(AssemblyDefinitionReferenceAsset)) iconNames.Add("cs Script Icon"); else if (AssetPreview.GetMiniTypeThumbnail(type)?.name is string iconName) iconNames.Add(iconName); folderState.contentMinimapIconNames = iconNames.Distinct().OrderBy(r => r).ToList(); } void autoIconName() { folderState.autoIconName = ""; var types = Directory.GetFiles(guid.ToPath(), "*.*") .Select(r => AssetDatabase.GetMainAssetTypeAtPath(r)) .Where(r => r != null); if (!types.Any()) return; if (!types.All(r => r == types.First())) return; var type = types.First(); if (type == typeof(SceneAsset)) folderState.autoIconName = "SceneAsset Icon"; else if (type == typeof(GameObject)) folderState.autoIconName = "Prefab Icon"; else if (type == typeof(Material)) folderState.autoIconName = "Material Icon"; else if (type == typeof(Texture)) folderState.autoIconName = "Texture Icon"; else if (type.BaseType == typeof(ScriptableObject)) folderState.autoIconName = "ScriptableObject Icon"; else if (type == typeof(TerrainData)) folderState.autoIconName = "TerrainData Icon"; else if (type == typeof(AudioClip)) folderState.autoIconName = "AudioClip Icon"; else if (type == typeof(Shader)) folderState.autoIconName = "Shader Icon"; else if (type == typeof(ComputeShader)) folderState.autoIconName = "ComputeShader Icon"; else if (type == typeof(MonoScript) || type == typeof(AssemblyDefinitionAsset) || type == typeof(AssemblyDefinitionReferenceAsset)) folderState.autoIconName = "cs Script Icon"; } isEmpty(); contentTypeNames(); autoIconName(); folderState.needsUpdate = false; } getFromCache(); create(); update(); return folderState; } class FolderStateChangeDetector : AssetPostprocessor { #if UNITY_2021_2_OR_NEWER static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths, bool didDomainReload) #else static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) #endif { foreach (var path in importedAssets.Concat(deletedAssets).Concat(movedAssets).Concat(movedFromAssetPaths)) if (path.HasParentPath()) if (cache.folderStates_byGuid.TryGetValue(path.GetParentPath().ToGuid(), out var folderState)) folderState.needsUpdate = true; } } public static VFoldersCache cache => VFoldersCache.instance; [InitializeOnLoadMethod] static void Init() { if (VFoldersMenu.pluginDisabled) return; void subscribe() { EditorApplication.projectWindowItemOnGUI -= ProjectBrowserItemGUI; EditorApplication.projectWindowItemOnGUI = ProjectBrowserItemGUI + EditorApplication.projectWindowItemOnGUI; EditorApplication.update -= Update; EditorApplication.update += Update; var globalEventHandler = typeof(EditorApplication).GetFieldValue("globalEventHandler"); typeof(EditorApplication).SetFieldValue("globalEventHandler", CheckShortcuts + (globalEventHandler - CheckShortcuts)); // EditorApplication.playModeStateChanged += (PlayModeStateChange obj) => UpdateFoldersFirst(); // EditorApplication.projectChanged += UpdateFoldersFirst; } void loadData() { data = AssetDatabase.LoadAssetAtPath(EditorPrefs.GetString("vFolders-lastKnownDataPath-" + GetProjectId())); if (data) return; data = AssetDatabase.FindAssets("t:VFoldersData").Select(guid => AssetDatabase.LoadAssetAtPath(guid.ToPath())).FirstOrDefault(); if (!data) return; EditorPrefs.SetString("vFolders-lastKnownDataPath-" + GetProjectId(), data.GetPath()); } void loadPalette() { palette = AssetDatabase.LoadAssetAtPath(EditorPrefs.GetString("vFolders-lastKnownPalettePath-" + GetProjectId())); if (palette) return; palette = AssetDatabase.FindAssets("t:VFoldersPalette").Select(guid => AssetDatabase.LoadAssetAtPath(guid.ToPath())).FirstOrDefault(); if (!palette) return; EditorPrefs.SetString("vFolders-lastKnownPalettePath-" + GetProjectId(), palette.GetPath()); } void loadDataAndPaletteDelayed() { if (!data) EditorApplication.delayCall += () => EditorApplication.delayCall += loadData; if (!palette) EditorApplication.delayCall += () => EditorApplication.delayCall += loadPalette; // AssetDatabase isn't up to date at this point (it gets updated after InitializeOnLoadMethod) // and if current AssetDatabase state doesn't contain the data - it won't be loaded during Init() // so here we schedule an additional, delayed attempt to load the data // this addresses reports of data loss when trying to load it on a new machine } void migrateDataFromV1() { if (!data) return; if (EditorPrefs.GetBool("vFolders-dataMigrationFromV1Attempted-" + GetProjectId(), false)) return; EditorPrefs.SetBool("vFolders-dataMigrationFromV1Attempted-" + GetProjectId(), true); var lines = System.IO.File.ReadAllLines(data.GetPath()); if (lines.Length < 15 || !lines[14].Contains("folderDatasByGuid")) return; var guids = new List(); var icons = new List(); var colors = new List(); void parseGudis() { for (int i = 16; i < lines.Length; i++) { if (lines[i].Contains("values:")) break; var startIndex = lines[i].IndexOf("- ") + 2; if (startIndex < lines[i].Length) guids.Add(lines[i].Substring(startIndex)); else guids.Add(""); } } void parseIcons() { for (int i = 0; i < guids.Count; i++) if (lines[29 + i * 5 + 3] is string line) if (line.Length > line.IndexOf(": ") + 2) icons.Add(line.Substring(line.IndexOf(": ") + 2)); else icons.Add(""); } void parseColors() { for (int i = 0; i < guids.Count; i++) if (lines[29 + i * 5 + 1] is string line) if (line.Length > line.IndexOf(": ") + 2) colors.Add(int.Parse(line.Substring(line.IndexOf(": ") + 2))); else colors.Add(0); } void remapColors() { for (int i = 0; i < colors.Count; i++) if (colors[i] == 10) colors[i] = 1; else if (colors[i] != 0) colors[i]++; } void fillData() { for (int i = 0; i < guids.Count; i++) if (icons[i] != "" || colors[i] != 0) data.folderDatas_byGuid[guids[i]] = new FolderData { iconNameOrGuid = icons[i], colorIndex = colors[i] }; data.Dirty(); data.Save(); } try { parseGudis(); parseIcons(); parseColors(); remapColors(); fillData(); } catch { } } subscribe(); loadData(); loadPalette(); loadDataAndPaletteDelayed(); migrateDataFromV1(); } public static VFoldersData data; public static VFoldersPalette palette; static EditorWindow hoveredProjectBrowser => EditorWindow.mouseOverWindow?.GetType() == t_ProjectBrowser ? EditorWindow.mouseOverWindow : null; static IEnumerable allProjectBrowsers => _allProjectBrowsers ??= t_ProjectBrowser.GetFieldValue("s_ProjectBrowsers").Cast(); static IEnumerable _allProjectBrowsers; static Type t_ProjectBrowser = typeof(Editor).Assembly.GetType("UnityEditor.ProjectBrowser"); const string version = "2.0.8"; } } #endif