Editor/Inspector/PanelPropertyEditor.cs
using Sandbox;
using Sandbox.UI;
using System;
using System.Linq;
using System.Reflection;

namespace Panelize;

public class PanelPropertyEditor : Widget
{
	Panel panel;
	PanelEditorSession session;
	PropertySheet sheet;
	public PanelPropertyEditor( Widget parent, Panel panel, PanelEditorSession session ) : base( parent )
	{
		ArgumentNullException.ThrowIfNull( panel );
		Layout = Layout.Column();

		this.panel = panel;
		this.session = session;

		Rebuild();
	}

	public void Rebuild()
	{
		Layout.Clear( true );

		sheet?.Clear( true );
		sheet = new();
		sheet.PropertyOrder = ( prop ) => prop.DisplayName;

		var scroller = Layout.Add(new ScrollArea( this ));
		scroller.Canvas = new Widget();
		scroller.Canvas.Layout = Layout.Column();
		scroller.Canvas.Layout.Add( sheet );
		scroller.Canvas.Layout.AddStretchCell(1);

		BuildGenericProperties( panel );
		sheet.Rebuild();
	}

	[EditorEvent.Frame]
	public void Frame()
	{
		if ( SetContentHash( HashCode.Combine( panel ), 0.1f ) )
		{
			Rebuild();
		}
	}

	
	private void BuildGenericProperties( Panel panel)
	{
		Type panelType = panel.GetType();
		var properties = panelType.GetProperties( BindingFlags.Instance | BindingFlags.Public )
							.Select( prop => BuildProperty(prop, panel) )
							.OrderBy( prop => prop.Name );

		foreach ( SerializedProperty prop in properties )
		{
			if ( !CanEdit(prop)) continue;
			sheet.AddProperty( prop );
		}
	}

	private CustomSerializedProperty BuildProperty(PropertyInfo prop, Panel panel)
	{
		var sp = new CustomSerializedProperty( prop, panel );
		sp.CustomGroup = prop.DeclaringType.Name;
		sp.OnSetValue += (val) => PropertyEdited(panel, prop, val);
		return sp;
	}

	private void PropertyEdited( Panel panel, PropertyInfo prop, object val )
	{
		//Log.Info( $"Property edited: {panel} [{prop.Name} = {val}]" );
		var state = session.GetPanelState( panel );
		state.SetProperty( prop.Name, val );
		session.SetPanelState( panel, state );
	}

	private bool CanEdit(SerializedProperty prop)
	{
		// Ignore these properties because
		// a) we already have an editor for them elsewhere or
		// b) there is no reason for a user to edit them in this tool.
		string[] ignoredProperties = [
			"Id",
			"Classes",
			"Scene",
			"ChildContent",
			"PseudoClass",
			"ElementName"
		];

		return prop.IsEditable && !ignoredProperties.Contains( prop.Name );
	}
}