Editor/ContructedGenericTypeControlWidget.cs
using System.Reflection;
using System.Runtime.CompilerServices;
using Facepunch.ActionGraphs;
using Sandbox.Diagnostics;
using Nodebox.Attributes;
using Editor.NodeEditor;
using Nodebox.Extensions;
namespace Nodebox.Editor;
[CustomEditor(typeof(Type), WithAllAttributes = [typeof(GenericAttribute)])]
public class ConstructedGenericTypeControlWidget : ControlWidget {
protected static HashSet<Type> GenSystemTypes() => [
typeof(object),
typeof(char), typeof(string),
typeof(bool),
typeof(byte), typeof(sbyte),
typeof(ushort), typeof(short),
typeof(uint), typeof(int),
typeof(ulong), typeof(long),
typeof(float), typeof(double),
typeof(Nullable<>),
typeof(List<>),
typeof(Dictionary<,>),
typeof(HashSet<>),
typeof(Array),
typeof(IList<>),
typeof(ValueTuple<>),
typeof(ValueTuple<,>),
typeof(ValueTuple<,,>),
typeof(ValueTuple<,,,>),
typeof(ValueTuple<,,,,>),
typeof(ValueTuple<,,,,,>),
typeof(ValueTuple<,,,,,,>),
typeof(ValueTuple<,,,,,,,>),
];
protected static HashSet<Type> SystemTypes { get; set; } = GenSystemTypes();
// Whether or not this control supports multi-editing (if you have multiple GameObjects selected)
public override bool SupportsMultiEdit => false;
public Type ParentType { get; set; } = null;
protected Menu _menu;
protected Layout GenericsLayout { get; set; }
public ConstructedGenericTypeControlWidget(SerializedProperty property) : base(property) {
Cursor = CursorShape.Finger;
Layout = Layout.Row();
Layout.Spacing = 2;
if (property.TryGetAttribute<TargetTypeAttribute>(out var targetTypeAttribute)) {
ParentType = targetTypeAttribute.Type;
}
Layout.AddSeparator();
GenericsLayout = Layout.AddRow();
GenericsLayout.Spacing = 2;
}
public int[] GenericPath { get; protected set; } = null;
public ConstructedGenericTypeControlWidget(SerializedProperty property, int[] genericPath) : this(property) {
GenericPath = genericPath;
LayoutGenerics();
}
protected override void PaintControl() {
base.PaintControl();
var value = SerializedProperty.GetValue<Type>();
var color = IsControlHovered ? Theme.Blue : Theme.TextControl;
var rect = LocalRect;
rect = rect.Shrink(8, 0);
var desc = value is not null ? TypeLibrary.GetType(value) : null;
Paint.SetPen(color);
Paint.DrawText(rect, desc == null ? "None" : Sandbox.DisplayInfo.ForGenericType(desc?.TargetType).Name, TextFlag.LeftCenter);
Paint.SetPen(color);
Paint.DrawIcon(rect, "Arrow_Drop_Down", 17, TextFlag.RightCenter);
}
protected override void OnMousePress(MouseEvent e) {
base.OnMousePress(e);
if (e.LeftMouseButton && !_menu.IsValid()) {
OpenMenu();
}
}
protected void OpenMenu() {
_menu = CreateMenu(
null,
null,
[.. SerializedProperty.GetAttributes()],
TypeSelected,
ParentType,
true
);
_menu.DeleteOnClose = true;
_menu.OpenAtCursor(true);
_menu.MinimumWidth = ScreenRect.Width;
}
protected void TypeSelected(Type selectedType) {
if (GenericPath == null || GenericPath.Length == 0) {
SerializedProperty.SetValue(selectedType);
} else {
var type = SerializedProperty.GetValue<Type>();
// Add<float, List<float>>
// SetTypeAt(Add<float, List<float>>, int, [1, 0]);
// SetTypeAt(Add<float, List<float>>, int, [1, 0], 0);
// inargs'0 = [float, List<float>]
// SetTypeAt(List<float>, int, [1, 0], 1);
// inargs'1 = [float]
// SetTypeAt(float, int, [1, 0], 2);
// return int;
// inargs'1 = [int]
// return List<int>;
// inargs'0 = [float, List<int>]
// return Add<float, List<int>>
static Type SetTypeAt(Type type, Type typeArgument, int[] path, int level = 0) {
if (level > path.Length - 1) {
return typeArgument;
}
var typeDesc = TypeLibrary.GetType(type);
var inargs = TypeLibrary.GetGenericArguments(type);
var index = path[level];
inargs[index] = SetTypeAt(inargs[index], typeArgument, path, level + 1);
return typeDesc.MakeGenericType(inargs);
}
type = SetTypeAt(type, selectedType, GenericPath);
SerializedProperty.SetValue(type);
}
SignalValuesChanged();
LayoutGenerics();
}
[EditorEvent.Hotload]
protected void LayoutGenerics() {
var attribute = SerializedProperty.GetAttributes<GenericAttribute>().FirstOrDefault();
if (!attribute.WithArguments) { return; }
GenericsLayout.Clear(true);
var type = SerializedProperty.GetValue<Type>();
if (type == null) { return; }
var typeDesc = TypeLibrary.GetType(type);
if (!typeDesc.IsGenericType) {
return;
}
Type[] arguments = type.GetGenericArguments();
if (GenericPath != null) {
for (int i = 0; i < GenericPath.Length; i++) {
type = arguments[GenericPath[i]];
typeDesc = TypeLibrary.GetType(type);
arguments = type.GetGenericArguments();
}
}
if (arguments == null) { return; }
foreach (var pair in arguments.Enumerate()) {
var genericPathCopy = (GenericPath ?? []).Append(pair.Index).ToArray();
var row = GenericsLayout.AddRow();
row.Add(new ConstructedGenericTypeControlWidget(SerializedProperty, genericPathCopy));
}
}
public static Menu CreateMenu(Type[] options = null, Type genericParam = null, Attribute[] attributes = null, Action<Type> action = null, Type parentType = null, bool canSetNone = false) {
var types = GetPossibleTypes(options, genericParam, attributes, parentType)
.ToArray();
var menu = new ContextMenu(null);
menu.AddLineEdit("Filter",
placeholder: "Filter Types..",
autoFocus: true,
onChange: s => PopulateTypeMenu(menu, types, action, s));
menu.AboutToShow += () => {
PopulateTypeMenu(menu, types, action, canSetNone: canSetNone);
};
return menu;
}
protected record TypeOption(Menu.PathElement[] Path, Type Type, string Title, string Description, string Icon);
protected static string FormatAssemblyName(Assembly asm) {
var name = asm.GetName().Name!;
if (name.StartsWith("package.", StringComparison.OrdinalIgnoreCase)) {
name = name["package.".Length..];
}
if (name.StartsWith("local.", StringComparison.OrdinalIgnoreCase)) {
name = name["local.".Length..];
}
return name.ToTitleCase();
}
protected static string GetAssemblyQualifiedPath(TypeDescription typeDesc) {
var path = !string.IsNullOrEmpty(typeDesc.Namespace)
? typeDesc.Namespace.Replace('.', '/')
: FormatAssemblyName(typeDesc.TargetType.Assembly);
return path;
}
protected static string GetTypePath(TypeDescription typeDesc) {
if (typeDesc.TargetType.DeclaringType != null) {
return $"{GetTypePath(TypeLibrary.GetType(typeDesc.TargetType.DeclaringType))}/{typeDesc.Title}";
}
// var prefix = typeDesc.Group ?? GetAssemblyQualifiedPath(typeDesc);
var prefix = GetAssemblyQualifiedPath(typeDesc);
var icon = typeDesc.Icon;
if (typeDesc.TargetType.IsAssignableTo(typeof(Resource))) {
prefix = $"Resource/{prefix}";
icon ??= "description";
} else if (typeDesc.TargetType.IsAssignableTo(typeof(Component))) {
prefix = $"Component/{prefix}";
icon ??= "category";
} else if (SystemTypes.Contains(typeDesc.TargetType)) {
prefix = typeDesc.Group ?? typeDesc.Namespace?.Replace('.', '/') ?? "Sandbox";
}
icon ??= "check_box_outline_blank";
return $"{prefix}/{Sandbox.DisplayInfo.ForGenericType(typeDesc.TargetType).Name}:{icon}@2000";
}
protected static TypeOption GetTypeOption(Type type) {
var typeDesc = TypeLibrary.GetType(type);
var path = typeDesc is { }
? GetTypePath(typeDesc)
: $"System/{type.Name}";
return new TypeOption(Menu.GetSplitPath(path),
type,
type.Name,
typeDesc?.Description,
typeDesc?.Icon);
}
protected static IEnumerable<TypeOption> GetPossibleTypes(Type[] options = null, Type genericParam = null, Attribute[] attributes = null, Type parentType = null) {
if (options is not null) {
foreach (var option in options) {
yield return GetTypeOption(option);
}
yield break;
}
var listedTypes = new HashSet<Type>();
// var convertFrom = attributes?.OfType<HasConversionFromAttribute>().FirstOrDefault()?.Type;
foreach (var type in SystemTypes) {
if (parentType is not null && !type.IsAssignableTo(parentType)) { continue; }
if (!listedTypes.Add(type)) { continue; }
if (!SatisfiesConstraints(type, genericParam)) { continue; }
// if (convertFrom is not null && !CanConvert(convertFrom, type)) continue;
yield return GetTypeOption(type);
}
var componentTypes = TypeLibrary.GetTypes<Component>();
var resourceTypes = TypeLibrary.GetTypes<GameResource>();
var userTypes = TypeLibrary.GetTypes()
.Where(x => x.TargetType.Assembly.IsPackage());
foreach (var typeDesc in componentTypes.Union(resourceTypes).Union(userTypes)) {
if (parentType is not null && !typeDesc.TargetType.IsAssignableTo(parentType)) { continue; }
if (typeDesc.IsStatic) { continue; }
// if (typeDesc.IsGenericType) { continue; }
if (typeDesc.HasAttribute<CompilerGeneratedAttribute>()) { continue; }
if (typeDesc.Name.StartsWith('<') || typeDesc.Name.StartsWith('_')) { continue; }
if (!listedTypes.Add(typeDesc.TargetType)) { continue; }
if (!SatisfiesConstraints(typeDesc.TargetType, genericParam)) { continue; }
// if (convertFrom is not null && !CanConvert(convertFrom, typeDesc.TargetType)) continue;
yield return GetTypeOption(typeDesc.TargetType);
}
}
protected static bool SatisfiesConstraints(Type type, Type genericParam) {
if (genericParam is null) {
return true;
}
if (!genericParam.GenericParameterAttributes.AreSatisfiedBy(type)) {
return false;
}
foreach (var constraint in genericParam.GetGenericParameterConstraints()) {
if (ApplyGenericArgument(constraint, genericParam, type) is not { } resolvedConstraint) {
return false;
}
if (resolvedConstraint.IsGenericParameter) {
// A bit worried about a stack overflow here
if (resolvedConstraint != genericParam && !SatisfiesConstraints(type, resolvedConstraint)) {
return false;
}
} else if (!type.IsAssignableTo(resolvedConstraint)) {
return false;
}
}
foreach (var hasImpl in genericParam.GetCustomAttributes<HasImplementationAttribute>()) {
// Easy case
if (type.IsAssignableTo(hasImpl.BaseType)) {
continue;
}
var anyImplementing = TypeLibrary.GetTypes(hasImpl.BaseType)
.Where(x => !x.IsAbstract && !x.IsInterface)
.Any(x => x.TargetType.IsAssignableTo(type));
if (!anyImplementing) {
return false;
}
}
return true;
}
protected static Type ApplyGenericArgument(Type type, Type genericParam, Type genericArg) {
if (type == genericParam) {
return genericArg;
}
if (!type.ContainsGenericParameters) {
return type;
}
if (type.IsByRef || type.IsArray) {
if (ApplyGenericArgument(type.GetElementType(), genericParam, genericArg) is not { } elemType) {
return null;
}
if (type.IsByRef) {
return elemType.MakeByRefType();
}
Assert.True(type.IsArray);
return type.GetArrayRank() == 1
? elemType.MakeArrayType()
: elemType.MakeArrayType(type.GetArrayRank());
}
if (!type.IsGenericType) {
return null;
}
try {
return type.GetGenericTypeDefinition()
.MakeGenericType([.. type.GetGenericArguments().Select(x => ApplyGenericArgument(x, genericParam, genericArg))]);
}
catch (ArgumentException) {
// Generic constraints not satisfied
return null;
}
}
protected static void PopulateTypeMenu(Menu menu, IEnumerable<TypeOption> types, Action<Type> action, string filter = null, bool canSetNone = false) {
menu.RemoveMenus();
menu.RemoveOptions();
foreach (var widget in menu.Widgets.Skip(1)) {
menu.RemoveWidget(widget);
}
if (canSetNone) {
menu.AddOption("None", "cancel", () => action?.Invoke(null));
}
const int maxFiltered = 10;
var useFilter = !string.IsNullOrEmpty(filter);
var truncated = 0;
if (useFilter) {
var filtered = types.Where(x => x.Type.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToArray();
if (filtered.Length > maxFiltered + 1) {
truncated = filtered.Length - maxFiltered;
types = filtered.Take(maxFiltered);
} else {
types = filtered;
}
}
menu.AddOptions(types, x => x.Path, x => action?.Invoke(x.Type), flat: useFilter);
if (truncated > 0) {
menu.AddOption($"...and {truncated} more");
}
menu.AdjustSize();
menu.Update();
}
[EditorEvent.Hotload]
public static void OnHotload() {
SystemTypes = GenSystemTypes();
}
}