Editor/ChitChat/CustomWidgets/ListControlView.cs
using Sandbox;
using Editor;
using System;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;
namespace ChitChat.Editor;
public class ListControlView : ControlWidget
{
public Action<SerializedProperty> onItemSelected;
/// <summary>
/// Invokes when an item has been removed. Gives index of the removed item.
/// </summary>
public Action<int> onItemRemoved;
public Action<int> onItemMoved;
public Action<int> onItemRightClicked;
public delegate ControlWidget CreateItemUI(SerializedProperty prop);
private CreateItemUI _onCreateUIItem = null;
public override bool SupportsMultiEdit => true;
public int SelectedIndex { get; set; } = -1;
private bool _hasAddButton = false;
internal SerializedCollection Collection;
private readonly Layout _content;
private IconButton _addButton;
private Layout _addButtonLayout;
private bool _preventRebuild = false;
private bool _movedItem = false;
private int? _buildHash;
private object _buildValue;
protected override int ValueHash => Collection is null ? base.ValueHash : HashCode.Combine(base.ValueHash, Collection.Count(), _movedItem);
public ListControlView(SerializedProperty property, CreateItemUI onCreateItemUI = null, bool hasAddButton = true)
: this(property, GetCollection(property), onCreateItemUI, hasAddButton)
{
}
private static SerializedCollection GetCollection(SerializedProperty property)
{
if (property == null)
return null;
if (!property.TryGetAsObject(out var so) || so is not SerializedCollection sc)
{
Log.Error("Couldn't get serialized property as serialized collection!");
return null;
}
return sc;
}
public ListControlView(SerializedProperty property, SerializedCollection sc, CreateItemUI createItemUI = null, bool hasAddButton = true)
: base(property)
{
Layout = Layout.Column();
Layout.Spacing = 2;
_hasAddButton = hasAddButton;
_onCreateUIItem = createItemUI;
if (sc == null && !property.IsMultipleValues) return;
if (sc != null)
{
Collection = sc;
Collection.OnEntryAdded = Rebuild;
Collection.OnEntryRemoved = Rebuild;
}
_buildValue = SerializedProperty?.GetValue<object>();
_content = Layout.Column();
Layout.Add(_content);
Rebuild();
}
public void SelectItem(int index)
{
if (index < Collection.Count() && index >= 0)
OnItemClicked(index);
}
internal ControlWidget CreateUIItem(SerializedProperty prop)
{
if (prop == null) return null;
if (_onCreateUIItem == null)
return Create(prop);
else
return _onCreateUIItem?.Invoke(prop);
}
private void RefreshCollection()
{
var value = SerializedProperty?.GetValue<object>();
if (_buildValue == value)
return;
_buildValue = value;
// Collection has changed, need to get the new one
var sc = GetCollection(SerializedProperty);
if (sc != null)
{
Collection = sc;
Collection.OnEntryAdded = Rebuild;
Collection.OnEntryRemoved = Rebuild;
}
}
protected override void OnValueChanged()
{
RefreshCollection();
Rebuild();
}
public void Rebuild()
{
if (_preventRebuild) return;
if (Collection != null)
{
var hash = ValueHash;
if (_buildHash.HasValue && hash == _buildHash.Value) return;
_buildHash = hash;
}
using var _ = SuspendUpdates.For(this);
_content.Clear(true);
_content.Margin = 0;
var column = Layout.Column();
if (SerializedProperty.IsMultipleValues)
{
RebuildMultiple(column);
}
else
{
RebuildSingle(column);
}
// bottom row
if (!IsControlDisabled && _hasAddButton)
{
_addButtonLayout = Layout.Row();
_addButtonLayout.Margin = new Sandbox.UI.Margin(Theme.ControlHeight + 2, 0, 0, 0);
_addButton = new ListControlAddButton(this);
_addButton.MouseClick = AddEntry;
_addButtonLayout.Add(_addButton);
_addButtonLayout.AddStretchCell(1);
column.Add(_addButtonLayout);
}
_content.Add(column);
}
private void OnItemClicked(int index)
{
SelectedIndex = index;
onItemSelected?.Invoke(Collection.Skip(index).First());
}
private void RebuildSingle(Layout column)
{
if (Collection is null) return;
int index = 0;
foreach (var itemProp in Collection)
{
ListItemWidget item = new ListItemWidget(this, itemProp, index);
item.onItemClicked += OnItemClicked;
column.Add(item);
column.AddSeparator(8, Color.Transparent);
index++;
}
}
private void RebuildMultiple(Layout column)
{
var minCount = GetMultipleMin();
if (minCount == int.MaxValue || minCount == 0) return;
for (int i = 0; i < minCount; i++)
{
MultiSerializedObject mo = new();
foreach (var listProp in SerializedProperty.MultipleProperties)
{
var collection = GetCollection(listProp);
if (collection == null) continue;
var property = collection.ElementAt(i);
if (property != null)
{
mo.Add(property.Parent);
}
}
mo.Rebuild();
var so = (SerializedObject)mo;
var itemProp = so.GetProperty(i.ToString());
ListItemWidget item = new ListItemWidget(this, itemProp, i);
item.onItemClicked += OnItemClicked;
column.Add(item);
column.AddSeparator(8, Color.Transparent);
}
}
private void AddEntry()
{
if (Collection != null)
{
Collection.Add(null);
}
else
{
var minCount = GetMultipleMin();
var maxCount = GetMultipleMax();
var props = SerializedProperty.MultipleProperties.ToList();
foreach (var prop in props)
{
var collection = GetCollection(prop);
if (collection is null) continue;
if ( minCount == maxCount || collection.Count() == minCount)
{
if (collection.Count() == 0 && minCount != 0) continue;
collection?.Add(null);
}
}
Rebuild();
}
}
internal void InsertEntry(int index, object obj)
{
if (Collection != null)
{
Collection.Add(index + 1, obj);
}
else
{
foreach (var prop in SerializedProperty.MultipleProperties)
{
var collection = GetCollection(prop);
if (index < collection.Count())
{
Collection.Add(index + 1, obj);
}
}
Rebuild();
}
}
private void RemoveEntry(int index)
{
if (Collection != null)
{
Collection.RemoveAt(index);
}
else
{
foreach (var prop in SerializedProperty.MultipleProperties)
{
var collection = GetCollection(prop);
if (index < collection.Count())
{
collection.RemoveAt(index);
}
}
Rebuild();
}
onItemRemoved?.Invoke(index);
}
private void DuplicateEntry(int index)
{
if (Collection != null)
{
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));
}
else
{
foreach (var prop in SerializedProperty.MultipleProperties)
{
var collection = GetCollection(prop);
if (index < collection.Count())
{
var sourceProperty = collection.ElementAt(index);
var sourceObj = sourceProperty.GetValue<object>();
var sourceJson = Json.ToNode(sourceObj);
collection.Add( index + 1, Json.FromNode(sourceJson, sourceProperty.PropertyType));
}
}
Rebuild();
}
}
private void MoveEntry(int index, int delta)
{
var movingIndex = index + delta;
if (movingIndex < 0 || movingIndex > GetMultipleMin() - 1) return;
_preventRebuild = true;
if (Collection != null)
{
Move(Collection, index, delta);
}
else
{
foreach (var prop in SerializedProperty.MultipleProperties)
{
var collection = GetCollection(prop);
if (collection is null ) continue;
Move(collection, index, delta);
}
}
_preventRebuild = false;
_movedItem = !_movedItem;
onItemMoved?.Invoke(movingIndex);
Rebuild();
}
private void Move(SerializedCollection collection, int index, int delta)
{
List<object> list = new();
var movingIndex = index + delta;
foreach (var item in collection)
{
list.Add(item.GetValue<object>());
}
var prop = list.ElementAt(movingIndex);
list.RemoveAt(movingIndex);
list.Insert(index, prop);
while (collection.Count() > 0)
{
collection.RemoveAt(0);
}
foreach (var item in list)
{
collection.Add(item);
}
}
private int GetMultipleMin()
{
var minCount = int.MaxValue;
foreach (var entry in SerializedProperty.MultipleProperties)
{
var collection = GetCollection(entry);
if (collection == null) continue;
var count = collection.Count();
if (count < minCount) minCount = count;
}
if (minCount == int.MaxValue) return 0;
return minCount;
}
private int GetMultipleMax()
{
var maxCount = int.MinValue;
foreach (var entry in SerializedProperty.MultipleProperties)
{
var collection = GetCollection(entry);
if (collection is null) continue;
var count = collection.Count();
if (count > maxCount) maxCount = count;
}
if (maxCount == int.MinValue) return 0;
return maxCount;
}
private object GetDragData(int index)
{
if (Collection != null)
{
return index;
}
else
{
var firstProp = SerializedProperty.MultipleProperties.FirstOrDefault();
if (firstProp == null) return null;
var collection = GetCollection(firstProp);
if (index < collection.Count())
{
return index;
}
}
return null;
}
protected override void OnPaint()
{
Paint.Antialiasing = true;
Paint.ClearPen();
Paint.SetBrush( Theme.TextControl.Darken( 0.6f ) );
}
private class ListItemWidget : Widget
{
public Action<int> onItemClicked;
private Drag _dragData;
private DragHandle _dragHandle;
private ControlWidget _controlWidget;
private bool _draggingAbove = false;
private bool _draggingBelow = false;
private ListControlView _listWidget;
private int _index = -1;
public ListItemWidget(ListControlView parent, SerializedProperty property, int index) : base(parent)
{
_listWidget = parent;
_index = index;
ContentMargins = new Sandbox.UI.Margin(0, 0, 15, 0);
Layout = Layout.Row();
Layout.Margin = new Sandbox.UI.Margin(0, 2);
Layout.Spacing = 2;
ReadOnly = parent.ReadOnly;
Enabled = parent.Enabled;
Cursor = CursorShape.Finger;
ToolTip = $"Element {_index}";
_controlWidget = _listWidget.CreateUIItem(property);
_controlWidget.ReadOnly = ReadOnly;
_controlWidget.Enabled = Enabled;
_controlWidget.MouseTracking = true;
if (_controlWidget.IsControlDisabled)
{
Layout.Add(_controlWidget);
}
else
{
IsDraggable = !_controlWidget.IsControlDisabled;
_dragHandle = new DragHandle(this)
{
IconSize = 13,
Foreground = Theme.TextControl,
Background = Color.Transparent,
FixedWidth = Theme.ControlHeight,
FixedHeight = Theme.ControlHeight
};
IconButton removeButton = new IconButton("clear", () => parent.RemoveEntry(index))
{
ToolTip = "Remove",
Background = Theme.ControlBackground,
FixedWidth = Theme.ControlHeight,
FixedHeight = Theme.ControlHeight
};
Layout.Add(_dragHandle);
Layout.Add(_controlWidget);
Layout.Add(removeButton);
_dragHandle.MouseRightClick += () =>
{
ContextMenu 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 OnMouseClick(MouseEvent e)
{
base.OnMouseClick(e);
if (e.LeftMouseButton)
{
onItemClicked?.Invoke(_index);
}
}
protected override void OnMouseRightClick(MouseEvent e)
{
base.OnMouseRightClick(e);
if (_controlWidget.IsUnderMouse)
_listWidget.onItemRightClicked?.Invoke(_index);
}
protected override void OnPaint()
{
if (Enabled)
{
Rect rect = LocalRect;
if (Paint.HasPressed)
{
Paint.ClearPen();
Paint.SetBrush(Color.Blue);
Paint.DrawRect(in rect, 3);
}
else if(Paint.HasMouseOver)
{
Paint.ClearPen();
Paint.SetBrush(new Color(0.37f, 0.37f, 0.37f));
Paint.DrawRect(in rect, 3);
}
if (_draggingAbove)
{
Paint.SetPen(Theme.SelectedBackground, 2f, PenStyle.Solid);
Paint.DrawLine(LocalRect.TopLeft, LocalRect.TopRight);
_draggingAbove = false;
}
else if (_draggingBelow)
{
Paint.SetPen(Theme.SelectedBackground, 2f, PenStyle.Solid);
Paint.DrawLine(LocalRect.BottomLeft, LocalRect.BottomRight);
_draggingBelow = false;
}
}
}
public override void OnDragHover(DragEvent ev)
{
if (!TryDragOperation(ev, out var dragDelta))
{
_draggingAbove = false;
_draggingBelow = false;
return;
}
_draggingAbove = dragDelta > 0;
_draggingBelow = dragDelta < 0;
}
public override void OnDragDrop(DragEvent ev)
{
if(ev.Action == DropAction.Ignore)
return;
ev.Action = DropAction.Ignore;
if (!TryDragOperation(ev, out var delta)) return;
_listWidget.MoveEntry(_index, delta);
}
private bool TryDragOperation(DragEvent ev, out int delta)
{
delta = 0;
var obj = ev.Data.Object;
if (obj == null) return false;
if (obj is int otherIndex)
{
if (_index == -1 || otherIndex == -1)
return false;
else if (_index == otherIndex)
{
_dragHandle.IsDraggable = true;
return false;
}
delta = otherIndex - _index;
return true;
}
return false;
}
private class DragHandle : IconButton
{
private ListItemWidget _item;
public DragHandle(ListItemWidget item) : base("drag_handle")
{
_item = item;
IsDraggable = _item.IsDraggable;
}
protected override void OnDragStart()
{
IsDraggable = false;
_item._dragData = new Drag(this);
_item._dragData.Data.Object = _item._listWidget.GetDragData(_item._index);
_item._dragData.Execute();
}
}
}
private class ListControlAddButton : IconButton
{
ListControlView ParentList;
public ListControlAddButton(ListControlView parentList) : base("add")
{
ParentList = parentList;
Background = Theme.ControlBackground;
ToolTip = "Add Element";
FixedWidth = Theme.ControlHeight;
FixedHeight = Theme.ControlHeight;
AcceptDrops = true;
}
public override void OnDragDrop(DragEvent ev)
{
base.OnDragDrop(ev);
var dataObj = ev.Data.Object;
var parentType = ParentList.Collection.ValueType;
// Allow dragging objects onto the add button
if (dataObj is object[] dataArray)
{
foreach (var obj in dataArray)
{
var objType = obj.GetType();
if (objType != parentType)
{
if ( obj is GameObject gameObject && parentType.IsAssignableTo(typeof(Component)))
{
var comp = gameObject.Components.Get(parentType);
if (comp is not null)
{
ParentList.Collection.Add(comp);
}
}
continue;
}
ParentList.Collection.Add(obj);
}
}
else if (dataObj?.GetType() == parentType)
{
ParentList.Collection.Add(dataObj);
}
else if (ev.Data.Assets.Count > 0)
{
DropAssets(ev);
}
}
private async void DropAssets(DragEvent ev)
{
var parentType = ParentList.Collection.ValueType;
// Special case for SoundFile
if (parentType == typeof(SoundFile))
{
await DropSoundFiles(ev);
return;
}
foreach (var dataAsset in ev.Data.Assets)
{
if (dataAsset is null) continue;
var asset = await dataAsset.GetAssetAsync();
if (asset is null) continue;
var resource = asset.LoadResource();
if (resource is null) continue;
ParentList.Collection.Add(resource);
}
}
private async Task DropSoundFiles(DragEvent ev)
{
foreach (var dataAsset in ev.Data.Assets)
{
if (dataAsset is null) continue;
if (dataAsset.AssetPath.EndsWith(".sound")) continue;
if (!dataAsset.IsInstalled)
{
await dataAsset.GetAssetAsync();
}
var sound = SoundFile.Load(dataAsset.AssetPath);
if (sound is null) continue;
ParentList.Collection.Add(sound);
}
}
}
}