diff --git a/ScriptCore/Editor/CustomEditorAttribute.cs b/ScriptCore/Editor/CustomEditorAttribute.cs new file mode 100644 index 0000000..208653e --- /dev/null +++ b/ScriptCore/Editor/CustomEditorAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace GlitchyEngine.Editor; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)] +public sealed class CustomEditorAttribute : Attribute +{ + public Type Type { get; private set; } + + public CustomEditorAttribute(Type type) + { + Type = type; + } +} diff --git a/ScriptCore/Editor/DictionaryEditor.cs b/ScriptCore/Editor/DictionaryEditor.cs new file mode 100644 index 0000000..a2c6502 --- /dev/null +++ b/ScriptCore/Editor/DictionaryEditor.cs @@ -0,0 +1,124 @@ +using GlitchyEngine.Extensions; +using ImGuiNET; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace GlitchyEngine.Editor; + +[CustomEditor(typeof(Dictionary<,>))] +public class DictionaryEditor +{ + private static object? dictionaryForNewValue = null; + + public static object? ShowEditor(object? reference, Type fieldType, string fieldName) + { + object newDictionary = EntityEditor.DidNotChange; + IDictionary? dictionary = reference as IDictionary; + + Type keyType = fieldType.GetGenericArguments()[0]; + Type valueType = fieldType.GetGenericArguments()[1]; + + // The buttons should always be visible -> save whether the node is open + // If we have no instance, the user shouldn't be able to open the list + bool listOpen = ImGui.TreeNodeEx(fieldName, ImGuiTreeNodeFlags.AllowOverlap | ImGuiTreeNodeFlags.SpanFullWidth | + (dictionary == null ? ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen : ImGuiTreeNodeFlags.Framed)); + + if (dictionary == null) + listOpen = false; + + var addButtonWidth = ImGui.CalcTextSize("+").X + 2 * ImGui.GetStyle().FramePadding.X; + var removeButtonWidth = ImGui.CalcTextSize("-").X + 2 * ImGui.GetStyle().FramePadding.X; + + ImGui.SameLine(ImGui.GetWindowContentRegionMax().X - addButtonWidth - ImGui.GetStyle().FramePadding.X); + + if (ImGui.SmallButton("+")) + { + if (dictionary == null) + { + newDictionary = Activator.CreateInstance(fieldType); + dictionary = newDictionary as IDictionary; + + Debug.Assert(dictionary != null); + } + + dictionaryForNewValue = dictionary; + + // TODO: show fake entry! + + //object? newKey = ActivatorExtension.CreateInstanceSafe(keyType); + //object? newValue = ActivatorExtension.CreateInstanceSafe(valueType); + + //if (newKey != null) + // dictionary!.Add(newKey, newValue); + } + + ImGuiExtension.AttachTooltip("Add a new Element to the dictionary."); + + if (listOpen) + { + Debug.Assert(dictionary != null, "Opened tree node even though it should have been a leaf!"); + + int id = 0; + + List newEntries = new(); + List keysToDelete = new(); + + foreach (DictionaryEntry entry in dictionary!) + { + id++; + ImGui.PushID(id); + + bool isEntryOpen = ImGui.TreeNode(""); + + ImGui.SameLine(ImGui.GetWindowContentRegionMax().X - removeButtonWidth); + + if (ImGui.SmallButton("-")) + { + keysToDelete.Add(entry.Key!); + } + + ImGuiExtension.AttachTooltip("Remove the last Element from the list."); + + if (isEntryOpen) + { + object newKey = EntityEditor.ShowFieldEditor(entry.Key, entry.Key?.GetType() ?? keyType, "Key"); + + if (newKey != EntityEditor.DidNotChange) + { + keysToDelete.Add(entry.Key!); + newEntries.Add(new DictionaryEntry(newKey, entry.Value)); + } + + object newValue = EntityEditor.ShowFieldEditor(entry.Value, entry.Value?.GetType() ?? valueType, "Value"); + + if (newValue != EntityEditor.DidNotChange) + { + keysToDelete.Add(entry.Key!); + newEntries.Add(new DictionaryEntry(entry.Key!, newValue)); + } + + ImGui.TreePop(); + } + + ImGui.PopID(); + } + + foreach (object key in keysToDelete) + { + dictionary.Remove(key); + } + + foreach (DictionaryEntry newEntry in newEntries) + { + dictionary.Add(newEntry.Key, newEntry.Value); + } + + ImGui.TreePop(); + } + + return newDictionary; + } +} diff --git a/ScriptCore/Editor/EntityEditor.cs b/ScriptCore/Editor/EntityEditor.cs index 7af0d12..e69de8c 100644 --- a/ScriptCore/Editor/EntityEditor.cs +++ b/ScriptCore/Editor/EntityEditor.cs @@ -10,6 +10,7 @@ using System.Text; using GlitchyEngine.Core; using GlitchyEngine.Extensions; using GlitchyEngine.Math; +using GlitchyEngine.Serialization; using ImGuiNET; namespace GlitchyEngine.Editor; @@ -20,7 +21,63 @@ class DidNotChange internal class EntityEditor { - private static readonly DidNotChange DidNotChange = new DidNotChange(); + public static readonly DidNotChange DidNotChange = new DidNotChange(); + + private delegate object? ShowCustomEditorMethod(object reference, Type fieldType, string fieldName); + + private static Dictionary _customEditors = new(); + + static EntityEditor() + { + foreach (Type type in TypeExtension.EnumerateAllTypes()) + { + if (type.TryGetCustomAttribute(out var attribute)) + { + MethodInfo? showEditorMethod = type.GetMethod("ShowEditor", BindingFlags.Static | BindingFlags.Public, + null, + new []{ typeof(object), typeof(Type), typeof(string) }, null); + + if (showEditorMethod == null) + { + Log.Error($"No ShowEditor-method found for type {type}"); + } + else + { + ShowCustomEditorMethod method = showEditorMethod.GetDelegate(); + + _customEditors.Add(attribute.Type, method); + } + } + } + } + + private static bool TryShowCustomEditor(object reference, Type fieldType, string fieldName, out object? editedValue) + { + editedValue = DidNotChange; + + try + { + // Try to match the concrete type first (e.g. Foo -> Foo and Foo -> List) + // Note: Foo wont match a serializer for Foo<> + if (_customEditors.TryGetValue(fieldType, out ShowCustomEditorMethod customEditorMethod)) + { + editedValue = customEditorMethod(reference, fieldType, fieldName); + return true; + } + + if (fieldType.IsGenericType && _customEditors.TryGetValue(fieldType.GetGenericTypeDefinition(), out customEditorMethod)) + { + editedValue = customEditorMethod(reference, fieldType, fieldName); + return true; + } + } + catch (Exception e) + { + Log.Error(e); + } + + return false; + } public static bool ShowFieldInEditor(FieldInfo fieldInfo) { @@ -49,12 +106,12 @@ internal class EntityEditor ShowEditor(entityType, instance); } - private static T GetAttribute(IEnumerable attributes) where T : Attribute + private static T? GetAttribute(IEnumerable? attributes) where T : Attribute { - return (T)attributes?.FirstOrDefault(a => a is T); + return (T?)attributes?.FirstOrDefault(a => a is T); } - private static object ShowPrimitiveEditor(object reference, Type fieldType, string fieldName, IEnumerable attributes) + private static object ShowPrimitiveEditor(object reference, Type fieldType, string fieldName, IEnumerable? attributes) { object newValue = DidNotChange; @@ -75,7 +132,7 @@ internal class EntityEditor } } - RangeAttribute range = GetAttribute(attributes); + RangeAttribute? range = GetAttribute(attributes); // Get Min and Max Values from Type T T min = ReadStaticField("MinValue"); @@ -91,8 +148,8 @@ internal class EntityEditor } else { - MinimumAttribute minimum = GetAttribute(attributes); - MaximumAttribute maximum = GetAttribute(attributes); + MinimumAttribute? minimum = GetAttribute(attributes); + MaximumAttribute? maximum = GetAttribute(attributes); // Don't use range, if it is invalid if (minimum != null && maximum != null && minimum.Min > maximum.Max) @@ -197,24 +254,24 @@ internal class EntityEditor return newValue; } - public static object ShowFieldEditor(object reference, Type fieldType, string fieldName, IEnumerable attributes = null) + public static object ShowFieldEditor(object reference, Type fieldType, string fieldName, IEnumerable? attributes = null) { ReadonlyAttribute readonlyAttribute = GetAttribute(attributes); ImGui.BeginDisabled(readonlyAttribute != null); - object newValue = DidNotChange; + object? newValue = DidNotChange; + + if (TryShowCustomEditor(reference, fieldType, fieldName, out newValue)) + { - if (fieldType.IsGenericType) + } + else if (fieldType.IsGenericType) { if (fieldType.GetGenericTypeDefinition() == typeof(List<>)) { newValue = ShowListEditor(fieldType, reference, fieldName); } - else if (fieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) - { - ImGui.Text($"{fieldName} Dictionary"); - } else { // TODO: Try to use a custom editor