Editor/HierarchySelection.cs
using System.Collections.Generic;
using System.Linq;
namespace HierarchyExtension;
/// <summary>
/// The Hierarchy's selection, so its rows are right while a drag selection is going on in the scene view.
/// <para>
/// The stock drag selection puts whatever its box touches into the scene's selection on every frame of the drag, and
/// the Hierarchy highlights exactly that: parents, maps and unselectable objects that the fully-inside rule
/// (<see cref="ScenePicking"/>) only takes out when the mouse comes up. Taking them out every frame would have the stock
/// one put them back every frame. So the Hierarchy gets a selection of its own instead: its tree's
/// <c>TreeView.Selection</c> is a public setting, and the tree paints rows by asking it, repaints when its hash changes,
/// and scrolls to what's added to it (tools source, <c>BaseItemWidget</c>, 26.09.29).
/// </para>
/// <para>
/// It holds a copy of what the Hierarchy should show: the scene's selection, or during a drag selection what that will
/// select (<see cref="ScenePicking.DragResult"/>). Selecting in the Hierarchy goes straight to the scene's selection,
/// as before. The inspector and the move handles follow the scene's selection, so during a drag they still show the
/// stock one's picks.
/// </para>
/// </summary>
public sealed class HierarchySelection : SelectionSystem
{
static HierarchySelection _current;
/// <summary>The one on the Hierarchy in use, if any.</summary>
internal static HierarchySelection Current => _current;
readonly SelectionSystem _selection;
HierarchySelection( SelectionSystem selection )
{
_selection = selection;
}
/// <summary>
/// Puts one on the Hierarchy's tree, and again whenever the tree is given the scene's selection back: it does that
/// each time it rebuilds, and on a new scene. Then brings its copy up to date.
/// </summary>
[EditorEvent.Frame]
static void Frame()
{
var tree = SceneTreeWidget.Current?.TreeView;
var selection = SceneEditorSession.Active?.Selection;
if ( !tree.IsValid() || selection is null ) return;
if ( tree.Selection == _current && _current?._selection == selection )
{
_current.Sync();
return;
}
// the tree has the scene's own, or one of these from before a code reload that wasn't carried over
var ours = tree.Selection?.GetType().FullName == typeof( HierarchySelection ).FullName;
if ( tree.Selection != selection && !ours ) return;
var view = new HierarchySelection( selection );
// filled before the tree has it, so filling it doesn't scroll the tree
view.Sync();
tree.Selection = view;
_current = view;
}
/// <summary>Makes the copy what the Hierarchy should show now, adding and removing only what differs.</summary>
internal void Sync()
{
IEnumerable<object> target = ScenePicking.DragResult ?? (IEnumerable<object>)_selection;
var wanted = target.ToHashSet();
foreach ( var obj in this.ToArray() )
{
if ( !wanted.Contains( obj ) ) base.Remove( obj );
}
foreach ( var obj in target )
{
if ( !base.Contains( obj ) ) base.Add( obj );
}
}
public override bool Contains( object obj ) => ScenePicking.DragResult is null ? _selection.Contains( obj ) : base.Contains( obj );
public override bool Any() => ScenePicking.DragResult is null ? _selection.Any() : base.Any();
public override bool Add( object obj )
{
var added = _selection.Add( obj );
Sync();
return added;
}
public override bool Remove( object obj )
{
var removed = _selection.Remove( obj );
Sync();
return removed;
}
public override void Clear()
{
_selection.Clear();
Sync();
}
public override bool Set( object obj )
{
var changed = _selection.Set( obj );
Sync();
return changed;
}
}