using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Drawing; using System.Linq; using System.Reflection; using System.Windows.Forms; using BizHawk.Client.Common; namespace BizHawk.Experiment.AutoGenConfig { public static class ConfigEditorUIGenerators { public interface IConfigPropEditorUIGen { bool MatchesBaseline(Control c, ConfigEditorMetadata metadata); /// /// A with set to the property name (including nesting) and set to a .
/// Multiple Controls may be needed, the returned value is always the topmost parent (probably a ). ///
/// in will be updated. Control GenerateControl(string nesting, PropertyInfo pi, object config, ConfigEditorMetadata metadata); /// value represented by or its nested Controls object? GetTValue(Control c); string SerializeTValue(object? v); /// iff equal bool TValueEquality(object? a, object? b); } public sealed class CheckBoxForBoolEditorUIGen : ConfigPropEditorUIGenValT { protected override void ControlEventHandler(object sender, EventArgs args) { var cb = (CheckBox) sender; cb.ForeColor = GetComparisonColor(cb.Name, cb.Checked, (ConfigPropEditorUITag) cb.Tag); } protected override CheckBox GenerateControl(string nesting, PropertyInfo pi, object config, ConfigEditorMetadata metadata) { if (pi.PropertyType != typeof(bool)) throw new Exception(); var baseline = (bool) pi.GetValue(config); var nestedName = $"{nesting}/{pi.Name}"; metadata.BaselineValues[nestedName] = baseline; var tag = new ConfigPropEditorUITag(metadata, this); return new CheckBox { AutoSize = true, Checked = baseline, ForeColor = GetUnchangedComparisonColor(nestedName, in baseline, tag), Name = nestedName, Tag = tag, Text = GetPropertyNameDesc(pi) }.Also(it => it.CheckedChanged += ControlEventHandler); } protected override bool GetTValue(CheckBox c) => c.Checked; protected override bool TValueEquality(bool a, bool b) => a == b; } public sealed class ComparisonColors { public readonly Color Changed; public readonly Color ChangedInvalid; public readonly Color ChangedUnset; public readonly Color Unchanged; public readonly Color UnchangedDefault; public ComparisonColors(Color changed, Color changedInvalid, Color changedUnset, Color unchanged, Color unchangedDefault) { Changed = changed; ChangedInvalid = changedInvalid; ChangedUnset = changedUnset; Unchanged = unchanged; UnchangedDefault = unchangedDefault; } public static readonly ComparisonColors Defaults = new ComparisonColors( Color.FromArgb(unchecked((int) 0xFFBF5F1F)), Color.FromArgb(unchecked((int) 0xFF9F0000)), Color.FromArgb(unchecked((int) 0xFFBF1F5F)), Color.FromArgb(unchecked((int) 0xFF00003F)), Color.Black ); } /// Holds computed data that, with reference to a specified config-containing type, cannot change during the lifetime of the program. public sealed class ConfigEditorCache { public readonly IDictionary DefaultValues = new Dictionary(); private readonly IReadOnlyDictionary FallbackGenerators; private readonly IConfigPropEditorUIGen FinalFallbackGenerator; public readonly IList<(string, FieldInfo)> Groups = new List<(string, FieldInfo)>(); public readonly IList<(string, PropertyInfo, IConfigPropEditorUIGen)> PropEditorUIGenerators = new List<(string, PropertyInfo, IConfigPropEditorUIGen)>(); public ConfigEditorCache(Type configType, IReadOnlyDictionary? fallbackGenerators = null, IConfigPropEditorUIGen? finalFallbackGenerator = null) { FallbackGenerators = fallbackGenerators == null ? DefaultFallbackGeneratorSet : new Dictionary().Also(it => { // Concat will effectively use parameter to overwrite where specified foreach (var kvp in DefaultFallbackGeneratorSet.Concat(fallbackGenerators)) it[kvp.Key] = kvp.Value; }); FinalFallbackGenerator = finalFallbackGenerator ?? new UnrepresentablePropEditorUIGen(); static object? TrueGenericDefault(Type type) { try { return Activator.CreateInstance(type); } catch { return null; } } void TraversePropertiesOf(Type type, string nesting) { foreach (var pi in type.GetProperties() .Where(pi => pi.GetCustomAttributes(typeof(EditableAttribute), false).All(attr => ((EditableAttribute) attr).AllowEdit))) { var gen = FallbackGenerators.TryGetValue(pi.PropertyType, out var fallbackGen) ? fallbackGen : FinalFallbackGenerator; if (pi.GetCustomAttributes(typeof(EditorUIGeneratorAttribute), false).FirstOrDefault() is EditorUIGeneratorAttribute attr && typeof(IConfigPropEditorUIGen).IsAssignableFrom(attr.GeneratorType) && TrueGenericDefault(attr.GeneratorType) is IConfigPropEditorUIGen overrideGen) { gen = overrideGen; } PropEditorUIGenerators.Add((nesting, pi, gen)); DefaultValues[$"{nesting}/{pi.Name}"] = pi.GetCustomAttributes(typeof(DefaultValueAttribute), false).FirstOrDefault() ?.Let(it => ((DefaultValueAttribute) it).Value) ?? TrueGenericDefault(pi.PropertyType); } foreach (var fi in type.GetFields() .Where(fi => fi.CustomAttributes.Any(cad => cad.AttributeType == typeof(ConfigGroupingStructAttribute)))) { Groups.Add((nesting, fi)); TraversePropertiesOf(fi.FieldType, $"{nesting}/{fi.Name}"); } } TraversePropertiesOf(configType, string.Empty); } private static readonly IReadOnlyDictionary DefaultFallbackGeneratorSet = new Dictionary { [typeof(bool)] = new CheckBoxForBoolEditorUIGen(), [typeof(int)] = new NumericUpDownForInt32EditorUIGen(), [typeof(string)] = new TextBoxForStringEditorUIGen() }; } public sealed class ConfigEditorMetadata { public readonly IDictionary BaselineValues = new Dictionary(); /// public readonly ConfigEditorCache Cache; public readonly ComparisonColors ComparisonColors; /// global cache /// default of uses public ConfigEditorMetadata(ConfigEditorCache cache, ComparisonColors? colors = null) { Cache = cache; ComparisonColors = colors ?? ComparisonColors.Defaults; } } public sealed class ConfigPropEditorUITag { public readonly IConfigPropEditorUIGen Generator; public readonly ConfigEditorMetadata Metadata; public ConfigPropEditorUITag(ConfigEditorMetadata metadata, IConfigPropEditorUIGen generator) { Metadata = metadata; Generator = generator; } } public abstract class ConfigPropEditorUIGen : IConfigPropEditorUIGen where TListed : Control { protected abstract void ControlEventHandler(object sender, EventArgs args); /// protected abstract TListed GenerateControl(string nesting, PropertyInfo pi, object config, ConfigEditorMetadata metadata); Control IConfigPropEditorUIGen.GenerateControl(string nesting, PropertyInfo pi, object config, ConfigEditorMetadata metadata) => GenerateControl(nesting, pi, config, metadata); /// protected abstract TValue GetTValue(TListed c); object? IConfigPropEditorUIGen.GetTValue(Control c) => GetTValue((TListed) c); /// /// Default implementation didn't play nice with , so multiple behaviours are available for custom generators: /// inherit from or instead. /// protected abstract bool MatchesBaseline(TListed c, ConfigEditorMetadata metadata); bool IConfigPropEditorUIGen.MatchesBaseline(Control c, ConfigEditorMetadata metadata) => MatchesBaseline((TListed) c, metadata); protected virtual string SerializeTValue(TValue v) => v?.ToString() ?? NULL_SERIALIZATION; #pragma warning disable CS8600 #pragma warning disable CS8604 string IConfigPropEditorUIGen.SerializeTValue(object? v) => SerializeTValue((TValue) v); #pragma warning restore CS8604 #pragma warning restore CS8600 /// protected abstract bool TValueEquality(TValue a, TValue b); #pragma warning disable CS8600 #pragma warning disable CS8604 bool IConfigPropEditorUIGen.TValueEquality(object? a, object? b) => TValueEquality((TValue) a, (TValue) b); #pragma warning restore CS8604 #pragma warning restore CS8600 protected const string NULL_SERIALIZATION = "(null)"; protected static Color GetComparisonColor(string nestedName, T? currentValue, ConfigPropEditorUITag tag) where T : class => tag.Generator.TValueEquality(currentValue, (T?) tag.Metadata.BaselineValues[nestedName]) ? GetUnchangedComparisonColor(nestedName, currentValue, tag) : tag.Generator.TValueEquality(currentValue, (T?) tag.Metadata.Cache.DefaultValues[nestedName]) ? tag.Metadata.ComparisonColors.ChangedUnset : tag.Metadata.ComparisonColors.Changed; protected static Color GetComparisonColor(string nestedName, in T currentValue, ConfigPropEditorUITag tag) where T : struct => tag.Generator.TValueEquality(currentValue, tag.Metadata.BaselineValues[nestedName] is T baseline ? baseline : throw new Exception()) ? GetUnchangedComparisonColor(nestedName, in currentValue, tag) : tag.Generator.TValueEquality(currentValue, tag.Metadata.Cache.DefaultValues[nestedName] is T defaultValue ? defaultValue : throw new Exception()) ? tag.Metadata.ComparisonColors.ChangedUnset : tag.Metadata.ComparisonColors.Changed; protected static string GetPropertyNameDesc(PropertyInfo pi) => pi.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() ?.Let(it => $"{pi.Name}: {((DescriptionAttribute) it).Description}") ?? pi.Name; protected static Color GetUnchangedComparisonColor(string nestedName, T? currentValue, ConfigPropEditorUITag tag) where T : class => tag.Generator.TValueEquality(currentValue, (T?) tag.Metadata.Cache.DefaultValues[nestedName]) ? tag.Metadata.ComparisonColors.UnchangedDefault : tag.Metadata.ComparisonColors.Unchanged; protected static Color GetUnchangedComparisonColor(string nestedName, in T currentValue, ConfigPropEditorUITag tag) where T : struct => tag.Generator.TValueEquality(currentValue, tag.Metadata.Cache.DefaultValues[nestedName] is T defaultValue ? defaultValue : throw new Exception()) ? tag.Metadata.ComparisonColors.UnchangedDefault : tag.Metadata.ComparisonColors.Unchanged; } public abstract class ConfigPropEditorUIGenRefT : ConfigPropEditorUIGen where TListed : Control where TValue : class { /// /// Checked in in the case where the baseline value is . /// If its implementation returns , an exception will be thrown, otherwise ' implementation will be called with . /// protected abstract bool AllowNull { get; } /// protected override bool MatchesBaseline(TListed c, ConfigEditorMetadata metadata) => metadata.BaselineValues[c.Name] is TValue v ? TValueEquality(GetTValue(c), v) : AllowNull ? TValueEquality(GetTValue(c), null) : throw new Exception(); } public abstract class ConfigPropEditorUIGenValT : ConfigPropEditorUIGen where TListed : Control where TValue : struct { /// protected override bool MatchesBaseline(TListed c, ConfigEditorMetadata metadata) => metadata.BaselineValues[c.Name] is TValue v ? TValueEquality(GetTValue(c), v) : throw new Exception(); } public sealed class NumericUpDownForInt32EditorUIGen : ConfigPropEditorUIGenValT { protected override void ControlEventHandler(object sender, EventArgs args) { var nud = (NumericUpDown) sender; nud.Parent.ForeColor = GetComparisonColor(nud.Parent.Name, (int) nud.Value, (ConfigPropEditorUITag) nud.Parent.Tag); } protected override FlowLayoutPanel GenerateControl(string nesting, PropertyInfo pi, object config, ConfigEditorMetadata metadata) { if (pi.PropertyType != typeof(int)) throw new Exception(); var baseline = (int) pi.GetValue(config); var nestedName = $"{nesting}/{pi.Name}"; metadata.BaselineValues[nestedName] = baseline; var tag = new ConfigPropEditorUITag(metadata, this); return new FlowLayoutPanel { AutoSize = true, Controls = { new Label { Anchor = AnchorStyles.None, AutoSize = true, Text = GetPropertyNameDesc(pi) }, new NumericUpDown { Maximum = int.MaxValue, Minimum = int.MinValue, Size = new Size(72, 20), Value = baseline }.Also(it => { if (pi.GetCustomAttributes(typeof(RangeAttribute), false).FirstOrDefault() is RangeAttribute range) { it.Maximum = (int) range.Maximum; it.Minimum = (int) range.Minimum; } it.ValueChanged += ControlEventHandler; }) }, ForeColor = GetUnchangedComparisonColor(nestedName, in baseline, tag), Name = nestedName, Tag = tag }; } protected override int GetTValue(FlowLayoutPanel c) => (int) ((NumericUpDown) c.Controls[1]).Value; protected override bool TValueEquality(int a, int b) => a == b; } public sealed class TextBoxForStringEditorUIGen : ConfigPropEditorUIGenRefT { protected override bool AllowNull => true; protected override void ControlEventHandler(object sender, EventArgs args) { var tb = (TextBox) sender; tb.Parent.ForeColor = GetComparisonColor(tb.Parent.Name, tb.Text, (ConfigPropEditorUITag) tb.Parent.Tag); } protected override FlowLayoutPanel GenerateControl(string nesting, PropertyInfo pi, object config, ConfigEditorMetadata metadata) { if (!pi.PropertyType.IsAssignableFrom(typeof(string))) throw new Exception(); var baseline = (string) pi.GetValue(config); var nestedName = $"{nesting}/{pi.Name}"; metadata.BaselineValues[nestedName] = baseline; var tag = new ConfigPropEditorUITag(metadata, this); return new FlowLayoutPanel { AutoSize = true, Controls = { new Label { Anchor = AnchorStyles.None, AutoSize = true, Text = GetPropertyNameDesc(pi) }, new TextBox { AutoSize = true, Text = baseline }.Also(it => it.TextChanged += ControlEventHandler) }, ForeColor = GetUnchangedComparisonColor(nestedName, baseline, tag), Name = nestedName, Tag = tag }; } protected override string? GetTValue(FlowLayoutPanel c) => ((TextBox) c.Controls[1]).Text; protected override string SerializeTValue(string? v) => v == null ? NULL_SERIALIZATION : $"\"{v}\""; protected override bool TValueEquality(string? a, string? b) => a == b || string.IsNullOrEmpty(a) && string.IsNullOrEmpty(b); } public sealed class UnrepresentablePropEditorUIGen : ConfigPropEditorUIGen { protected override void ControlEventHandler(object sender, EventArgs args) => throw new InvalidOperationException(); protected override GroupBox GenerateControl(string nesting, PropertyInfo pi, object config, ConfigEditorMetadata metadata) => new GroupBox { AutoSize = true, Controls = { new FlowLayoutPanel { AutoSize = true, Controls = { new Label { AutoSize = true, Text = $"no editor found for type {pi.PropertyType}" } }, Location = new Point(4, 16), MaximumSize = new Size(int.MaxValue, 20) } }, MaximumSize = new Size(int.MaxValue, 40), Name = $"{nesting}/{pi.Name}", Tag = new ConfigPropEditorUITag(metadata, this), Text = pi.Name }; protected override object? GetTValue(GroupBox c) => throw new InvalidOperationException(); protected override bool MatchesBaseline(GroupBox c, ConfigEditorMetadata metadata) => true; protected override bool TValueEquality(object? a, object? b) => throw new InvalidOperationException(); } } }