Editor/Unselectable.cs
using System.Collections.Generic;
using System.Linq;
namespace HierarchyExtension;
/// <summary>
/// Objects that can't be selected in the scene view. A click goes through them to whatever is behind, and a drag
/// selection leaves them out. They still select from the Hierarchy, so they can be inspected and edited as usual.
/// <para>
/// An object is made unselectable with the <see cref="Tag"/> tag, the way the stock Hide marks hidden objects with
/// "hidden". It saves with the scene, shows in the object's Tags, takes undo, and counts as an override on a prefab
/// instance. Tags pass down to children, so everything under an unselectable object is unselectable too.
/// </para>
/// <para>
/// Set it with the hand on each Hierarchy row (<see cref="SelectableColumn"/>), with <b>Scene > View > Toggle
/// Selectable</b> (Alt+L) for the selected objects, or with <b>Make All Selectable</b>. <see cref="ScenePicking"/>
/// makes the scene view honour it.
/// </para>
/// </summary>
public static class Unselectable
{
/// <summary>The tag that makes an object, and everything under it, unselectable in the scene view.</summary>
public const string Tag = "unselectable";
/// <summary>Whether <paramref name="go"/> can't be selected in the scene view, from its own tag or a parent's.</summary>
public static bool Has( GameObject go ) => go.IsValid() && go is not Scene && go.Tags.Has( Tag );
/// <summary>Whether <paramref name="go"/> has the tag itself, not only from a parent.</summary>
public static bool IsSet( GameObject go ) => go.IsValid() && go is not Scene && go.Tags.Has( Tag, false );
/// <summary>
/// The object whose tag makes <paramref name="go"/> unselectable: itself, or the nearest parent with the tag.
/// Null when it's selectable.
/// </summary>
public static GameObject Source( GameObject go )
{
for ( var o = go; o.IsValid() && o is not Scene; o = o.Parent )
{
if ( IsSet( o ) ) return o;
}
return null;
}
/// <summary>
/// Make <paramref name="objects"/> unselectable in the scene view, or selectable again, as one undo step. Objects
/// that are that way already are left alone. Making an object selectable only takes its own tag off, so one under
/// an unselectable parent stays unselectable; pass <see cref="Source"/> to free it.
/// </summary>
public static void Set( IEnumerable<GameObject> objects, bool unselectable )
{
var changing = objects.Where( x => x.IsValid() && x is not Scene && IsSet( x ) != unselectable ).Distinct().ToArray();
if ( changing.Length == 0 ) return;
var session = SessionOf( changing[0] );
if ( session is null ) return;
var name = unselectable ? "Make Unselectable" : "Make Selectable";
if ( changing.Length == 1 ) name = $"{name}: {changing[0].Name}";
using var sceneScope = session.Scene.Push();
// only the objects' own properties, which is where tags live: a tile layer's children and components can be big
using ( session.UndoScope( name ).WithGameObjectChanges( changing, GameObjectUndoFlags.Properties ).Push() )
{
foreach ( var go in changing )
{
if ( unselectable ) go.Tags.Add( Tag );
else go.Tags.Remove( Tag );
}
}
}
/// <summary>
/// Alt+L: make the selected objects unselectable in the scene view or, when they all are already, selectable again.
/// One that's unselectable through a parent frees that parent, and so everything under it.
/// </summary>
[Menu( "Editor", "Scene/View/Toggle Selectable", "do_not_touch" )]
[Shortcut( "editor.toggle-selectable", "Alt+L" )]
public static void ToggleSelected()
{
var session = SceneEditorSession.Active;
if ( session is null ) return;
var selected = session.Selection.OfType<GameObject>().Where( x => x.IsValid() && x is not Scene ).ToArray();
if ( selected.Length == 0 ) return;
if ( selected.Any( x => !Has( x ) ) ) Set( selected, true );
else Set( selected.Select( Source ), false );
}
/// <summary>Make everything in the open scene selectable in the scene view again.</summary>
[Menu( "Editor", "Scene/View/Make All Selectable", "touch_app" )]
public static void MakeAllSelectable()
{
var scene = SceneEditorSession.Active?.Scene;
if ( !scene.IsValid() ) return;
Set( scene.GetAllObjects( false ).Where( IsSet ), false );
}
/// <summary>Whether anything in <paramref name="scene"/> is unselectable. Cheap enough to ask on every click.</summary>
public static bool AnyIn( Scene scene )
{
if ( !scene.IsValid() ) return false;
foreach ( var go in scene.GetAllObjects( true ) )
{
if ( IsSet( go ) ) return true;
}
return false;
}
/// <summary>The editor session whose scene holds <paramref name="go"/>, the play session's included.</summary>
public static SceneEditorSession SessionOf( GameObject go ) => SessionOf( go?.Scene );
/// <summary>The editor session of <paramref name="scene"/>, the play session's included.</summary>
public static SceneEditorSession SessionOf( Scene scene )
{
if ( scene is null ) return null;
return SceneEditorSession.All.FirstOrDefault( x => x.Scene == scene );
}
}