Files
sbox-public/game/addons/tools/Code/Editor/Inspector.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

395 lines
7.9 KiB
C#

namespace Editor;
[Dock( "Editor", "Inspector", "manage_search" )]
public class Inspector : Widget
{
Layout Editor;
InspectorToolbar Toolbar;
public bool IsLocked = false;
public string CurrentTime => System.DateTime.Now.ToString();
private InspectorWidget _currentInspector;
public Inspector( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Toolbar = new InspectorToolbar( this );
Layout.Add( Toolbar );
Layout.AddSeparator();
Editor = Layout.AddRow( 1 );
Layout.AddStretchCell();
EditorUtility.OnInspect += StartInspecting;
}
protected override void OnPaint()
{
if ( _currentInspector is not null ) return;
Paint.ClearPen();
Paint.ClearBrush();
Paint.SetDefaultFont( italic: true );
Paint.SetPen( Theme.SurfaceLightBackground );
var r = LocalRect;
r.Top += 128;
Paint.DrawText( r, "No object selected.", TextFlag.CenterTop );
}
private void StartInspecting( EditorUtility.OnInspectArgs args )
{
if ( !StartInspecting( args.Object, true ) )
{
args.Cancel = true;
}
}
private bool StartInspecting( object obj, bool addToHistory = true )
{
if ( IsLocked ) return true;
if ( Editor is null ) return true;
using var sx = SuspendUpdates.For( this );
SerializedObject so = obj?.GetSerialized();
if ( obj is Array array )
{
if ( array.Length == 0 )
return true;
if ( array.Length == 1 )
{
so = array.GetValue( 0 )?.GetSerialized();
}
else
{
var mo = new MultiSerializedObject();
for ( int i = 0; i < array.Length; i++ )
{
var val = array?.GetValue( i );
if ( val is null ) continue;
mo.Add( val.GetSerialized() );
}
mo.Rebuild();
so = mo;
}
}
if ( !so.IsValid() )
{
Editor.Clear( true );
return true;
}
if ( IsAlreadyInspectingObject( so ) ) return true;
// The current inspector may not want to be closed, maybe changes are unsaved.
if ( _currentInspector != null && !_currentInspector.CloseInspector( obj ) )
{
return false;
}
Editor.Clear( true );
_currentInspector = InspectorWidget.Create( so );
if ( _currentInspector.IsValid() )
{
Editor.Add( _currentInspector, 1 );
}
else
{
// Try CanEdit still..
// todo: Everything that should be an inspector should be an InspectorWidget
var customeditor = CanEditAttribute.CreateEditorForObject( obj );
if ( customeditor.IsValid() )
{
Editor.Add( customeditor, 1 );
}
else
{
try
{
var sheet = new ControlSheet();
sheet.IncludePropertyNames = true;
sheet.AddObject( so );
var scroller = new ScrollArea( this );
scroller.Canvas = new Widget();
scroller.Canvas.Layout = Layout.Column();
scroller.Canvas.VerticalSizeMode = SizeMode.CanGrow;
scroller.Canvas.HorizontalSizeMode = SizeMode.Flexible;
scroller.Canvas.Layout.Add( sheet );
scroller.Canvas.Layout.AddStretchCell();
Editor.Add( scroller );
}
catch { }
}
}
if ( addToHistory )
{
// Clear everything in our forward
while ( ObjectHistory.Count > HistoryPlace + 1 )
ObjectHistory.RemoveAt( ObjectHistory.Count - 1 );
// Add to history
ObjectHistory.Add( new HistoryReference( obj ) );
HistoryPlace = ObjectHistory.Count - 1;
// limit history size
if ( ObjectHistory.Count > 100 )
{
ObjectHistory.RemoveAt( 0 );
HistoryPlace--;
}
}
// keep buttons updates
UpdateBackForward();
return true;
}
public override void OnDestroyed()
{
base.OnDestroyed();
EditorUtility.OnInspect -= StartInspecting;
}
protected override void OnMousePress( MouseEvent e )
{
if ( e.Button == MouseButtons.Back && GoBack() )
{
e.Accepted = true;
return;
}
if ( e.Button == MouseButtons.Forward && GoForward() )
{
e.Accepted = true;
return;
}
base.OnMousePress( e );
}
public bool GoBack()
{
HistoryPlace = HistoryPlace.Clamp( 0, ObjectHistory.Count );
while ( HistoryPlace > 0 )
{
HistoryPlace--;
if ( JumpToHistory() )
{
UpdateBackForward();
return true;
}
else
{
ObjectHistory.RemoveAt( HistoryPlace );
}
}
UpdateBackForward();
return false;
}
public bool GoForward()
{
HistoryPlace = HistoryPlace.Clamp( 0, ObjectHistory.Count );
while ( HistoryPlace < ObjectHistory.Count - 1 )
{
HistoryPlace++;
if ( JumpToHistory() )
{
UpdateBackForward();
return true;
}
else
{
ObjectHistory.RemoveAt( HistoryPlace );
HistoryPlace--;
}
}
UpdateBackForward();
return false;
}
private bool JumpToHistory()
{
var entry = ObjectHistory[HistoryPlace];
if ( !entry.TryGetTarget( out var target ) )
{
return false;
}
StartInspecting( target, false );
return true;
}
public void ToggleLock()
{
IsLocked = !IsLocked;
Toolbar?.UpdateLock( IsLocked );
if ( !IsLocked )
{
// Update selection when unlocking
var selection = SceneEditorSession.Active?.Selection;
if ( selection is not null && selection.Count > 0 )
{
StartInspecting( EditorUtility.InspectorObject );
}
}
}
void UpdateBackForward()
{
Toolbar.UpdateBackForward( HistoryPlace > 0, HistoryPlace < ObjectHistory.Count() - 1 );
}
[EditorEvent.Frame]
public void Frame()
{
if ( _currentInspector.IsValid() )
{
if ( !_currentInspector.SerializedObject.IsValid() )
{
_currentInspector.Destroy();
return;
}
}
}
private bool IsAlreadyInspectingObject( SerializedObject newObj )
{
if ( _currentInspector is null || _currentInspector.SerializedObject is null )
{
return false;
}
var oldTargets = _currentInspector.SerializedObject.Targets.ToHashSet();
if ( oldTargets.Count != newObj.Targets.Count() ) return false;
foreach ( var newTarget in newObj.Targets )
{
if ( !oldTargets.Contains( newTarget ) )
{
return false;
}
}
return true;
}
public int HistoryPlace = 0;
public List<HistoryReference> ObjectHistory = new();
// Often we're inspecting local arrays of targets, so we store WeakReferences to the actual contents
// to avoid ending up with dangling or invalid references after GC. This wraps that so we don't have to think about it.
public struct HistoryReference
{
List<WeakReference<object>> _objects = new List<WeakReference<object>>();
public HistoryReference( object obj )
{
if ( obj is Array array )
{
foreach ( var element in array )
{
_objects.Add( new WeakReference<object>( element ) );
}
return;
}
_objects.Add( new WeakReference<object>( obj ) );
}
public bool TryGetTarget( out object target )
{
var list = new List<object>();
foreach ( var item in _objects )
{
if ( item.TryGetTarget( out var element ) )
{
// skip stuff that's no longer valid
if ( !((element as IValid)?.IsValid() ?? true) )
continue;
list.Add( element );
}
}
target = list.ToArray();
return list.Any();
}
}
}
public class InspectorToolbar : Widget
{
Inspector Inspector;
IconButton Back;
IconButton Forward;
IconButton Lock;
public InspectorToolbar( Inspector parent ) : base( parent )
{
Inspector = parent;
Layout = Layout.Row();
Layout.Margin = 4;
Layout.Spacing = 4;
//SetIconSize( 16 );
Back = Layout.Add( new IconButton( "arrow_back", () => Inspector.GoBack() ) );
Back.IconSize = 16;
Back.Background = Color.Transparent;
Forward = Layout.Add( new IconButton( "arrow_forward", () => Inspector.GoForward() ) );
Forward.IconSize = 16;
Forward.Background = Color.Transparent;
Layout.AddStretchCell( 1 );
Lock = Layout.Add( new IconButton( "lock_open", () => Inspector.ToggleLock() ) );
Lock.IconSize = 14;
Lock.Background = Color.Transparent;
UpdateBackForward( false, false );
UpdateLock( Inspector.IsLocked );
}
public void UpdateBackForward( bool back, bool forward )
{
Back.Enabled = back;
Forward.Enabled = forward;
}
public void UpdateLock( bool locked )
{
Lock.Icon = locked ? "lock" : "lock_open";
}
}