Disable Studio Add for record types the model cannot create

The window lost its disabled state when record creation moved into the model:
Add and Create were enabled for abstract types and threw on click, and
CreateElement(type, type) on an abstract [Union] type threw "X is not a choice
for X". CultInspectorModel.CanCreate is now the one rule CreateDefault and the
window both read, a slot's own type is always a choice, and an abstract one
returns the "abstract or no parameterless constructor" notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-14 14:53:32 +02:00
commit baa8de0baf
3 changed files with 20 additions and 10 deletions

View file

@ -345,14 +345,19 @@ namespace GameCult.Caching
return new CultInspectorEdit(record, Clone(record.Document, record.Descriptor.DocumentType));
}
// Whether CreateDefault makes a value: a string, struct, list or dictionary, or a concrete class with a parameterless constructor.
public bool CanCreate(Type type) =>
type == typeof(string) || type.IsValueType ||
ShapeOf(type).Kind is CultInspectorValueKind.List or CultInspectorValueKind.Dictionary ||
!type.IsAbstract && !type.IsInterface && type.GetConstructor(Type.EmptyTypes) != null;
public object? CreateDefault(Type type)
{
if (!CanCreate(type)) return null;
if (type == typeof(string)) return string.Empty;
if (type.IsValueType) return Activator.CreateInstance(type);
var shape = ShapeOf(type);
if (shape.Kind == CultInspectorValueKind.List && type.IsArray) return Array.CreateInstance(shape.ElementType!, 0);
if (shape.Kind == CultInspectorValueKind.Dictionary) return Activator.CreateInstance(shape.BuildType!);
return type.IsAbstract || type.IsInterface || type.GetConstructor(Type.EmptyTypes) == null ? null : Activator.CreateInstance(type);
if (type.IsArray) return Array.CreateInstance(shape.ElementType!, 0);
return Activator.CreateInstance(shape.Kind == CultInspectorValueKind.Dictionary ? shape.BuildType! : type);
}
// What a new value for a slot of elementType (a list element, a dictionary value, a union pick) may be: a union's
@ -366,11 +371,10 @@ namespace GameCult.Caching
// A new value of choice for a slot of elementType, or null with a notice when choice cannot be made.
public object? CreateElement(Type elementType, Type choice, out string? notice)
{
if (!ElementChoices(elementType).Contains(choice))
// A slot's own type is always offered; an abstract one is refused with the notice below.
if (choice != elementType && !ElementChoices(elementType).Contains(choice))
throw new ArgumentException($"{choice.Name} is not a choice for {elementType.Name}.", nameof(choice));
var created = choice == elementType ? CreateDefault(choice)
: choice.GetConstructor(Type.EmptyTypes) == null ? null
: Activator.CreateInstance(choice);
var created = CreateDefault(choice);
// Null is a value only for a Nullable<T> slot; anywhere else it is nothing made.
notice = created == null && !choice.IsValueType
? $"{choice.Name} is abstract or has no parameterless constructor; nothing was created."

View file

@ -138,9 +138,10 @@ namespace GameCult.Unity.Caching.Editor
{
var descriptor = _selectedType == null ? null : _cache.Registry.GetRequired(_selectedType);
var selected = Selected();
var cannotAdd = descriptor == null || ReadOnly || !_model.CanCreate(_selectedType);
using (new EditorGUILayout.HorizontalScope())
{
using (new EditorGUI.DisabledScope(descriptor == null || descriptor.IsGlobal || ReadOnly))
using (new EditorGUI.DisabledScope(cannotAdd || descriptor.IsGlobal))
{
if (GUILayout.Button("Add", EditorStyles.miniButtonLeft)) Add();
}
@ -169,7 +170,7 @@ namespace GameCult.Unity.Caching.Editor
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("Global is absent.");
using (new EditorGUI.DisabledScope(ReadOnly))
using (new EditorGUI.DisabledScope(cannotAdd))
{
if (GUILayout.Button("Create", GUILayout.Width(56))) Add();
}

View file

@ -85,6 +85,11 @@ namespace GameCult.Caching.Tests
Assert.That(bare, Does.Contain("parameterless"), "a plain element type that cannot be made says so; nothing is added");
Assert.That(model.CreateElement(typeof(int?), typeof(int?), out var nullable), Is.Null);
Assert.That(nullable, Is.Null, "null is a Nullable<T> element, not a failure");
Assert.That(model.CreateElement(typeof(InspectShape), typeof(InspectShape), out var @abstract), Is.Null);
Assert.That(@abstract, Does.Contain("abstract"), "a union's own type cannot be made; it says so instead of throwing");
Assert.That(new[] { typeof(InspectShape), typeof(InspectNeedsArgs), typeof(IList<int>) }.Any(model.CanCreate), Is.False);
Assert.That(new[] { typeof(InspectItem), typeof(string), typeof(int[]), typeof(IDictionary<string, int>), typeof(InspectOpenStruct) }.All(model.CanCreate), Is.True);
}
[Test]