Editor/Templates/SaveTemplateDialog.cs
using System;
using System.Collections.Generic;
using Editor;
using Grains.RazorDesigner.Document;
using Sandbox;
namespace Grains.RazorDesigner.Templates;
public sealed class SaveTemplateDialog
{
private const string LogPrefix = "[Grains.RazorDesigner]";
private static readonly char[] InvalidNameChars = { '/', '\\', ':', '*', '?', '"', '<', '>', '|' };
private readonly Widget _parent;
private readonly PaletteTemplateStore _store;
private readonly IReadOnlyList<ControlRecord> _selectedRoots;
private readonly ControlRecord _wrapInheritFrom;
private Editor.Dialog _dialog;
private LineEdit _nameEdit;
private Editor.Label _errorLabel;
private Editor.Label _wrapHint;
private Checkbox _wrapCheckbox;
private Editor.Button _saveButton;
private IconHolder _iconHolder;
public SaveTemplateDialog( Widget parent, PaletteTemplateStore store, IReadOnlyList<ControlRecord> selectedRoots, ControlRecord wrapInheritFrom )
{
_parent = parent;
_store = store;
_selectedRoots = selectedRoots;
_wrapInheritFrom = wrapInheritFrom;
_iconHolder = new IconHolder { Icon = "" };
}
public void Show( Action<PaletteTemplate> onConfirm, Action onCancel = null )
{
_dialog = new Editor.Dialog( _parent );
_dialog.Window.WindowTitle = "Save as Template";
_dialog.Window.SetWindowIcon( "bookmark_add" );
_dialog.Window.SetModal( true, true );
_dialog.Window.MinimumWidth = 360;
_dialog.Layout = Layout.Column();
_dialog.Layout.Margin = 16;
_dialog.Layout.Spacing = 10;
// Name field with live validation.
_dialog.Layout.Add( new Editor.Label( _dialog ) { Text = "Name" } );
_nameEdit = new LineEdit( _dialog ) { PlaceholderText = "ButtonRow" };
_nameEdit.TextEdited += _ => RevalidateName();
_dialog.Layout.Add( _nameEdit );
_errorLabel = new Editor.Label( _dialog ) { Text = "" };
_errorLabel.SetStyles( "color: #e07070; font-size: 11px;" );
_dialog.Layout.Add( _errorLabel );
// Icon picker — engine [IconName] + ControlSheet binding.
_dialog.Layout.Add( new Editor.Label( _dialog ) { Text = "Icon" } );
var iconSheet = new ControlSheet();
iconSheet.IncludePropertyNames = false;
var serialized = EditorTypeLibrary.GetSerializedObject( _iconHolder );
iconSheet.AddObject( serialized );
_dialog.Layout.Add( iconSheet );
// Wrap checkbox + hint.
var canWrap = _wrapInheritFrom is not null && _selectedRoots is { Count: > 1 };
_wrapCheckbox = new Checkbox( "Wrap selected controls in a new Panel", _dialog );
_wrapCheckbox.Enabled = canWrap;
_wrapCheckbox.Value = false;
_wrapCheckbox.ToolTip = canWrap
? $"Inherits Direction / Wrap / Justify / Align from \"{_wrapInheritFrom.ClassName}\"."
: "Only meaningful when 2+ siblings are selected.";
_dialog.Layout.Add( _wrapCheckbox );
_wrapHint = new Editor.Label( _dialog )
{
Text = canWrap
? "Disabled when the selection is a single root or spans multiple parents."
: "(Save the selection as-is.)",
};
_wrapHint.SetStyles( "color: #888; font-size: 11px;" );
_dialog.Layout.Add( _wrapHint );
// Buttons.
var buttonRow = _dialog.Layout.Add( Layout.Row() );
buttonRow.Spacing = 6;
buttonRow.AddStretchCell();
var cancelButton = new Editor.Button( _dialog ) { Text = "Cancel", MinimumWidth = 72 };
cancelButton.MouseLeftPress += () =>
{
Log.Info( $"{LogPrefix} SaveTemplateDialog cancelled" );
_dialog.Close();
onCancel?.Invoke();
};
buttonRow.Add( cancelButton );
_saveButton = new Editor.Button( _dialog ) { Text = "Save", MinimumWidth = 72 };
_saveButton.MouseLeftPress += () => OnSave( onConfirm );
buttonRow.Add( _saveButton );
_dialog.Window.AdjustSize();
_dialog.Show();
RevalidateName();
_nameEdit.Focus();
}
private void RevalidateName()
{
var name = (_nameEdit.Text ?? "").Trim();
string error = null;
if ( string.IsNullOrEmpty( name ) )
{
error = "Name required.";
}
else if ( name.IndexOfAny( InvalidNameChars ) >= 0 )
{
error = "Name contains an invalid character (/ \\ : * ? \" < > |).";
}
else if ( _store.NameExists( name ) )
{
error = $"A template named \"{name}\" already exists.";
}
_errorLabel.Text = error ?? "";
_saveButton.Enabled = error is null;
}
private void OnSave( Action<PaletteTemplate> onConfirm )
{
var name = (_nameEdit.Text ?? "").Trim();
var icon = _iconHolder.Icon ?? "";
var wrap = _wrapCheckbox.Enabled && _wrapCheckbox.Value;
IReadOnlyList<ControlRecord> roots;
if ( wrap && _wrapInheritFrom is not null )
{
var wrapper = new ControlRecord
{
Type = ControlType.Panel,
ClassName = "wrapper",
Width = Length.Auto,
Height = Length.Auto,
Direction = _wrapInheritFrom.Direction,
Wrap = _wrapInheritFrom.Wrap,
Justify = _wrapInheritFrom.Justify,
Align = _wrapInheritFrom.Align,
};
foreach ( var r in _selectedRoots )
{
if ( r is null ) continue;
wrapper.Children.Add( CloneSerialisable( r ) );
}
roots = new[] { wrapper };
}
else
{
var list = new List<ControlRecord>( _selectedRoots.Count );
foreach ( var r in _selectedRoots )
{
if ( r is null ) continue;
list.Add( CloneSerialisable( r ) );
}
roots = list;
}
// FilePath is overwritten by Store.Save with the actual on-disk path.
var template = new PaletteTemplate(
Name: name,
IconName: icon,
WrappedInContainer: wrap,
Roots: roots,
FilePath: "" );
Log.Info( $"{LogPrefix} SaveTemplateDialog confirm: \"{name}\", icon=\"{icon}\", wrap={wrap}, roots={roots.Count}" );
_dialog.Close();
onConfirm?.Invoke( template );
}
private static ControlRecord CloneSerialisable( ControlRecord src )
{
var clone = new ControlRecord
{
Type = src.Type,
ClassName = src.ClassName,
};
src.CopyFieldsTo( clone );
foreach ( var c in src.Children )
clone.Children.Add( CloneSerialisable( c ) );
return clone;
}
private sealed class IconHolder
{
[IconName]
[Title( "" )]
public string Icon { get; set; }
}
}