Editor/Commands/SuiDuplicateElementCommand.cs
using System.Collections.Generic;
using SboxUiDesigner.Runtime;
namespace SboxUiDesigner.EditorUi.Commands;
/// <summary>
/// Duplicate an element together with its descendants. The clone gets fresh
/// ids throughout the subtree so it does not collide with the original. The
/// duplicate is inserted as a sibling immediately after the source.
///
/// Cannot duplicate the root element (it has no parent).
/// </summary>
public sealed class SuiDuplicateElementCommand : ISuiCommand
{
private readonly string _sourceElementId;
private List<SuiElement> _addedElements;
private string _parentId;
private int _insertIndex = -1;
private string _newRootId;
public string Description => "Duplicate";
/// <summary>The id of the cloned root element after Apply has run. Useful so the
/// controller can select the duplicate.</summary>
public string ResultingElementId => _newRootId;
public SuiDuplicateElementCommand( string sourceElementId )
{
_sourceElementId = sourceElementId;
}
public void Apply( SuiDocument doc )
{
if ( doc == null ) return;
var source = doc.GetElement( _sourceElementId );
if ( source == null || string.IsNullOrEmpty( source.ParentId ) )
return; // cannot duplicate root or unknown
_parentId = source.ParentId;
var parent = doc.GetElement( _parentId );
if ( parent == null ) return;
var byId = new Dictionary<string, SuiElement>();
foreach ( var el in doc.Elements )
if ( !string.IsNullOrEmpty( el.Id ) ) byId[el.Id] = el;
_addedElements = new List<SuiElement>();
var clonedRoot = CloneSubtree( source, byId, _addedElements );
clonedRoot.ParentId = _parentId;
_newRootId = clonedRoot.Id;
// Give the duplicate a unique name so the user can tell it apart in the
// hierarchy. Strips a trailing _N from the source so "Panel_2" → "Panel_3"
// rather than "Panel_2_2". ClassName follows Name (matches the convention
// used by AddElement).
clonedRoot.Name = SuggestUniqueDuplicateName( doc, source.Name );
if ( clonedRoot.Style != null )
clonedRoot.Style.ClassName = SuiDocumentValidator.SanitizeClassName( clonedRoot.Name );
var sourceIdx = parent.Children.IndexOf( _sourceElementId );
_insertIndex = sourceIdx >= 0 ? sourceIdx + 1 : parent.Children.Count;
parent.Children.Insert( _insertIndex, clonedRoot.Id );
foreach ( var el in _addedElements ) doc.Elements.Add( el );
}
private static string SuggestUniqueDuplicateName( SuiDocument doc, string sourceName )
{
if ( string.IsNullOrEmpty( sourceName ) ) sourceName = "Element";
// Strip an existing "_<int>" suffix so duplicating "Panel_2" picks up at
// "Panel_3" instead of producing "Panel_2_2".
var baseName = sourceName;
var underscore = sourceName.LastIndexOf( '_' );
if ( underscore > 0 && underscore < sourceName.Length - 1
&& int.TryParse( sourceName.Substring( underscore + 1 ), out _ ) )
{
baseName = sourceName.Substring( 0, underscore );
}
for ( int i = 2; i < 1000; i++ )
{
var candidate = $"{baseName}_{i}";
if ( !NameExists( doc, candidate ) ) return candidate;
}
return $"{baseName}_{System.Guid.NewGuid().ToString( "N" ).Substring( 0, 4 )}";
}
private static bool NameExists( SuiDocument doc, string name )
{
foreach ( var el in doc.Elements )
if ( string.Equals( el.Name, name, System.StringComparison.OrdinalIgnoreCase ) ) return true;
return false;
}
public void Undo( SuiDocument doc )
{
if ( doc == null || _addedElements == null ) return;
var parent = doc.GetElement( _parentId );
if ( parent != null && _insertIndex >= 0 && _insertIndex < parent.Children.Count )
parent.Children.RemoveAt( _insertIndex );
foreach ( var el in _addedElements ) doc.Elements.Remove( el );
_addedElements = null;
}
private static SuiElement CloneSubtree(
SuiElement source,
Dictionary<string, SuiElement> byId,
List<SuiElement> output )
{
var clone = source.Clone();
clone.Id = SuiDocument.NewElementId();
clone.Children = new List<string>();
output.Add( clone );
foreach ( var childId in source.Children )
{
if ( !byId.TryGetValue( childId, out var child ) ) continue;
var clonedChild = CloneSubtree( child, byId, output );
clonedChild.ParentId = clone.Id;
clone.Children.Add( clonedChild.Id );
}
return clone;
}
}