Editor/ControlWidgets/FoldableControlWidget.cs
using Editor;
using ExtendedEditor.Attributes;
using ExtendedEditor.TypeLibraryFixes;
using Sandbox;
using Sandbox.UI;
using System;
using System.Collections;
using System.Linq;
namespace ExtendedEditor.ControlWidgets;
[CustomEditor(typeof(object), WithAllAttributes = [typeof(FoldableEditorAttribute)])]
public class FoldableControlWidget : ControlWidget
{
private Widget _body = null!;
private Header _header = null!;
private bool IsFolded => _body?.Hidden ?? true;
public FoldableControlWidget(SerializedProperty property) : base(property.Fix())
{
Layout = Layout.Column();
Layout.Margin = 0;
Layout.Spacing = 0;
RebuildLayout();
}
private void RebuildLayout()
{
Layout.Clear(true);
if(SerializedProperty.TryGetAsObject(out var serializedObject) && serializedObject is SerializedCollection)
{
this.Skip();
return;
}
var startFolded = true;
if(SerializedProperty.TryGetAttribute<FoldableEditorAttribute>(out var foldableEditorAttribute))
startFolded = foldableEditorAttribute.StartFolded;
var attributes = SerializedProperty.GetAttributes()
.Where(x => x is not FoldableEditorAttribute)
.Append(new InlineEditorAttribute() { Label = false });
if(SerializedProperty.Parent is SerializedCollection)
attributes = attributes.Where(x => x is not InspectorVisibilityAttribute);
var property = SerializedProperty.CreateProxy(attributes);
_body = new Widget
{
Hidden = true,
VerticalSizeMode = SizeMode.CanGrow,
HorizontalSizeMode = SizeMode.Flexible,
ReadOnly = ReadOnly,
Layout = Layout.Column()
};
_body.Layout.Margin = new Margin(12, 4, 0, 4);
_body.Layout.Spacing = 0;
var bodyControlWidget = ControlWidget.Create(property);
bodyControlWidget.PaintBackground = false;
_body.Layout.Add(bodyControlWidget);
_header = new();
_header.OnToggled += opened => _body.Hidden = !opened;
Layout.Add(_header);
Layout.Add(_body);
UpdateHeaderName();
if(_header.Opened == startFolded)
_header.Toggle();
}
protected override void OnValueChanged()
{
RebuildLayout();
}
public override void ChildValuesChanged(Widget source)
{
base.ChildValuesChanged(source);
UpdateHeaderName();
}
private void UpdateHeaderName()
{
if(_header is null)
return;
if(SerializedProperty.TryGetAttribute<FoldableEditorAttribute>(out var foldableEditorAttribute) &&
!string.IsNullOrEmpty(foldableEditorAttribute.Label))
{
_header.Title = foldableEditorAttribute.Label;
return;
}
var folderName = SerializedProperty.GetValue<object>()?.ToString()?.Replace('\n', ' ') ??
$"None {SerializedProperty.PropertyType}";
if(SerializedProperty.TryGetAsObject(out var serializedObject))
{
var keyProperty = serializedObject.FirstOrDefault(x => x.HasAttribute<FoldableObjectKeyAttribute>());
if(keyProperty is not null)
{
var value = keyProperty.GetValue<object>();
if(value is IEnumerable enumerable)
{
var enumerableObj = enumerable.Cast<object>().Where(x => x is not null);
if(enumerableObj.Any())
folderName = $"{string.Join(", ", enumerable.Cast<object>().Take(3).Select(x => $"'{x}'"))}{(enumerableObj.Count() > 3 ? $" ... +{enumerableObj.Count() - 3} more" : string.Empty)}";
}
else
{
var str = value?.ToString();
if(str is not null && (value is not IValid valid || valid.IsValid))
folderName = str;
}
}
}
_header.Title = folderName;
}
protected override void PaintUnder()
{
if(!PaintBackground)
return;
var backgroundColor = Theme.WidgetBackground;
var parent = Parent;
while(parent is not null)
{
if(parent is FoldableControlWidget)
backgroundColor = backgroundColor.Darken(0.1f);
parent = parent.Parent;
}
Paint.ClearPen();
Paint.SetBrush(backgroundColor);
Paint.DrawRect(LocalRect, Theme.ControlRadius);
}
public class Header : Widget
{
public Action<bool>? OnToggled;
public bool Opened { get; private set; } = false;
public string Title { get; set; } = string.Empty;
public string CookieName
{
get
{
return _cookieName;
}
set
{
_cookieName = value;
var newState = ProjectCookie.Get(_cookieName, Opened);
if(newState == Opened)
return;
Toggle();
}
}
private string _cookieName = string.Empty;
private readonly Layout _toggleLayout;
public Header() : base(null)
{
FixedHeight = Theme.RowHeight;
VerticalSizeMode = SizeMode.CanGrow;
HorizontalSizeMode = SizeMode.Flexible;
Cursor = CursorShape.Finger;
Layout = Layout.Row();
Layout.Spacing = 5;
Layout.Margin = new Margin(16, 0, 0, 0);
Layout.AddSpacingCell(10);
_toggleLayout = Layout.AddColumn();
Layout.AddStretchCell();
}
protected override void OnMousePress(MouseEvent e)
{
base.OnMousePress(e);
if(e.Button == MouseButtons.Left)
Toggle();
}
protected override void OnDoubleClick(MouseEvent e)
{
e.Accepted = false;
}
protected override void OnPaint()
{
float spacing = 0;
var textRect = Paint.MeasureText(LocalRect.Shrink(_toggleLayout.OuterRect.Right + spacing, 0, 0, 0), Title, TextFlag.LeftCenter);
var backgroundRect = LocalRect.Shrink(3, 4, 4, 4);
backgroundRect.Height = 22 - 8;
backgroundRect.Right = textRect.Right + 8;
backgroundRect.Width = backgroundRect.Height;
// Background
{
var backgroundColor = Theme.WindowBackground.Lighten(0.2f).WithAlphaMultiplied(Opened ? 1 : 0.5f);
Paint.SetBrushAndPen(backgroundColor);
Paint.DrawRect(backgroundRect, 6);
}
Paint.ClearBrush();
var iconIntensity = IsUnderMouse ? 1.5f : 1f;
if(Opened)
{
Paint.Pen = Theme.TextControl.WithAlpha(0.2f * iconIntensity);
Paint.DrawIcon(backgroundRect, "remove", 12, TextFlag.Center);
}
else
{
Paint.Pen = Theme.TextControl.WithAlpha(0.4f * iconIntensity);
Paint.DrawIcon(backgroundRect, "add", 12, TextFlag.Center);
}
Paint.Pen = Theme.TextControl.WithAlpha(Opened ? 1 : 0.8f);
Paint.SetDefaultFont(11, weight: 400, sizeInPixels: true);
Paint.DrawText(LocalRect.Shrink(_toggleLayout.OuterRect.Right + spacing, 0, 0, 0), Title, TextFlag.LeftCenter);
}
public void Toggle()
{
Opened = !Opened;
OnToggled?.Invoke(Opened);
if(CookieName is not null)
{
ProjectCookie.Set(CookieName, Opened);
}
}
}
}