Editor/Widgets/InstancedListControlWidget.cs
using System;
using System.Collections.Generic;
using System.Linq;
using DataTables;
using Editor;
using Sandbox;
using Json = Sandbox.Json;
namespace DataTablesEditor;
[CustomEditor( typeof(List<>), WithAllAttributes = new[] { typeof(JsonTypeAnnotateAttribute), typeof(InstancedAttribute) } )]
[CustomEditor( typeof(System.Array), WithAllAttributes = new[] { typeof(JsonTypeAnnotateAttribute), typeof(InstancedAttribute) } )]
public class InstancedListControlWidget : ControlWidget
{
public override bool SupportsMultiEdit => false;
internal SerializedCollection Collection;
Layout Content;
IconButton addButton;
bool preventRebuild = false;
public Type ListType;
public InstancedListControlWidget( SerializedProperty property )
: this( property, GetCollection( property ) )
{
}
private static SerializedCollection GetCollection( SerializedProperty property )
{
if ( !property.TryGetAsObject( out var so ) || so is not SerializedCollection sc )
return null;
return sc;
}
public InstancedListControlWidget( SerializedProperty property, SerializedCollection sc )
: base( property )
{
Layout = Layout.Column();
Layout.Spacing = 2;
if ( sc is null ) return;
ListType = property.PropertyType.GetGenericArguments().First();
Collection = sc;
Collection.OnEntryAdded = Rebuild;
Collection.OnEntryRemoved = Rebuild;
Content = Layout.Column();
Layout.Add( Content );
Rebuild();
}
public void Rebuild()
{
if ( preventRebuild ) return;
using var _ = SuspendUpdates.For( this );
Content.Clear( true );
Content.Margin = 0;
var column = Layout.Column();
int index = 0;
foreach ( var entry in Collection )
{
column.Add( new ListEntryWidget( this, entry, index ) );
index++;
}
// bottom row
if ( !IsControlDisabled )
{
var buttonRow = Layout.Row();
buttonRow.Margin = new Sandbox.UI.Margin( ControlRowHeight + 2, 0, 0, 0 );
addButton = new IconButton( "add" )
{
Background = Theme.ControlBackground,
ToolTip = "Add Element",
FixedWidth = ControlRowHeight,
FixedHeight = ControlRowHeight
};
addButton.MouseClick = AddEntry;
buttonRow.Add( addButton );
buttonRow.AddStretchCell( 1 );
column.Add( buttonRow );
}
Content.Add( column );
}
void AddEntry()
{
Collection.Add( null );
}
void RemoveEntry( int index )
{
Collection.RemoveAt( index );
}
void DuplicateEntry( int index )
{
var sourceProperty = Collection.Skip( index ).First();
var sourceObj = sourceProperty.GetValue<object>();
var sourceJson = Json.ToNode( sourceObj );
Collection.Add( index + 1, Json.FromNode( sourceJson, sourceProperty.PropertyType ) );
}
protected override void OnPaint()
{
Paint.Antialiasing = true;
Paint.ClearPen();
Paint.SetBrush( Theme.ControlText.Darken( 0.6f ) );
}
// individual list entry
class ListEntryWidget : Widget
{
Drag dragData;
bool draggingAbove = false;
bool draggingBelow = false;
InstancedListControlWidget ListWidget;
int Index = -1;
public ControlSheet Sheet;
public ListEntryWidget( InstancedListControlWidget parent, SerializedProperty property, int index ) : base( parent )
{
ListWidget = parent;
Index = index;
Layout = Layout.Row();
Layout.Margin = new Sandbox.UI.Margin( 0, 2 );
Layout.Spacing = 2;
ReadOnly = parent.ReadOnly;
Enabled = parent.Enabled;
ToolTip = $"Element {Index}";
var control = Create( property );
control.ReadOnly = ReadOnly;
control.Enabled = Enabled;
if ( control.IsControlDisabled )
{
Layout.Add( control );
}
else
{
IsDraggable = !control.IsControlDisabled;
var dragHandle = new DragHandle( this )
{
IconSize = 13,
Foreground = Theme.ControlText,
Background = Color.Transparent,
FixedWidth = ControlRowHeight,
FixedHeight = ControlRowHeight
};
var removeButton = new IconButton( "clear", () => parent.RemoveEntry( index ) )
{
ToolTip = "Remove",
Background = Theme.ControlBackground,
FixedWidth = ControlRowHeight,
FixedHeight = ControlRowHeight
};
Layout.Add( dragHandle );
//Layout.Add( control );
var col = new Widget();
col.Layout = Layout.Column();
Sheet = new ControlSheet();
var dropdown = new Dropdown( "Select a class..." );
object instance = null;
var firstType = TypeLibrary.GetTypes().FirstOrDefault( x => x.TargetType == ListWidget.ListType );
if ( firstType is not null )
{
instance = Activator.CreateInstance( firstType.TargetType );
dropdown.Value = instance.GetType();
Sheet.Clear( true );
Sheet.AddObject( instance.GetSerialized() );
}
dropdown.PopulatePopup = widget =>
{
var structTypes = TypeLibrary.GetTypes().Where( x => x.TargetType == ListWidget.ListType || x.TargetType.IsSubclassOf( ListWidget.ListType ) );
foreach ( var structType in structTypes )
{
var btn = new DropdownButton( dropdown, structType.Name );
btn.Value = structType.TargetType;
btn.Icon = "account_tree";
btn.Clicked = () =>
{
var instance = Activator.CreateInstance( structType.TargetType );
property.SetValue( instance );
Sheet.Clear( true );
Sheet.AddObject( instance.GetSerialized() );
};
widget.Layout.Add( btn );
}
};
var value = property.GetValue<object>();
if ( value is not null )
{
dropdown.Icon = "account_tree";
dropdown.Text = value.GetType().Name;
dropdown.Value = value.GetType();
Sheet.Clear( true );
Sheet.AddObject( value.GetSerialized() );
}
else
{
dropdown.Icon = "error";
if ( instance is not null )
{
dropdown.Icon = "account_tree";
dropdown.Text = instance.GetType().Name;
dropdown.Value = instance.GetType();
property.SetValue( instance );
}
}
col.Layout.Add( dropdown );
col.Layout.Add( Sheet );
Layout.Add( col );
Layout.Add( removeButton );
dragHandle.MouseRightClick += () =>
{
var menu = new ContextMenu( this );
menu.AddOption( "Remove", "clear", () => parent.RemoveEntry( index ) );
menu.AddOption( "Duplicate", "content_copy", () => parent.DuplicateEntry( index ) );
menu.OpenAtCursor();
};
}
AcceptDrops = true;
}
protected override void OnPaint()
{
base.OnPaint();
if ( draggingAbove )
{
Paint.SetPen( Theme.Selection, 2f, PenStyle.Solid );
Paint.DrawLine( LocalRect.TopLeft, LocalRect.TopRight );
draggingAbove = false;
}
else if ( draggingBelow )
{
Paint.SetPen( Theme.Selection, 2f, PenStyle.Solid );
Paint.DrawLine( LocalRect.BottomLeft, LocalRect.BottomRight );
draggingBelow = false;
}
}
public override void OnDragHover( DragEvent ev )
{
base.OnDragHover( ev );
if ( !TryDragOperation( ev, out var dragDelta ) )
{
draggingAbove = false;
draggingBelow = false;
return;
}
draggingAbove = dragDelta > 0;
draggingBelow = dragDelta < 0;
}
public override void OnDragDrop( DragEvent ev )
{
base.OnDragDrop( ev );
if ( !TryDragOperation( ev, out var delta ) ) return;
ListWidget.preventRebuild = true;
List<object> list = new();
var movingIndex = Index + delta;
foreach ( var item in ListWidget.Collection )
{
list.Add( item.GetValue<object>() );
}
var prop = list.ElementAt( movingIndex );
list.RemoveAt( movingIndex );
list.Insert( Index, prop );
while ( ListWidget.Collection.Count() > 0 )
{
ListWidget.Collection.RemoveAt( 0 );
}
foreach ( var item in list )
{
ListWidget.Collection.Add( item );
}
ListWidget.preventRebuild = false;
ListWidget.Rebuild();
}
bool TryDragOperation( DragEvent ev, out int delta )
{
delta = 0;
var obj = ev.Data.OfType<SerializedProperty>().FirstOrDefault();
if ( obj == null ) return false;
var otherIndex = ListWidget.Collection.ToList().IndexOf( obj );
if ( Index == -1 || otherIndex == -1 )
{
return false;
}
delta = otherIndex - Index;
return true;
}
class DragHandle : IconButton
{
ListEntryWidget Entry;
public DragHandle( ListEntryWidget entry ) : base( "drag_handle" )
{
Entry = entry;
IsDraggable = Entry.IsDraggable;
}
protected override void OnDragStart()
{
base.OnDragStart();
Entry.dragData = new Drag( this );
Entry.dragData.Data.Object = Entry.ListWidget.Collection.ElementAt( Entry.Index );
Entry.dragData.Execute();
}
}
}
}