Editor-side presets UI for the Advanced Mapping Tools tools. Lists saved presets for the current tool, applies, renames, duplicates, deletes, searches, saves, exports/imports JSON and resets to defaults, plus a static clipboard for copying settings between tools.
namespace AdvancedMappingTools;
/// <summary>
/// Settings copied from one tool, to paste into the same tool or another one. Only values whose
/// name and type the other tool has go across; a part tool's generator settings only into a tool
/// of the same kind.
/// </summary>
public static class ToolSettingsClipboard
{
public static string Tool { get; private set; }
public static string ToolTitle { get; private set; }
static Dictionary<string, string> _values;
static Dictionary<string, string> _types;
public static bool Has => _values is not null;
public static void Copy( BuilderEditor editor )
{
Tool = editor.GetType().Name;
ToolTitle = editor.Title;
_values = editor.CaptureSettings();
_types = editor.SettingTypes();
}
/// <summary>
/// Puts what fits into the editor. Returns how many values went across and how many were copied.
/// </summary>
public static (int Pasted, int Copied) PasteInto( BuilderEditor editor )
{
if ( !Has ) return (0, 0);
var sameTool = editor.GetType().Name == Tool;
var targetTypes = editor.SettingTypes();
var fitting = _values
.Where( x => sameTool || (_types.TryGetValue( x.Key, out var type ) && targetTypes.TryGetValue( x.Key, out var target ) && type == target) )
.ToDictionary( x => x.Key, x => x.Value );
editor.ApplySettings( fitting );
return (fitting.Count, _values.Count);
}
}
/// <summary>
/// The presets of a tool as a list: a click applies one, the buttons on a row rename, copy or
/// delete it. The preset the current settings match is marked. Below: save, copy and paste of
/// settings, export and import, and reset.
/// </summary>
public sealed class PresetPanel : Widget
{
readonly BuilderEditor _editor;
readonly Action _applied;
readonly Widget _list;
readonly LineEdit _search;
string TypeName => _editor.GetType().Name;
public PresetPanel( Widget parent, BuilderEditor editor, Action applied ) : base( parent )
{
_editor = editor;
_applied = applied;
Layout = Layout.Column();
Layout.Spacing = 4;
Layout.Margin = new Sandbox.UI.Margin( 4, 0, 4, 0 );
_search = Layout.Add( new LineEdit( this ) { PlaceholderText = "Find a preset…", ClearButtonEnabled = true } );
_search.TextEdited += _ => Refresh();
_list = Layout.Add( new Widget( this ) { Layout = Layout.Column() } );
_list.Layout.Spacing = 1;
var save = Layout.AddRow();
save.Spacing = 4;
save.Add( new ListButton( "Save As Preset", "bookmark_add" ) { Clicked = Save, ToolTip = "Keep the current settings under a name." }, 1 );
var clip = Layout.AddRow();
clip.Spacing = 4;
clip.Add( new ListButton( "Copy Settings", "content_copy" ) { Clicked = Copy, ToolTip = "Copy every setting of this tool, to paste here later or into another tool." }, 1 );
clip.Add( new ListButton( "Paste", "content_paste" ) { Clicked = Paste, ToolTip = "Take over the copied settings. From another tool only the settings this tool has as well." }, 1 );
var files = Layout.AddRow();
files.Spacing = 4;
files.Add( new ListButton( "Export", "upload_file" ) { Clicked = Export, ToolTip = "Save the presets of this tool to a file, to hand to someone else." }, 1 );
files.Add( new ListButton( "Import", "file_open" ) { Clicked = Import, ToolTip = "Add the presets from a file. Names that exist already get a number." }, 1 );
Layout.Add( new ListButton( "Reset To Defaults", "restart_alt" ) { Clicked = Reset, ToolTip = "Every setting of this tool back to how it came. Saved presets stay." } );
Refresh();
}
Dictionary<string, Dictionary<string, string>> Presets => BuilderStore.LoadPresets( TypeName );
/// <summary>
/// True when every value the preset holds is what the tool has now.
/// </summary>
bool InUse( Dictionary<string, string> preset )
{
var current = _editor.CaptureSettings();
return preset.All( x => current.TryGetValue( x.Key, out var value ) && value == x.Value );
}
public void Refresh()
{
_list.Layout.Clear( true );
var presets = Presets;
_search.Visible = presets.Count >= 6;
var filter = _search.Text?.Trim() ?? "";
var shown = presets.Where( x => filter == "" || x.Key.Contains( filter, StringComparison.OrdinalIgnoreCase ) ).OrderBy( x => x.Key ).ToList();
if ( presets.Count == 0 )
_list.Layout.Add( new Label( "No presets yet. Save the current settings to come back to them.", _list ) { WordWrap = true, Color = Theme.TextLight } );
else if ( shown.Count == 0 )
_list.Layout.Add( new Label( $"No preset is called “{filter}”.", _list ) { Color = Theme.TextLight } );
foreach ( var (name, settings) in shown )
{
var preset = name;
_list.Layout.Add( new PresetRow( _list, name, InUse( settings ) )
{
Applied = () => Apply( preset ),
Renamed = () => Rename( preset ),
Duplicated = () => Duplicate( preset ),
Deleted = () => Delete( preset )
} );
}
}
void Apply( string name )
{
if ( !Presets.TryGetValue( name, out var settings ) ) return;
_editor.ApplySettings( settings );
_applied?.Invoke();
}
void Save()
{
Dialog.AskString( name =>
{
name = name?.Trim();
if ( string.IsNullOrEmpty( name ) ) return;
var presets = Presets;
if ( presets.ContainsKey( name ) )
{
Dialog.AskConfirm( () => Store( name ), $"A preset called “{name}” exists already. Replace it with the current settings?", "Replace preset", "Replace" );
return;
}
Store( name );
}, "Name of this preset", "Save", initialName: $"{_editor.Title} {Presets.Count + 1}", title: "Save preset", minLength: 1 );
}
void Store( string name )
{
var presets = Presets;
presets[name] = _editor.CaptureSettings();
BuilderStore.SavePresets( TypeName, presets );
Refresh();
}
void Rename( string name )
{
Dialog.AskString( newName =>
{
newName = newName?.Trim();
var presets = Presets;
if ( string.IsNullOrEmpty( newName ) || newName == name || !presets.TryGetValue( name, out var settings ) ) return;
if ( presets.ContainsKey( newName ) )
{
Log.Warning( $"{_editor.Title}: a preset called {newName} exists already." );
return;
}
presets.Remove( name );
presets[newName] = settings;
BuilderStore.SavePresets( TypeName, presets );
Refresh();
}, "New name", "Rename", initialName: name, title: "Rename preset", minLength: 1 );
}
void Duplicate( string name )
{
var presets = Presets;
if ( !presets.TryGetValue( name, out var settings ) ) return;
var copy = $"{name} copy";
for ( var i = 2; presets.ContainsKey( copy ); i++ ) copy = $"{name} copy {i}";
presets[copy] = new Dictionary<string, string>( settings );
BuilderStore.SavePresets( TypeName, presets );
Refresh();
}
void Delete( string name )
{
Dialog.AskConfirm( () =>
{
var presets = Presets;
if ( !presets.Remove( name ) ) return;
BuilderStore.SavePresets( TypeName, presets );
Refresh();
}, $"Delete the preset “{name}”? This cannot be undone.", "Delete preset", "Delete" );
}
void Copy()
{
ToolSettingsClipboard.Copy( _editor );
Log.Info( $"{_editor.Title}: settings copied." );
}
void Paste()
{
if ( !ToolSettingsClipboard.Has )
{
Log.Info( "No settings copied yet. Use Copy Settings in a tool first." );
return;
}
var (pasted, copied) = ToolSettingsClipboard.PasteInto( _editor );
Log.Info( ToolSettingsClipboard.Tool == TypeName
? $"{_editor.Title}: settings pasted."
: $"{_editor.Title}: {pasted} of {copied} settings from {ToolSettingsClipboard.ToolTitle} fit and were pasted." );
_applied?.Invoke();
}
void Export()
{
var dialog = new FileDialog( null );
dialog.Title = $"Export {_editor.Title} Presets";
dialog.Directory = Environment.GetFolderPath( Environment.SpecialFolder.MyDocuments );
dialog.DefaultSuffix = ".json";
dialog.SelectFile( $"{_editor.Title} Presets.json" );
dialog.SetFindFile();
dialog.SetModeSave();
dialog.SetNameFilter( "Presets (*.json)" );
if ( !dialog.Execute() ) return;
var count = BuilderStore.ExportPresets( TypeName, dialog.SelectedFile, _editor.CaptureSettings() );
Log.Info( $"{count} {_editor.Title} preset(s) saved to {dialog.SelectedFile}" );
}
void Import()
{
var dialog = new FileDialog( null );
dialog.Title = $"Import {_editor.Title} Presets";
dialog.Directory = Environment.GetFolderPath( Environment.SpecialFolder.MyDocuments );
dialog.SetFindFile();
dialog.SetModeOpen();
dialog.SetNameFilter( "Presets (*.json)" );
if ( !dialog.Execute() ) return;
var (added, problem) = BuilderStore.ImportPresets( TypeName, dialog.SelectedFile );
if ( problem is not null )
{
Log.Warning( $"{_editor.Title}: {problem}" );
return;
}
Log.Info( $"{added} {_editor.Title} preset(s) added" );
Refresh();
}
void Reset()
{
Dialog.AskConfirm( () =>
{
_editor.ApplySettings( BuilderEditor.Defaults( _editor.GetType() ) );
_applied?.Invoke();
}, $"Set every setting of {_editor.Title} back to its default? Saved presets stay as they are." );
}
/// <summary>
/// One preset: its name, a mark when the settings match it, and on hover rename, duplicate and delete.
/// </summary>
sealed class PresetRow : Widget
{
const float Button = 24.0f;
static readonly string[] Icons = ["edit", "content_copy", "delete"];
static readonly string[] Tips = ["Rename", "Duplicate", "Delete"];
readonly string _name;
readonly bool _inUse;
Vector2 _mouse;
public Action Applied { get; set; }
public Action Renamed { get; set; }
public Action Duplicated { get; set; }
public Action Deleted { get; set; }
public PresetRow( Widget parent, string name, bool inUse ) : base( parent )
{
_name = name;
_inUse = inUse;
FixedHeight = 28;
}
int HoveredButton => IsUnderMouse && _mouse.x > Width - Button * Icons.Length ? (int)((_mouse.x - (Width - Button * Icons.Length)) / Button) : -1;
protected override void OnPaint()
{
var rect = LocalRect;
Paint.ClearPen();
if ( _inUse )
{
Paint.SetBrush( Theme.Primary.WithAlpha( 0.22f ) );
Paint.DrawRect( rect, 4 );
Paint.SetBrush( Theme.Primary );
Paint.DrawRect( new Rect( 0, 4, 3, rect.Height - 8 ), 1.5f );
}
else if ( IsUnderMouse )
{
Paint.SetBrush( Theme.SurfaceBackground.Lighten( 0.15f ) );
Paint.DrawRect( rect, 4 );
}
Paint.SetPen( _inUse ? Theme.Primary.Lighten( 0.4f ) : Theme.TextLight );
Paint.DrawIcon( new Rect( 8, 0, 18, rect.Height ), _inUse ? "bookmark" : "bookmark_border", 15, TextFlag.Center );
var right = IsUnderMouse ? rect.Width - Button * Icons.Length - 4 : rect.Width - 8;
if ( _inUse && !IsUnderMouse )
{
Paint.SetDefaultFont( 7, 400, false, false );
Paint.SetPen( Theme.Primary.Lighten( 0.35f ) );
Paint.DrawText( new Rect( rect.Width - 60, 0, 52, rect.Height ), "in use", TextFlag.RightCenter | TextFlag.SingleLine );
right -= 56;
}
Paint.SetPen( Theme.Text );
Paint.SetDefaultFont( 8, _inUse ? 600 : 400, false, false );
var text = new Rect( 32, 0, MathF.Max( right - 32, 10 ), rect.Height );
Paint.DrawText( text, Paint.GetElidedText( _name, text.Width, ElideMode.Right, TextFlag.SingleLine ), TextFlag.LeftCenter | TextFlag.SingleLine );
if ( !IsUnderMouse ) return;
var hovered = HoveredButton;
for ( var i = 0; i < Icons.Length; i++ )
{
var box = new Rect( rect.Width - Button * (Icons.Length - i), 2, Button, rect.Height - 4 );
if ( i == hovered )
{
Paint.ClearPen();
Paint.SetBrush( Theme.SurfaceBackground.Lighten( 0.3f ) );
Paint.DrawRect( box, 4 );
}
Paint.SetPen( i == hovered ? (i == 2 ? Theme.Red : Theme.Text) : Theme.TextLight );
Paint.DrawIcon( box, Icons[i], 14, TextFlag.Center );
}
}
protected override void OnMouseEnter() => Update();
protected override void OnMouseLeave() => Update();
protected override void OnMouseMove( MouseEvent e )
{
_mouse = e.LocalPosition;
var b = HoveredButton;
ToolTip = b >= 0 ? Tips[b] : $"Apply {_name}";
Update();
}
protected override void OnMousePress( MouseEvent e )
{
if ( !e.LeftMouseButton ) return;
_mouse = e.LocalPosition;
switch ( HoveredButton )
{
case 0: Renamed?.Invoke(); break;
case 1: Duplicated?.Invoke(); break;
case 2: Deleted?.Invoke(); break;
default: Applied?.Invoke(); break;
}
}
}
}