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
    macagu and nirvanajie 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:
    199
    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:
    578
    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:
    115
    Is there a way to also expand prefabs?
     
  6. nosebleed_dre

    nosebleed_dre

    Joined:
    Jul 25, 2013
    Posts:
    18
    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:
    35
    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.