Creating an Inspector view that only changes on scene hierarchy selection or asset selection
I have a project architecture that relies ScriptableObject assets that are attached to GameObjects in the scene. Working with this architecture involves making changes to both scene objects and the assets that are attached to them.
What I would like to do is have two Inspector windows -- one which only displays objects that I have selected in the scene hierarchy, and another which only displays objects I have selected from my project assets.
Some workarounds I have seen include using two vanilla Inspector panes and locking one of them -- this is workable and what I'm currently doing, but extremely inconvenient. I've also seen inline ScriptableObject drawers, but since the SOs I am using correspond to things like shared variables, I would like to keep them separated in their own Inspector window to maintain the distinction between data set on a scene object versus data stored in a SO asset.
Does anybody have any insight into how one would go about creating an Inspector window that only shows scene or asset objects? Or does such a tool exist already? (I've looked at the Unity reference code, but an 1800+ line class is... a bit much for my brain to parse through right now.)
Thanks in advance for any help!
![alt text]Two inspectors -- top for scene objects, bottom for assets[1]
[1]: /storage/temp/122528-twoinspectors.png
Answer by stektpotet · Aug 16, 2018 at 11:32 PM
It can be done...
However it's not the easiest thing to make:
All things in unity that derive from the type Object
has the capability to be viewed in an Editor/Inspector . This is shown in the script reference documentation for the `Editor`-class (Specifically the CreateEditor()-function)
Using this to our advantage we can, by obtaining the selected objects in both the project view, scene view and hierarchy view, create an editor for the selected Object!
perfect! - right?
Well... turns out it's not that easy...
The first problem is that the Editor object itself merely acts as an interface to drawing the UI that's done by the inspector. so we'll need to manually make the different calls to the elements of the UI we want drawn.
Secondly, there's the bigger problem of GameObjects and how their inspectors work - You see as a gameobject contains a set of all the different components attached, the InspectorWindow needs to iterate over all the components in order to create a full representation of the gameobject in the inspector. Unity does this by using an Editor-object for each of the components attached to the gameobject and sequentially calling the editor-objects' OnInspectorGUI()-methods.
Approach:
- We need to create an EditorWindow that listens to selection changes in order to notify that we want to edit something selected if the selected item is a gameobject in the scene or an asset in the project
This can be achieved using the
Selection.selectionChanged
-callback and useSelection.GetFiltered(...)
- to filter the selection for only assets/gameobjects
- We need to use the selected item (preferrably items[plural]) to create editor objects that hold the UI
Provided we get the notification from the callback, that something in our selection is changed, we can update a field containing the Editor-object we're using to interface the UI for said object. we basically say something along the lines of
targetEditor = Editor.CreateEditor(filteredObjects)
(notice: plural filteredObjects - we can make an editor that targets multiple objects at the same time)
- We need to make UI calls through the editor Object in a sensible order to get results that look like the inspector.
this might be considered a bit tricky depending on how much like the normal inspector you want your inspector to look. Specifically the preview-part of an inspector is more difficult to achieve here, as we need to manage the resizing of it manually
I got started doing some of it, there's still a few kinks in it here and there (specifically: I've disallowed usage of the FilteredInpector
for multi-GameObject editing because it's a tough job to validate that selected gameobjects have all the same components and then make an editor for the components combined)
Anyways, here it is:
using UnityEditor;
using UnityEngine;
using System.Linq;
class FilteredInspector : EditorWindow
{
[System.Flags]
enum Filter
{
GameObjects = 0,
Assets = 1
}
Filter selectionFilter = Filter.Assets;
Vector2 scrollPosition;
Editor targetEditor;
Editor[] componentEditors;
//EDITOR STUFF
float previewHeight = 100;
Texture2D[] icons = new Texture2D[2];
bool[] foldouts;
[MenuItem("Window/Filtered Inspector")]
public static void OpenWindow()
{
var window = CreateInstance<FilteredInspector>(); //Allows multiple instances
window.titleContent = new GUIContent("Filtered Inspector", EditorGUIUtility.FindTexture("d_UnityEditor.InspectorWindow"));
window.icons[0] = EditorGUIUtility.FindTexture("SceneAsset Icon");
window.icons[1] = EditorGUIUtility.FindTexture("Folder Icon");
window.minSize = new Vector2(250, 250);
window.Show();
window.OnSelectionChange();
}
private void OnEnable()
{
Selection.selectionChanged += OnSelectionChange;
}
private void OnDisable()
{
Selection.selectionChanged -= OnSelectionChange;
}
private void OnSelectionChange()
{
Object[] objects;
if (selectionFilter == Filter.GameObjects)
{
objects = Selection.GetFiltered(typeof(GameObject), SelectionMode.ExcludePrefab | SelectionMode.OnlyUserModifiable | SelectionMode.Editable);
if (objects.Length > 0)
{
if (objects.Length > 1) { Debug.LogWarning("Multi Object selection only supported for Assets, not scene objects"); return; }
var components = ((GameObject)objects[0]).GetComponents<Component>().Where(c => c != null && (c.hideFlags & HideFlags.HideInInspector) != HideFlags.HideInInspector).ToArray(); //get all components that are not hidden
componentEditors = new Editor[components.Length];
foldouts = new bool[components.Length];
for (int i = 0; i < components.Length; i++)
{
componentEditors[i] = Editor.CreateEditor(components[i]);
foldouts[i] = true;
}
}
}
else
{
objects = Selection.GetFiltered(typeof(Object), SelectionMode.Assets);
}
if (objects.Length > 0)
targetEditor = Editor.CreateEditor(objects);
Repaint();
}
private void OnGUI()
{
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUILayout.Label("Inspector Mode", "HeaderLabel");
int newFilterMode = GUILayout.Toolbar((int)selectionFilter, icons, "Command");
if (selectionFilter != (Filter)newFilterMode)
{
selectionFilter = (Filter)newFilterMode;
OnSelectionChange();
}
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
if (targetEditor != null)
{
scrollPosition = GUILayout.BeginScrollView(scrollPosition);
targetEditor.DrawHeader();
EditorGUILayout.BeginVertical(EditorStyles.inspectorDefaultMargins);
targetEditor.OnInspectorGUI();
if (targetEditor.target is GameObject)
{
EditorGUIUtility.hierarchyMode = true;
for (int i = 0; i < componentEditors.Length; i++)
{
if (i != 0)
{
EditorGUIUtility.labelWidth = 120;
EditorGUI.indentLevel = 1;
}
else { EditorGUIUtility.labelWidth = 50; }
Editor e = componentEditors[i];
Rect foldoutRect =EditorGUILayout.GetControlRect(GUILayout.Height(20));
//Rect foldoutRect = GUILayoutUtility.GetLastRect();
foldoutRect.x -= 14;
foldoutRect.width += 18;
GUI.Box(foldoutRect, GUIContent.none, "IN Title");
foldoutRect.y += 2;
foldoutRect.height -= 2;
System.Reflection.PropertyInfo enableProperty = e.target.GetType().GetProperty("enabled"); //hack to obtain whether or not the component can be disabled
if (enableProperty != null)
{
foldoutRect.x += 20;
foldoutRect.width -= 20;
float width = foldoutRect.width;
foldoutRect.width = 16;
enableProperty.SetValue(e.target, GUI.Toggle(foldoutRect, ((bool)enableProperty.GetValue(e.target, null)), GUIContent.none, "IN Toggle"), null);
foldoutRect.x -= 20;
foldoutRect.width = width;
}
foldoutRect.x += 2;
foldouts[i] = GUI.Toggle(foldoutRect, foldouts[i], GUIContent.none, "IN Foldout");
foldoutRect.x += 18;
foldoutRect.width -= 20;
if (enableProperty != null)
{
foldoutRect.x += 20;
foldoutRect.width -= 20;
}
GUI.Label(foldoutRect, new GUIContent(ObjectNames.NicifyVariableName(e.target.GetType().Name), AssetPreview.GetMiniTypeThumbnail(e.target.GetType())), "IN TitleText");
if (foldouts[i])
{
e.OnInspectorGUI();
}
}
EditorGUI.indentLevel = 0;
EditorGUIUtility.hierarchyMode = false;
}
EditorGUILayout.EndVertical();
if (targetEditor.HasPreviewGUI()) //This part need some work in order to work like the real inspector. Currently there's it's not resizable
{
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Label(targetEditor.GetPreviewTitle(), "WindowBottomResize");
targetEditor.OnPreviewSettings();
GUILayout.EndHorizontal();
targetEditor.DrawPreview(GUILayoutUtility.GetRect(EditorGUIUtility.currentViewWidth, previewHeight));
}
GUILayout.EndScrollView();
}
}
}
End Result
Hope this proves useful