mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-04-19 13:59:22 -04:00
* SceneEditorSession: make game vs editor scenes more distinct, Scene is whichever is currently active * Route prefab update, model reload etc events to both scenes if needed * SceneTree update checking a bit cleaner * Bring back GameEditorSessions instead, so undo, selection etc can all be linked 1:1 with scenes again * tweak and tidy
307 lines
7.1 KiB
C#
307 lines
7.1 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace Editor;
|
|
|
|
[Dock( "Editor", "Hierarchy", "list" )]
|
|
public partial class SceneTreeWidget : Widget
|
|
{
|
|
public TreeView TreeView { get; private set; }
|
|
|
|
Layout Header;
|
|
Layout SubHeader;
|
|
LineEdit Search;
|
|
ToolButton SearchClear;
|
|
|
|
IDisposable _selectionUndoScope = null;
|
|
|
|
public static SceneTreeWidget Current { get; private set; }
|
|
|
|
public SceneTreeWidget( Widget parent ) : base( parent )
|
|
{
|
|
Layout = Layout.Column();
|
|
|
|
Current = this;
|
|
|
|
BuildUI();
|
|
}
|
|
|
|
public void BuildUI()
|
|
{
|
|
Layout.Clear( true );
|
|
Header = Layout.AddColumn();
|
|
|
|
SubHeader = Layout.AddRow();
|
|
SubHeader.Spacing = 2;
|
|
SubHeader.Margin = 2;
|
|
SubHeader.Alignment = TextFlag.LeftCenter;
|
|
|
|
var add = SubHeader.Add( new AddButton( "add" ) );
|
|
add.MouseLeftPress = CreateGameObjectMenu;
|
|
|
|
Search = SubHeader.Add( new LineEdit(), 1 );
|
|
Search.PlaceholderText = "⌕ Search";
|
|
Search.Layout = Layout.Row();
|
|
Search.Layout.AddStretchCell( 1 );
|
|
Search.TextChanged += x => queryDirty = true;
|
|
Search.FixedHeight = Theme.RowHeight;
|
|
|
|
SearchClear = Search.Layout.Add( new ToolButton( string.Empty, "clear", this ) );
|
|
SearchClear.MouseLeftPress = () =>
|
|
{
|
|
Search.Text = string.Empty;
|
|
Rebuild();
|
|
|
|
// make sure we're open to the stuff we picked from search
|
|
foreach ( var item in TreeView.Selection )
|
|
{
|
|
TreeView.ExpandPathTo( item );
|
|
}
|
|
TreeView.UpdateIfDirty();
|
|
|
|
var scrollTarget = TreeView.Selection.FirstOrDefault();
|
|
if ( scrollTarget is not null )
|
|
{
|
|
TreeView.ScrollTo( scrollTarget );
|
|
}
|
|
};
|
|
SearchClear.Visible = false;
|
|
|
|
TreeView = new TreeView();
|
|
TreeView.MultiSelect = true;
|
|
TreeView.BodyDropTarget = TreeView.DragDropTarget.LastRoot;
|
|
TreeView.BodyContextMenu = OpenTreeViewContextMenu;
|
|
|
|
TreeView.OnBeforeSelection = x => _selectionUndoScope = SceneEditorSession.Active.UndoScope( "Select GameObject(s)" ).Push();
|
|
TreeView.OnBeforeDeselection = x => _selectionUndoScope = SceneEditorSession.Active.UndoScope( "Deselect GameObject(s)" ).Push();
|
|
TreeView.OnSelectionChanged = x =>
|
|
{
|
|
_selectionUndoScope?.Dispose();
|
|
_selectionUndoScope = null;
|
|
};
|
|
|
|
TreeView.OnPaintOverride = () =>
|
|
{
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( Theme.ControlBackground );
|
|
Paint.DrawRect( TreeView.LocalRect, Theme.ControlRadius );
|
|
|
|
return false;
|
|
};
|
|
|
|
Layout.Add( TreeView, 1 );
|
|
|
|
_lastScene.SetTarget( null );
|
|
CheckForChanges();
|
|
|
|
EditorUtility.OnInspect -= OnInspect;
|
|
EditorUtility.OnInspect += OnInspect;
|
|
}
|
|
|
|
void CreateGameObjectMenu()
|
|
{
|
|
var m = new ContextMenu( TreeView );
|
|
|
|
using var scope = SceneEditorSession.Scope();
|
|
var selected = EditorScene.Selection.FirstOrDefault() as GameObject;
|
|
|
|
GameObjectNode.CreateObjectMenu( m, selected, go =>
|
|
{
|
|
TreeView.Open( this );
|
|
TreeView.SelectItem( go, skipEvents: true );
|
|
TreeView.BeginRename();
|
|
} );
|
|
|
|
m.OpenAtCursor( false );
|
|
}
|
|
|
|
void OpenTreeViewContextMenu()
|
|
{
|
|
var rootItem = TreeView.Items.FirstOrDefault();
|
|
if ( rootItem is null ) return;
|
|
|
|
if ( rootItem is TreeNode node )
|
|
{
|
|
node.OnContextMenu();
|
|
}
|
|
}
|
|
|
|
WeakReference<Scene> _lastScene = new( null );
|
|
bool queryDirty = false;
|
|
|
|
[EditorEvent.Frame]
|
|
public void CheckForChanges()
|
|
{
|
|
var session = SceneEditorSession.Active;
|
|
if ( session is null )
|
|
return;
|
|
|
|
_lastScene.TryGetTarget( out var last );
|
|
|
|
// if query AND scene is unchanged - no need to rebuild the tree
|
|
if ( !queryDirty && ReferenceEquals( last, session.Scene ) )
|
|
return;
|
|
|
|
_lastScene.SetTarget( session.Scene );
|
|
|
|
queryDirty = false;
|
|
Rebuild();
|
|
}
|
|
|
|
private void Rebuild()
|
|
{
|
|
var session = SceneEditorSession.Active;
|
|
|
|
Header.Clear( true );
|
|
|
|
// Copy the current selection as we're about to kill it
|
|
var selection = TreeView.Selection.Select( x => x as GameObject );
|
|
|
|
// treeview will clear the selection, so give it a new one to clear
|
|
TreeView.Selection = new SelectionSystem();
|
|
TreeView.Clear();
|
|
|
|
if ( session is null )
|
|
return;
|
|
|
|
bool hasSearch = !string.IsNullOrEmpty( Search.Text );
|
|
SearchClear.Visible = hasSearch;
|
|
|
|
var scene = session.Scene;
|
|
if ( hasSearch )
|
|
{
|
|
// flat search view
|
|
|
|
var tokens = Regex.Matches( Search.Text, @"(\w+):(\S+)" )
|
|
.ToDictionary( m => m.Groups[1].Value, m => m.Groups[2].Value );
|
|
|
|
var search = Regex.Replace( Search.Text, @"\b\w+:\S+\b", "" ).Trim();
|
|
|
|
IEnumerable<GameObject> objects = Enumerable.Empty<GameObject>();
|
|
if ( tokens.TryGetValue( "id", out string idfilter ) )
|
|
{
|
|
if ( Guid.TryParse( idfilter, out Guid guid ) )
|
|
{
|
|
var obj = scene.Directory.FindByGuid( guid );
|
|
objects = new List<GameObject>() { obj };
|
|
}
|
|
}
|
|
else
|
|
{
|
|
objects = scene.Directory.GetAll();
|
|
}
|
|
|
|
foreach ( var go in objects )
|
|
{
|
|
if ( !go.IsValid() ) continue;
|
|
|
|
if ( go.Parent is null || go.Flags.HasFlag( GameObjectFlags.Hidden ) )
|
|
continue;
|
|
|
|
if ( go.IsPrefabInstance && !go.IsPrefabInstanceRoot )
|
|
continue;
|
|
|
|
if ( !go.Name.Contains( search, StringComparison.OrdinalIgnoreCase ) )
|
|
continue;
|
|
|
|
if ( tokens.TryGetValue( "t", out string typeFilter ) )
|
|
{
|
|
var types = go.Components.GetAll().Select( x => EditorTypeLibrary.GetType( x.GetType() ) );
|
|
if ( types.FirstOrDefault( x => x.Name.Equals( typeFilter, StringComparison.OrdinalIgnoreCase ) ) is null )
|
|
continue;
|
|
}
|
|
|
|
if ( tokens.TryGetValue( "tag", out string tagFilter ) )
|
|
{
|
|
if ( !go.Tags.Contains( tagFilter ) )
|
|
continue;
|
|
}
|
|
|
|
TreeView.AddItem( new GameObjectSearchNode( go ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// normal heirarchy tree
|
|
|
|
if ( scene is PrefabScene prefabScene )
|
|
{
|
|
var node = TreeView.AddItem( new PrefabNode( prefabScene ) );
|
|
TreeView.Open( node );
|
|
}
|
|
else
|
|
{
|
|
var node = TreeView.AddItem( new SceneNode( scene ) );
|
|
TreeView.Open( node );
|
|
}
|
|
}
|
|
|
|
TreeView.Selection = session.Selection;
|
|
|
|
// Go through the current scene
|
|
// Feel like this could be loads faster
|
|
foreach ( var go in scene.GetAllObjects( false ) )
|
|
{
|
|
// If we find a matching item in our new scene
|
|
if ( selection.FirstOrDefault( x => x.IsValid() && x.Id == go.Id ).IsValid() )
|
|
{
|
|
// Add it to the current selection
|
|
TreeView.Selection.Add( go );
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnInspect( EditorUtility.OnInspectArgs args )
|
|
{
|
|
foreach ( var item in TreeView.Selection )
|
|
{
|
|
TreeView.ExpandPathTo( item );
|
|
}
|
|
var scrollTarget = TreeView.Selection.FirstOrDefault();
|
|
if ( scrollTarget is not null )
|
|
{
|
|
TreeView.ScrollTo( scrollTarget );
|
|
}
|
|
}
|
|
}
|
|
|
|
file class AddButton : Widget
|
|
{
|
|
public string Icon;
|
|
|
|
public AddButton( string icon ) : base( null )
|
|
{
|
|
Icon = icon;
|
|
|
|
Cursor = CursorShape.Finger;
|
|
FixedHeight = Theme.RowHeight;
|
|
}
|
|
|
|
protected override Vector2 SizeHint()
|
|
{
|
|
return new Vector2( Theme.RowHeight );
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
Paint.ClearBrush();
|
|
Paint.ClearPen();
|
|
|
|
var color = Enabled ? Theme.ControlBackground : Theme.SurfaceBackground;
|
|
|
|
if ( Enabled && Paint.HasMouseOver )
|
|
{
|
|
color = color.Lighten( 0.1f );
|
|
}
|
|
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( color );
|
|
Paint.DrawRect( LocalRect, Theme.ControlRadius );
|
|
|
|
Paint.ClearBrush();
|
|
Paint.ClearPen();
|
|
Paint.SetPen( Theme.Primary );
|
|
|
|
Paint.DrawIcon( LocalRect, Icon, 14, TextFlag.Center );
|
|
}
|
|
}
|