Editor utility that injects a read-only metric row into the Transform component editors in the editor UI. It scans the editor widget tree periodically, finds GameObjectInspector entries, locates the Transform component editor, builds a customized SerializedProperty for the LocalPosition, adds a MetersAttribute and display name, and appends a ControlSheet row to the ComponentEditorWidget layout once per widget.
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Editor;
using Editor.Inspectors;
using Sandbox;
namespace Metrify;
public static class MetricRowInjector
{
private static readonly ConditionalWeakTable<Widget, object> _injected = new();
private static RealTimeSince _sinceLastScan;
[EditorEvent.Frame]
public static void OnFrame()
{
// The UI tree only changes on selection/edit; a 10Hz scan is plenty and keeps the
// recursive traversal off the per-frame cost.
if ( _sinceLastScan < 0.1f ) return;
_sinceLastScan = 0;
var root = EditorWindow;
if ( !root.IsValid() ) return;
foreach ( var inspector in FindDescendants<GameObjectInspector>( root ) )
{
var transformSheet = FindDescendants<ComponentSheet>( inspector )
.FirstOrDefault( s => s.Header?.Title == "Transform" );
if ( !transformSheet.IsValid() ) continue;
var editorWidget = FindDescendants<ComponentEditorWidget>( transformSheet ).FirstOrDefault();
if ( !editorWidget.IsValid() || editorWidget.Layout is null ) continue;
if ( _injected.TryGetValue( editorWidget, out _ ) ) continue;
var metric = BuildMetricProperty( inspector.SerializedObject );
if ( metric is null ) continue;
var sheet = new ControlSheet();
sheet.IncludePropertyNames = true;
sheet.AddRow( metric );
editorWidget.Layout.Add( sheet );
_injected.Add( editorWidget, null );
}
}
private static SerializedProperty BuildMetricProperty( SerializedObject inspectorObject )
{
var transformProp = inspectorObject?.GetProperty( nameof( GameObject.Transform ) );
if ( transformProp is null ) return null;
if ( !transformProp.TryGetAsObject( out var transform ) ) return null;
var localPosition = transform.GetProperty( "LocalPosition" );
if ( localPosition is null ) return null;
var customizable = localPosition.GetCustomizable();
customizable.SetDisplayName( "Local Position (meters)" );
customizable.AddAttribute( new MetersAttribute() );
return customizable;
}
private static IEnumerable<T> FindDescendants<T>( Widget root ) where T : Widget
{
foreach ( var child in root.Children )
{
if ( !child.IsValid() ) continue;
if ( child is T match )
yield return match;
foreach ( var descendant in FindDescendants<T>( child ) )
yield return descendant;
}
}
}