Editor/ScfuConfigWindow.cs
using Editor;
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace Sandbox;
/// <summary>
/// Editor dialog for the <c>.scfu-config.json</c> file. Loads the config (or
/// defaults if absent), edits per-section via tabbed <see cref="ControlSheet"/>s
/// and writes back. Unknown fields the editor schema doesn't model are
/// preserved through each section's <c>[JsonExtensionData]</c> bag.
/// </summary>
public sealed class ScfuConfigWindow : Dialog
{
const string PreferredConfigName = ".scfu-config.json";
const string LegacyConfigName = "scfu.json";
readonly string _projectRoot;
readonly string _configPath;
readonly bool _configExists;
ScfuConfigSchema _config;
Label _statusLabel;
public ScfuConfigWindow( string projectRoot )
{
_projectRoot = projectRoot;
var preferred = Path.Combine( projectRoot, PreferredConfigName );
var legacy = Path.Combine( projectRoot, LegacyConfigName );
if ( File.Exists( preferred ) )
{
_configPath = preferred;
_configExists = true;
}
else if ( File.Exists( legacy ) )
{
_configPath = legacy;
_configExists = true;
}
else
{
_configPath = preferred;
_configExists = false;
}
_config = LoadConfig();
Window.SetModal( true, true );
Window.SetWindowIcon( "tune" );
Window.Title = "SCFU Configuration";
Window.Size = new Vector2( 760, 820 );
Window.MinimumSize = new Vector2( 640, 600 );
Layout = Layout.Column();
Layout.Margin = 12;
Layout.Spacing = 8;
BuildHeader();
BuildTabs();
BuildFooter();
}
void BuildHeader()
{
var header = Layout.AddColumn();
header.Spacing = 2;
header.Add( new Label.Header( "SCFU Configuration" ) );
var subtitle = new Label( _configExists ? $"Editing {Path.GetFileName( _configPath )}" : $"No config yet - defaults shown. Will write {PreferredConfigName} on save." );
subtitle.WordWrap = true;
header.Add( subtitle );
var pathLabel = new Label( _configPath ) { Color = Color.Gray };
pathLabel.WordWrap = true;
header.Add( pathLabel );
}
void BuildTabs()
{
var tabs = new TabWidget( this );
tabs.StateCookie = "scfu.config.tab";
tabs.AddPage( "General", "settings", BuildSectionPage( _config, includeOnly: GeneralTopLevelNames ) );
tabs.AddPage( "Safety", "shield", BuildSectionPage( _config.Safety ) );
tabs.AddPage( "C#", "code", BuildSectionPage( _config.CSharp ) );
tabs.AddPage( "Razor", "web", BuildSectionPage( _config.Razor ) );
tabs.AddPage( "SCSS", "format_paint", BuildSectionPage( _config.Scss ) );
tabs.AddPage( "JSON Assets", "data_object", BuildSectionPage( _config.JsonAssets ) );
tabs.AddPage( "Filenames", "folder", BuildSectionPage( _config.Filenames ) );
Layout.Add( tabs, 1 );
}
static readonly string[] GeneralTopLevelNames =
{
nameof( ScfuConfigSchema.Version ),
nameof( ScfuConfigSchema.Seed ),
nameof( ScfuConfigSchema.Include ),
nameof( ScfuConfigSchema.Exclude ),
};
Widget BuildSectionPage( object section, string[] includeOnly = null )
{
var page = new Widget( this );
page.Layout = Layout.Column();
page.Layout.Margin = 0;
var scroll = new ScrollArea( page );
scroll.Canvas = new Widget( scroll );
scroll.Canvas.Layout = Layout.Column();
scroll.Canvas.Layout.Margin = new Sandbox.UI.Margin( 12, 12 );
scroll.Canvas.Layout.Spacing = 4;
var so = EditorTypeLibrary.GetSerializedObject( section );
ControlSheet sheet;
if ( includeOnly is null )
{
sheet = ControlSheet.Create( so );
}
else
{
sheet = new ControlSheet();
foreach ( var name in includeOnly )
{
var prop = so.GetProperty( name );
if ( prop is null ) continue;
sheet.AddRow( prop );
}
}
scroll.Canvas.Layout.Add( sheet );
scroll.Canvas.Layout.AddStretchCell();
page.Layout.Add( scroll, 1 );
return page;
}
void BuildFooter()
{
var footer = Layout.AddRow();
footer.Spacing = 8;
_statusLabel = new Label( "" ) { Color = Color.Gray };
footer.Add( _statusLabel, 1 );
footer.AddStretchCell();
var reset = new Button( "Reset to Defaults", "history" );
reset.ToolTip = "Replace the in-memory config with fresh defaults. Not saved until you click Save.";
reset.Clicked = OnReset;
footer.Add( reset );
var cancel = new Button( "Cancel" );
cancel.Clicked = Close;
footer.Add( cancel );
var save = new Button.Primary( "Save", "save" );
save.Clicked = OnSave;
footer.Add( save );
}
ScfuConfigSchema LoadConfig()
{
if ( !_configExists )
return new ScfuConfigSchema();
try
{
var text = File.ReadAllText( _configPath );
return JsonSerializer.Deserialize<ScfuConfigSchema>( text, ScfuConfigSchema.JsonOptions ) ?? new ScfuConfigSchema();
}
catch ( Exception ex )
{
Log.Warning( $"[SCFU] Failed to load {_configPath}: {ex.Message}. Showing defaults." );
return new ScfuConfigSchema();
}
}
void OnReset()
{
_config = new ScfuConfigSchema();
Layout.Clear( true );
BuildHeader();
BuildTabs();
BuildFooter();
SetStatus( "Reset to defaults (unsaved)." );
}
void OnSave()
{
try
{
var json = JsonSerializer.Serialize( _config, ScfuConfigSchema.JsonOptions );
Directory.CreateDirectory( Path.GetDirectoryName( _configPath )! );
File.WriteAllText( _configPath, json );
Log.Info( $"[SCFU] Wrote {_configPath}" );
Close();
}
catch ( Exception ex )
{
Log.Warning( $"[SCFU] Save failed: {ex}" );
SetStatus( $"Save failed: {ex.Message}", error: true );
}
}
void SetStatus( string msg, bool error = false )
{
if ( !_statusLabel.IsValid() ) return;
_statusLabel.Text = msg;
_statusLabel.Color = error ? Color.Red : Color.Gray;
}
[EditorEvent.Frame]
void CheckKeys()
{
if ( !Window.IsValid() || !Window.Visible ) return;
if ( Editor.Application.IsKeyDown( KeyCode.Escape ) ) Close();
}
}