Search Unity

Project Window Extension Script: "Folders" sorting, double-click expand/collapse...

Discussion in 'Assets and Asset Store' started by unrealuniter, Apr 28, 2013.

  1. unrealuniter

    unrealuniter

    Joined:
    Apr 7, 2013
    Posts:
    25
    This script related to feature request: http://feedback.unity3d.com/unity/all-categories/1/hot/active/editor-sorting-in-project-windo
    Double-click expand/collapse feature request: http://feedback.unity3d.com/unity/editor/1/new/active/doubleclick-project-view-folder

    Made for Unity 3D v4.1.2.

    Features:
    1) "Folders" are sorted separately from "Files", like in Visual Studio. Sorting works only for Ptoject Window "One Column Layout".
    2) Double-click "Folders" expand/collapse, like in Visual Studio.

    Note:
    This script is unusual and using reflection, because Unity scripting doesn't support Project Window extending in that way. So i was lucky to be able to find the way of doing it, this also means that in future versions of Unity this script may not work.

    Also would be nice to implement and test:
    - Sort files by extensions.

    v1.0
    ProjectWindowExtension.cs
    Code (csharp):
    1.  
    2. /*
    3.  * Project Window Extension v1.0
    4.  *
    5.  * Copyright (c) 2013 newbprofi
    6.  *
    7.  * Permission is hereby granted, free of charge, to any person obtaining a copy
    8.  * of this software and associated documentation files (the "Software"), to deal
    9.  * in the Software without restriction, including without limitation the rights
    10.  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11.  * copies of the Software, and to permit persons to whom the Software is
    12.  * furnished to do so, subject to the following conditions:
    13.  *
    14.  * The above copyright notice and this permission notice shall be included in
    15.  * all copies or substantial portions of the Software.
    16.  *
    17.  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18.  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19.  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20.  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21.  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22.  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23.  * THE SOFTWARE.
    24. */
    25.  
    26. using System;
    27. using System.Reflection;
    28. using UnityEngine;
    29. using UnityEditor;
    30. using System.Collections.Generic;
    31. using System.Collections;
    32.  
    33. [InitializeOnLoad]
    34. public static class ProjectWindowExtension
    35. {
    36.     static Assembly unityEditorAssembly;
    37.  
    38.     static Type objectBrowserType;
    39.     static Type treeViewType;
    40.     static Type treeViewDataType;
    41.     static Type defaultTreeViewDataSourceType;
    42.     static Type nodeType;
    43.  
    44.     static MethodInfo isFolderMethod;
    45.     static MethodInfo findNodeByIDMethod;
    46.     static MethodInfo isExpandableMethod;
    47.     static MethodInfo isExpandedMethod;
    48.     static MethodInfo setExpandedWithChildrenMethod;
    49.     static MethodInfo setExpandedMethod;
    50.     static MethodInfo getInstanceIDFromGUIDMethod;
    51.  
    52.     static FieldInfo objectBrowserField;
    53.     static FieldInfo assetTreeField;
    54.     static FieldInfo folderTreeField;
    55.     static FieldInfo treeDataField;
    56.     static FieldInfo visibleRowsField;
    57.     static FieldInfo nodeInstanceIDField;
    58.     static FieldInfo nodeDepthField;
    59.  
    60.     static object objectBrowser;
    61.     static object assetTree;
    62.     static object folderTree;
    63.     static object treeData;
    64.  
    65.     static int lastVisibleRowsHash;
    66.  
    67.     static ProjectWindowExtension()
    68.     {
    69.         bool result = (unityEditorAssembly = Assembly.GetAssembly(typeof(Editor))) != null;
    70.         if(result)
    71.         {
    72.             result = (objectBrowserType = unityEditorAssembly.GetType("UnityEditor.ObjectBrowser")) != null;
    73.             result = (treeViewType = unityEditorAssembly.GetType("UnityEditor.TreeView")) != null;
    74.             result = (treeViewDataType = unityEditorAssembly.GetType("UnityEditor.ITreeViewDataSource")) != null;
    75.             result = (defaultTreeViewDataSourceType = unityEditorAssembly.GetType("UnityEditor.DefaultTreeViewDataSource")) != null;
    76.             result = (nodeType = unityEditorAssembly.GetType("UnityEditor.TreeView+Node")) != null;
    77.             if(result)
    78.             {
    79.                 result = (isFolderMethod = objectBrowserType.GetMethod("IsFolder", BindingFlags.Static | BindingFlags.Public)) != null;
    80.                 result = (findNodeByIDMethod = treeViewDataType.GetMethod("FindNodeByID", BindingFlags.Instance | BindingFlags.Public)) != null;
    81.                 result = (isExpandableMethod = treeViewDataType.GetMethod("IsExpandable", BindingFlags.Instance | BindingFlags.Public)) != null;
    82.                 result = (isExpandedMethod = treeViewDataType.GetMethod("IsExpanded", BindingFlags.Instance | BindingFlags.Public)) != null;
    83.                 result = (setExpandedWithChildrenMethod = treeViewDataType.GetMethod("SetExpandedWithChildren", BindingFlags.Instance | BindingFlags.Public)) != null;
    84.                 result = (setExpandedMethod = treeViewDataType.GetMethod("SetExpanded", BindingFlags.Instance | BindingFlags.Public)) != null;
    85.                 result = (getInstanceIDFromGUIDMethod = typeof(AssetDatabase).GetMethod("GetInstanceIDFromGUID", BindingFlags.Static | BindingFlags.NonPublic)) != null;
    86.  
    87.                 result = (objectBrowserField = objectBrowserType.GetField("s_LastInteractedObjectBrowser", BindingFlags.Static | BindingFlags.Public)) != null;
    88.                 result = (assetTreeField = objectBrowserType.GetField("m_AssetTree", BindingFlags.Instance | BindingFlags.NonPublic)) != null;
    89.                 result = (folderTreeField = objectBrowserType.GetField("m_FolderTree", BindingFlags.Instance | BindingFlags.NonPublic)) != null;
    90.                 result = (treeDataField = treeViewType.GetField("m_Data", BindingFlags.Instance | BindingFlags.Public)) != null;
    91.                 result = (visibleRowsField = defaultTreeViewDataSourceType.GetField("m_VisibleRows", BindingFlags.Instance | BindingFlags.NonPublic)) != null;
    92.                 result = (nodeInstanceIDField = nodeType.GetField("m_InstanceID", BindingFlags.Instance | BindingFlags.NonPublic)) != null;
    93.                 result = (nodeDepthField = nodeType.GetField("m_Depth", BindingFlags.Instance | BindingFlags.NonPublic)) != null;
    94.                 if(result)
    95.                 {
    96.                     EditorApplication.projectWindowItemOnGUI += ProjectWindowItem_OnGUI;
    97.                     EditorApplication.projectWindowChanged += ProjectWindow_Changed;
    98.                 }
    99.             }
    100.         }
    101.     }
    102.  
    103.     static bool IsFolder(object node)
    104.     {
    105.         int instanceID = (int)nodeInstanceIDField.GetValue(node);
    106.         bool isFolder = (bool)isFolderMethod.Invoke(null, new object[] { instanceID });
    107.         return isFolder;
    108.     }
    109.  
    110.     static int SortRecursiveByDepth(IList sortedList, IList list, int position, int depth, bool needFolders)
    111.     {
    112.         int pos = position;
    113.         int count = list.Count;
    114.         bool lastIsFolder = !needFolders;
    115.  
    116.         while(pos < count  sortedList.Count < count)
    117.         {
    118.             object node = list[pos];
    119.             int nodeDepth = (int)nodeDepthField.GetValue(list[pos]);
    120.  
    121.             // sort folders or files only
    122.             if(needFolders)
    123.             {
    124.                 if(nodeDepth == depth)
    125.                 {
    126.                     lastIsFolder = IsFolder(node);
    127.                     if(lastIsFolder)
    128.                         sortedList.Add(node);
    129.  
    130.                     pos++;
    131.                 }
    132.                 else if(nodeDepth > depth)
    133.                 {
    134.                     if(lastIsFolder)
    135.                         pos = SortRecursiveByDepth(sortedList, list, pos, nodeDepth, true);
    136.                     else
    137.                         pos++;
    138.                 }
    139.                 else
    140.                     break;
    141.             }
    142.             else
    143.             {
    144.                 if(nodeDepth == depth)
    145.                 {
    146.                     lastIsFolder = IsFolder(node);
    147.                     if(!lastIsFolder)
    148.                         sortedList.Add(node);
    149.  
    150.                     pos++;
    151.                 }
    152.                 else if(nodeDepth > depth)
    153.                 {
    154.                     if(!lastIsFolder)
    155.                         pos = SortRecursiveByDepth(sortedList, list, pos, nodeDepth, false);
    156.                     else
    157.                         pos++;
    158.                 }
    159.                 else
    160.                     break;
    161.             }
    162.         }
    163.  
    164.         // sort files
    165.         if(needFolders  sortedList.Count < count)
    166.             pos = SortRecursiveByDepth(sortedList, list, position, depth, false);
    167.  
    168.         return pos;
    169.     }
    170.  
    171.     static void SortAssetTree()
    172.     {
    173.         if(treeData != null  assetTree != null)
    174.         {
    175.             IList visibleRows = (IList)visibleRowsField.GetValue(treeData);
    176.             if(visibleRows != null)
    177.             {
    178.                 // check if was reallocated
    179.                 if(lastVisibleRowsHash != visibleRows.GetHashCode())
    180.                 {
    181.                     lastVisibleRowsHash = visibleRows.GetHashCode();
    182.  
    183.                     // sort
    184.                     ArrayList sortedList = new ArrayList(visibleRows.Count);
    185.                     SortRecursiveByDepth(sortedList, visibleRows, 0, 0, true);
    186.  
    187.                     // rewrite with sorted list
    188.                     for(int i = 0; i < visibleRows.Count; i++)
    189.                         visibleRows[i] = sortedList[i];
    190.                 }
    191.             }
    192.         }
    193.     }
    194.  
    195.     static void InitObjects()
    196.     {
    197.         // this objects changed when project changed and not only
    198.         assetTree = null;
    199.         folderTree = null;
    200.         treeData = null;
    201.         objectBrowser = objectBrowserField.GetValue(null);
    202.         if(objectBrowser != null)
    203.         {
    204.             assetTree = assetTreeField.GetValue(objectBrowser);
    205.             folderTree = folderTreeField.GetValue(objectBrowser);
    206.  
    207.             if(assetTree != null)
    208.                 treeData = treeDataField.GetValue(assetTree);
    209.             else if(folderTree != null)
    210.                 treeData = treeDataField.GetValue(folderTree);
    211.         }
    212.     }
    213.  
    214.     static void ProjectWindow_Changed()
    215.     {
    216.         InitObjects();
    217.     }
    218.  
    219.     static void ProjectWindowItem_OnGUI(string guid, Rect drawingRect)
    220.     {
    221.         InitObjects();
    222.         SortAssetTree();
    223.  
    224.         if(Event.current.type == EventType.MouseDown
    225.              Event.current.clickCount == 2
    226.              drawingRect.Contains(Event.current.mousePosition))
    227.         {
    228.             if(treeData != null)
    229.             {
    230.                 int instanceID = (int)getInstanceIDFromGUIDMethod.Invoke(null, new object[] { guid });
    231.                 object node = findNodeByIDMethod.Invoke(treeData, new object[] { instanceID });
    232.                 if(node != null  IsFolder(node))
    233.                 {
    234.                     bool isExpandable = (bool)isExpandableMethod.Invoke(treeData, new object[] { node });
    235.                     bool isExpanded = (bool)isExpandedMethod.Invoke(treeData, new object[] { node });
    236.                     if(isExpandable)
    237.                     {
    238.                         if(Event.current.alt)
    239.                         {
    240.                             if(isExpanded)
    241.                                 setExpandedWithChildrenMethod.Invoke(treeData, new object[] { node, false });
    242.                             else
    243.                                 setExpandedWithChildrenMethod.Invoke(treeData, new object[] { node, true });
    244.                         }
    245.                         else
    246.                         {
    247.                             if(isExpanded)
    248.                                 setExpandedMethod.Invoke(treeData, new object[] { node, false });
    249.                             else
    250.                                 setExpandedMethod.Invoke(treeData, new object[] { node, true });
    251.                         }
    252.                     }
    253.  
    254.                     //Event.current.Use();
    255.                 }
    256.             }
    257.         }
    258.     }
    259. }
    260.  
    EDIT: Actually, i found that Unity's default sorting is not so bad. I found that when everything is sorted alphabetically - its easier for brain to find things. You just have to make good hierarchy of folders to not allow "folders" and "files" mix very often. So when you adding "folder" sorting you also adding a little brain pain, because you have to always remember, that "folders" on top and "files" bottom. But it is still very useful and i think there must be an option to switch sorting.
     
    Last edited: May 1, 2013
    Fressbrett, mgear, Can-Baycay and 2 others like this.
  2. Helmut Duregger

    Helmut Duregger

    Joined:
    Mar 17, 2010
    Posts:
    18
    This is awesome! Great work.

    Can you add support for the "Two Columns Layout"?

    I'll try to add it myself if I find the time.
     
  3. MaximilianPs

    MaximilianPs

    Joined:
    Nov 7, 2011
    Posts:
    321
    How to install it ?

    [edit]
    Place it in a folder named "Editor"

    it works perfectly thnx a lot, I've finally found the peace of sense ^_^
     
    Last edited: Aug 23, 2013
  4. jedy

    jedy

    Joined:
    Aug 1, 2010
    Posts:
    579
    Man this is just awesome!

    I did a similar thing maybe a year ago but it didn't work that well. And the sorting - super simple yet making my life easier. Thanks!
     
  5. hogwash

    hogwash

    Joined:
    Oct 12, 2012
    Posts:
    117
    Is there a way to also expand prefabs?
     
  6. nosebleed_dre

    nosebleed_dre

    Joined:
    Jul 25, 2013
    Posts:
    23
    If all you want to do is collapse or open all sub folders (including prefab contents), you can just alt + click on the parent folder. No scripts required.
     
  7. Trung-Hieu

    Trung-Hieu

    Joined:
    Feb 18, 2013
    Posts:
    37
    Hi, maybe this is my browser problem, but I found that all the && (AND operators) are gone from the snippet (I am using Mac with Chrome browser)

    So if you guys facing the compiling errors when copy/paste, check whether the && operators are missing :)

    P.s: All the result = ... statements in the constructor (except the first) are actually result &= ... statements (& sign missing)
     
    Last edited: Nov 25, 2015
    macagu and zero_equals_zero like this.
  8. jnoel_bricklink

    jnoel_bricklink

    Joined:
    Nov 3, 2020
    Posts:
    2
    Sorry to raise this post, but some code was found on the unity answers forum that supports 1 and 2 column, original credit goes to @WeslomPo


    Code (CSharp):
    1.  
    2.     using System;
    3.     using System.Collections;
    4.     using System.Collections.Generic;
    5.     using System.Linq;
    6.     using UnityEngine;
    7.     using UnityEditor;
    8.     using System.Reflection;
    9.    
    10.     [InitializeOnLoad]
    11.     public static class ProjectBrowserExtension
    12.     {
    13.         private const string k_UnityEditorProjectBrowserAssemblyName = "UnityEditor.ProjectBrowser";
    14.         private const string k_ProjectBrowsersFieldName = "s_ProjectBrowsers";
    15.         private const string k_AssetTreeFieldName = "m_AssetTree";
    16.         private const string k_ListAreaFieldName = "m_ListArea";
    17.         private const string k_DataFieldName = "data";
    18.         private const string k_FoldersFirstFieldName = "foldersFirst";
    19.    
    20.         private static readonly object s_BoolTrue = true;
    21.        
    22.         static ProjectBrowserExtension()
    23.          {
    24.              EditorApplication.projectChanged += OnChanged;
    25.              EditorApplication.playModeStateChanged += OnPlayMode;
    26.              EditorApplication.projectWindowItemOnGUI += OnFirstTime;
    27.          }
    28.          private static void OnFirstTime(string guid, Rect _)
    29.          {
    30.              EditorApplication.projectWindowItemOnGUI -= OnFirstTime;
    31.              Refresh();
    32.          }
    33.          private static void OnChanged() => Refresh();
    34.          private static void OnPlayMode(PlayModeStateChange obj) => Refresh();
    35.        
    36.          /// <summary>
    37.          /// foreach browser in UnityEditor.ProjectBrowser.s_ProjectBrowsers
    38.          ///     browser.m_AssetTree.data.foldersFirst = true
    39.          ///     browser.m_ListArea.foldersFirst = true
    40.          /// </summary>
    41.          private static void Refresh()
    42.          {
    43.              Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.Editor));
    44.              Type projectBrowser = assembly.GetType(k_UnityEditorProjectBrowserAssemblyName);
    45.              FieldInfo field = projectBrowser.GetField(k_ProjectBrowsersFieldName, BindingFlags.Static | BindingFlags.NonPublic);
    46.              if (field == null)
    47.                  return;
    48.              IEnumerable list = (IEnumerable) field.GetValue(projectBrowser);
    49.              foreach (object pb in list)
    50.                  SetFolderFirstForProjectWindow(pb);
    51.          }
    52.          private static void SetFolderFirstForProjectWindow(object pb)
    53.          {
    54.              IEnumerable<FieldInfo> members = pb.GetType().GetRuntimeFields();
    55.              int maxMembersSought = 2;
    56.              foreach (FieldInfo member in members)
    57.              {
    58.                  switch (member.Name)
    59.                  {
    60.                      // One column
    61.                      case k_AssetTreeFieldName:
    62.                          SetOneColumnFolderFirst(pb, member);
    63.                          maxMembersSought--;
    64.                          break;
    65.                      // Two column
    66.                      case k_ListAreaFieldName:
    67.                          SetTwoColumnFolderFirst(pb, member);
    68.                          maxMembersSought--;
    69.                          break;
    70.                  }
    71.    
    72.                  if (maxMembersSought == 0)
    73.                      break;
    74.              }
    75.          }
    76.          private static void SetTwoColumnFolderFirst(object pb, FieldInfo listAreaField)
    77.          {
    78.              if (listAreaField == null)
    79.                  return;
    80.              object listArea = listAreaField.GetValue(pb);
    81.              // safety check
    82.              if (listArea == null)
    83.                  return;
    84.              PropertyInfo folderFirst = listArea.GetType().GetProperties().Single(x => x.Name == k_FoldersFirstFieldName);
    85.              folderFirst.SetValue(listArea, s_BoolTrue);
    86.          }
    87.          private static void SetOneColumnFolderFirst(object pb, FieldInfo assetTreeField)
    88.          {
    89.              if (assetTreeField == null)
    90.                  return;
    91.            
    92.              object assetTree = assetTreeField.GetValue(pb);
    93.              // Fix: as we are looping all members, it's possible to end up in a case where one member is seen first,
    94.              // this will be null.
    95.              if (assetTree == null)
    96.                  return;
    97.            
    98.              PropertyInfo data = assetTree.GetType().GetRuntimeProperties().Single(x => x.Name == k_DataFieldName);
    99.              // AssetsTreeViewDataSource
    100.              object dataSource = data.GetValue(assetTree);
    101.    
    102.              // safety check
    103.              if (dataSource == null)
    104.                  return;
    105.              PropertyInfo folderFirst = dataSource.GetType().GetProperties().Single(x => x.Name == k_FoldersFirstFieldName);
    106.              folderFirst.SetValue(dataSource, s_BoolTrue);
    107.          }
    108.     }
    109.  
    110.  
     
    Viole, WonkeeKim and WeslomPo like this.
  9. Viole

    Viole

    Joined:
    Dec 29, 2015
    Posts:
    38
    This is amazing.

    Also, this should be a standard feature.
     
  10. JollyTheory

    JollyTheory

    Joined:
    Dec 30, 2018
    Posts:
    155
    The script above was breaking every time anything reloaded and you'd need to click something inside the project folder for it to sort.
    So instead i just directly modified UnityEngine.CoreModule.dll using dnSpy:
    (Applications/Unity/Hub/Editor/2021.3.9f1/Unity.app/Contents/Managed/UnityEngine/UnityEngine.CoreModule.dll)
    UnityEditor -> ProjectBrowser -> GetShouldShowFoldersFirst() -> right click -> Edit IL instructions -> replace instructions with "ldc.i4.1", "ret" (meaning 'return true;').
    Save, replace the dll. Have windows style sorting forever without any scripts. Works with Unity 2021.3.9f1 for me.

    //Before this I also tried a bunch of things like: Harmony to inject stuff into this method using a script (Harmony doesn't work on M1 turns out), tried same with Mono.Cecil (turns out the core .dll is unreachable for modifications from there or something like that). dnSpy to the rescue, one less annoying thing about macOS.
     
    Last edited: Oct 3, 2022
    mentia_lin, Fressbrett, mgear and 2 others like this.
  11. JollyTheory

    JollyTheory

    Joined:
    Dec 30, 2018
    Posts:
    155
    Another cool tweak, since I'm not sure where to put it:
    Use this script to disable the annoying behaviour of Inspector selecting all text when you click on a property, now it will save you that extra click of deselecting everything.
     
  12. WeslomPo

    WeslomPo

    Joined:
    Oct 7, 2013
    Posts:
    8
    I was doing that before, but after update your editor it will broke and you need to do it again. >__<. So I though that fix that problem with click is more bearable than patch dll. Ahah
     
  13. SMFL_

    SMFL_

    Joined:
    Feb 8, 2020
    Posts:
    1
    Loving the effort here boys cba but It's good to know at least you can hack it in. Might give it a go but until then just going to add zeros to the start of my folders :p
     
  14. Fressbrett

    Fressbrett

    Joined:
    Apr 11, 2018
    Posts:
    97
    No idea why this is still not a feature in unity... being able to keep folders at the top of the project window seems like such a simple, must have feature.

    The .dll hack is nice @JollyTheory, but sadly dnSpy is only available for Windows users, not mac or linux.
     
    Viole likes this.
  15. Viole

    Viole

    Joined:
    Dec 29, 2015
    Posts:
    38
  16. JollyTheory

    JollyTheory

    Joined:
    Dec 30, 2018
    Posts:
    155
    This should be a Unity feature by default for sure.

    One can use the trial of parallels.com to run dnSpy on mac.
     
  17. yodamaycry

    yodamaycry

    Joined:
    Oct 18, 2019
    Posts:
    5
    So not just me, this is an actual thing... that's disappointing.
     
    Fressbrett likes this.