Yet more work on AutoGenConfig
This commit is contained in:
parent
6d93fe03f5
commit
c0fcd01b69
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class EditorUIGeneratorAttribute : Attribute
|
||||
{
|
||||
public readonly Type GeneratorType;
|
||||
|
||||
public EditorUIGeneratorAttribute(Type generatorType)
|
||||
{
|
||||
GeneratorType = generatorType;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
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.ApiHawk;
|
||||
|
@ -17,66 +14,17 @@ namespace BizHawk.Experiment.AutoGenConfig
|
|||
[ExternalTool("AutoGenConfig")]
|
||||
public class AutoGenConfigForm : Form, IExternalToolForm
|
||||
{
|
||||
private static readonly IList<(string, FieldInfo)> CachedGroupings;
|
||||
private static readonly WeakReference<ConfigEditorCache> _cache = new WeakReference<ConfigEditorCache>(new ConfigEditorCache(typeof(Config)));
|
||||
|
||||
private static readonly IList<(string, PropertyInfo, IConfigPropEditorUIGen<Control>)> CachedPropEditorUIGenerators;
|
||||
private static ConfigEditorCache Cache => _cache.TryGetTarget(out var c) ? c : new ConfigEditorCache(typeof(Config)).Also(_cache.SetTarget);
|
||||
|
||||
public static ComparisonColors ComparisonColors = new ComparisonColors
|
||||
{
|
||||
Changed = Color.FromArgb(unchecked((int) 0xFFBF5F1F)),
|
||||
ChangedInvalid = Color.FromArgb(unchecked((int) 0xFF9F0000)),
|
||||
ChangedUnset = Color.FromArgb(unchecked((int) 0xFFBF1F5F)),
|
||||
Unchanged = Color.FromArgb(unchecked((int) 0xFF00003F)),
|
||||
UnchangedDefault = Color.Black
|
||||
};
|
||||
private readonly IDictionary<string, Control> GroupUIs = new Dictionary<string, Control>();
|
||||
|
||||
public static readonly IDictionary<string, object?> DefaultValues;
|
||||
|
||||
static AutoGenConfigForm()
|
||||
{
|
||||
CachedGroupings = new List<(string, FieldInfo)>();
|
||||
CachedPropEditorUIGenerators = new List<(string, PropertyInfo, IConfigPropEditorUIGen<Control>)>();
|
||||
DefaultValues = new Dictionary<string, object?>();
|
||||
static void TraversePropertiesOf(Type type, string nesting)
|
||||
{
|
||||
foreach (var pi in type.GetProperties()
|
||||
.Where(pi => pi.GetCustomAttributes(typeof(EditableAttribute), false).All(attr => ((EditableAttribute) attr).AllowEdit)))
|
||||
{
|
||||
CachedPropEditorUIGenerators.Add((nesting, pi, FallbackGenerators.TryGetValue(pi.PropertyType, out var gen) ? gen : FinalFallbackGenerator));
|
||||
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))))
|
||||
{
|
||||
CachedGroupings.Add((nesting, fi));
|
||||
TraversePropertiesOf(fi.FieldType, $"{nesting}/{fi.Name}");
|
||||
}
|
||||
}
|
||||
TraversePropertiesOf(typeof(Config), string.Empty);
|
||||
}
|
||||
|
||||
/// <returns>value types: default(T); ref types: calls default (no-arg) ctor if it exists, else null</returns>
|
||||
private static object? TrueGenericDefault(Type t)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Activator.CreateInstance(t);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly IDictionary<string, object?> BaselineValues = new Dictionary<string, object?>();
|
||||
private readonly ConfigEditorMetadata Metadata = new ConfigEditorMetadata(Cache);
|
||||
|
||||
[RequiredApi]
|
||||
private IEmu? EmuHawkAPI { get; set; }
|
||||
|
||||
public readonly IDictionary<string, Control> GroupingUIs = new Dictionary<string, Control>();
|
||||
|
||||
public override string Text => "AutoGenConfig";
|
||||
|
||||
public bool UpdateBefore => false;
|
||||
|
@ -84,24 +32,25 @@ namespace BizHawk.Experiment.AutoGenConfig
|
|||
public AutoGenConfigForm()
|
||||
{
|
||||
ClientSize = new Size(640, 720);
|
||||
KeyPreview = true;
|
||||
SuspendLayout();
|
||||
Controls.Add(new FlowLayoutPanel {
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
|
||||
BorderStyle = BorderStyle.FixedSingle,
|
||||
Controls = {
|
||||
new Label { AutoSize = true, Text = "Legend:" },
|
||||
new Label { AutoSize = true, ForeColor = ComparisonColors.UnchangedDefault, Text = "default, unchanged" },
|
||||
new Label { AutoSize = true, ForeColor = ComparisonColors.Unchanged, Text = "custom, unchanged" },
|
||||
new Label { AutoSize = true, ForeColor = ComparisonColors.ChangedUnset, Text = "default, was custom" },
|
||||
new Label { AutoSize = true, ForeColor = ComparisonColors.ChangedInvalid, Text = "invalid" },
|
||||
new Label { AutoSize = true, ForeColor = ComparisonColors.Changed, Text = "custom, changed" }
|
||||
new Label { AutoSize = true, ForeColor = Metadata.ComparisonColors.UnchangedDefault, Text = "default, unchanged" },
|
||||
new Label { AutoSize = true, ForeColor = Metadata.ComparisonColors.Unchanged, Text = "custom, unchanged" },
|
||||
new Label { AutoSize = true, ForeColor = Metadata.ComparisonColors.ChangedUnset, Text = "default, was custom" },
|
||||
new Label { AutoSize = true, ForeColor = Metadata.ComparisonColors.ChangedInvalid, Text = "invalid" },
|
||||
new Label { AutoSize = true, ForeColor = Metadata.ComparisonColors.Changed, Text = "custom, changed" }
|
||||
},
|
||||
Location = new Point(4, 4),
|
||||
Padding = new Padding(0, 4, 0, 0),
|
||||
Size = new Size(ClientSize.Width - 8, 24),
|
||||
WrapContents = false
|
||||
});
|
||||
Controls.Add(GroupingUIs[string.Empty] = new FlowLayoutPanel {
|
||||
Controls.Add(GroupUIs[string.Empty] = new FlowLayoutPanel {
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
|
||||
AutoScroll = true,
|
||||
FlowDirection = FlowDirection.TopDown,
|
||||
|
@ -109,30 +58,65 @@ namespace BizHawk.Experiment.AutoGenConfig
|
|||
Size = new Size(ClientSize.Width - 8, ClientSize.Height - 64),
|
||||
WrapContents = false
|
||||
});
|
||||
var discardButton = new Button {
|
||||
Size = new Size(128, 24),
|
||||
Text = "Discard Changes"
|
||||
}.Also(it => it.Click += (clickEventSender, clickEventArgs) => Close());
|
||||
Controls.Add(new FlowLayoutPanel {
|
||||
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
|
||||
AutoScroll = true,
|
||||
AutoSize = true,
|
||||
Controls = {
|
||||
new Button {
|
||||
Size = new Size(128, 24),
|
||||
Text = "Discard Changes"
|
||||
}.Also(it => it.Click += (clickEventSender, clickEventArgs) => Close()),
|
||||
new Button {
|
||||
Size = new Size(128, 24),
|
||||
Text = "Review and Save..."
|
||||
}.Also(it => it.Click += (clickEventSender, clickEventArgs) => Close())
|
||||
}.Also(it => it.Click += (clickEventSender, clickEventArgs) =>
|
||||
{
|
||||
var state = GroupUIs.Values.SelectMany(group => group.Controls.Cast<Control>())
|
||||
.Select(c => (c, (c.Tag as ConfigPropEditorUITag)?.Generator))
|
||||
.Where(tuple =>
|
||||
tuple.Generator != null // Already iterating nested-config groupboxes as GroupUIs.Values; maybe this can be changed to iterate recursively starting with `GroupUIs[""]`?
|
||||
&& !tuple.Generator.MatchesBaseline(tuple.c, Metadata)
|
||||
)
|
||||
.Select(tuple => (tuple.c.Name, tuple.Generator, Baseline: Metadata.BaselineValues[tuple.c.Name], Current: tuple.Generator.GetTValue(tuple.c)))
|
||||
.Where(tuple => tuple.Baseline != tuple.Current)
|
||||
.ToList();
|
||||
if (state.Count == 0) {
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
string DescribeChange((string Name, IConfigPropEditorUIGen Generator, object? Baseline, object? Current) change)
|
||||
=> $"{change.Name}: {change.Generator.SerializeTValue(change.Baseline)} => {change.Generator.SerializeTValue(change.Current)}{(change.Generator.TValueEquality(change.Current, Metadata.Cache.DefaultValues[change.Name]) ? " (default)" : string.Empty)}";
|
||||
if (MessageBox.Show(
|
||||
$"Choose OK to save these changes to the config (in-memory, close EmuHawk to save to disk):\n\n{string.Join("\n", state.Select(DescribeChange))}",
|
||||
"Save changes?",
|
||||
MessageBoxButtons.OKCancel
|
||||
) == DialogResult.OK)
|
||||
{
|
||||
//TODO save
|
||||
Close();
|
||||
}
|
||||
}),
|
||||
discardButton
|
||||
},
|
||||
FlowDirection = FlowDirection.RightToLeft,
|
||||
Location = new Point(ClientSize.Width - 201, ClientSize.Height - 31),
|
||||
WrapContents = false
|
||||
});
|
||||
KeyDown += (keyDownEventSender, keyDownEventArgs) =>
|
||||
{
|
||||
// Eat TAB and Shift+TAB, do the expected tab behaviour. This means no tab in textboxes.
|
||||
if (keyDownEventArgs.KeyCode == Keys.Tab)
|
||||
{
|
||||
ProcessTabKey(keyDownEventArgs.Modifiers != Keys.Shift);
|
||||
keyDownEventArgs.Handled = true;
|
||||
}
|
||||
};
|
||||
Load += (loadEventSender, loadEventArgs) =>
|
||||
{
|
||||
// This magic works so long as `GroupingUIs[""]` is set to the main FLP before loading, and we create all the GroupBoxes before trying to populate them.
|
||||
foreach (var (nesting, fi) in CachedGroupings)
|
||||
// This magic works so long as `GroupUIs[""]` is set to the main FLP before loading, and we create all the GroupBoxes before trying to populate them.
|
||||
foreach (var (nesting, fi) in Metadata.Cache.Groups)
|
||||
{
|
||||
GroupingUIs[nesting].Controls.Add(new GroupBox {
|
||||
GroupUIs[nesting].Controls.Add(new GroupBox {
|
||||
Controls = {
|
||||
new FlowLayoutPanel {
|
||||
AutoScroll = true,
|
||||
|
@ -140,27 +124,28 @@ namespace BizHawk.Experiment.AutoGenConfig
|
|||
Dock = DockStyle.Fill,
|
||||
FlowDirection = FlowDirection.TopDown,
|
||||
WrapContents = false
|
||||
}.Also(it => GroupingUIs[$"{nesting}/{fi.Name}"] = it)
|
||||
}.Also(it => GroupUIs[$"{nesting}/{fi.Name}"] = it)
|
||||
},
|
||||
Size = new Size(400, 300),
|
||||
Size = new Size(560, 300),
|
||||
Text = fi.Name
|
||||
});
|
||||
}
|
||||
var config = (EmuHawkAPI as EmuApi ?? throw new Exception("required API wasn't fulfilled")).ForbiddenConfigReference;
|
||||
var groupings = new Dictionary<string, object> { [string.Empty] = config };
|
||||
void TraverseGroupings(object groupingObj, string nesting)
|
||||
void TraverseGroupings(object groupingObj, string parentNesting)
|
||||
{
|
||||
foreach (var (_, fi) in CachedGroupings.Where(tuple => tuple.Item1 == nesting))
|
||||
foreach (var (_, fi) in Metadata.Cache.Groups.Where(tuple => tuple.Item1 == parentNesting))
|
||||
{
|
||||
var newNesting = $"{nesting}/{fi.Name}";
|
||||
TraverseGroupings(groupings[newNesting] = fi.GetValue(groupingObj), newNesting);
|
||||
var nesting = $"{parentNesting}/{fi.Name}";
|
||||
TraverseGroupings(groupings[nesting] = fi.GetValue(groupingObj), nesting);
|
||||
}
|
||||
}
|
||||
TraverseGroupings(config, string.Empty);
|
||||
foreach (var (nesting, pi, gen) in CachedPropEditorUIGenerators)
|
||||
foreach (var (nesting, pi, gen) in Metadata.Cache.PropEditorUIGenerators)
|
||||
{
|
||||
GroupingUIs[nesting].Controls.Add(gen.GenerateControl(nesting, pi, groupings[nesting], BaselineValues));
|
||||
GroupUIs[nesting].Controls.Add(gen.GenerateControl(nesting, pi, groupings[nesting], Metadata));
|
||||
}
|
||||
discardButton.Select();
|
||||
};
|
||||
ResumeLayout();
|
||||
}
|
||||
|
|
|
@ -7,114 +7,304 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using BizHawk.Client.Common;
|
||||
|
||||
namespace BizHawk.Experiment.AutoGenConfig
|
||||
{
|
||||
public static class ConfigEditorUIGenerators
|
||||
{
|
||||
public static readonly IDictionary<Type, IConfigPropEditorUIGen<Control>> FallbackGenerators = new Dictionary<Type, IConfigPropEditorUIGen<Control>> {
|
||||
[typeof(bool)] = new CheckBoxForBoolEditorUIGen(),
|
||||
[typeof(int)] = new NumericUpDownForInt32EditorUIGen(),
|
||||
[typeof(string)] = new TextBoxForStringEditorUIGen()
|
||||
};
|
||||
|
||||
public static readonly IConfigPropEditorUIGen<GroupBox> FinalFallbackGenerator = new UnrepresentablePropEditorUIGen();
|
||||
|
||||
private static Color GetComparisonColorRefT<T>(string nestedName, T? currentValue, AutoGenConfigForm parentForm, Func<T?, T?, bool> equalityFunc)
|
||||
where T : class
|
||||
=> equalityFunc(currentValue, parentForm.BaselineValues[nestedName] as T)
|
||||
? GetInitComparisonColorRefT(nestedName, currentValue, equalityFunc)
|
||||
: equalityFunc(currentValue, AutoGenConfigForm.DefaultValues[nestedName] as T)
|
||||
? AutoGenConfigForm.ComparisonColors.ChangedUnset
|
||||
: AutoGenConfigForm.ComparisonColors.Changed;
|
||||
|
||||
private static Color GetComparisonColorValT<T>(string nestedName, T? currentValue, AutoGenConfigForm parentForm, Func<T?, T?, bool> equalityFunc)
|
||||
where T : struct
|
||||
=> equalityFunc(currentValue, parentForm.BaselineValues[nestedName]?.Let(it => (T) it))
|
||||
? GetInitComparisonColorValT(nestedName, currentValue, equalityFunc)
|
||||
: equalityFunc(currentValue, AutoGenConfigForm.DefaultValues[nestedName]?.Let(it => (T) it))
|
||||
? AutoGenConfigForm.ComparisonColors.ChangedUnset
|
||||
: AutoGenConfigForm.ComparisonColors.Changed;
|
||||
|
||||
private static Color GetInitComparisonColorRefT<T>(string nestedName, T? currentValue, Func<T?, T?, bool> equalityFunc)
|
||||
where T : class
|
||||
=> equalityFunc(currentValue, AutoGenConfigForm.DefaultValues[nestedName] as T)
|
||||
? AutoGenConfigForm.ComparisonColors.UnchangedDefault
|
||||
: AutoGenConfigForm.ComparisonColors.Unchanged;
|
||||
|
||||
private static Color GetInitComparisonColorValT<T>(string nestedName, T? currentValue, Func<T?, T?, bool> equalityFunc)
|
||||
where T : struct
|
||||
=> equalityFunc(currentValue, AutoGenConfigForm.DefaultValues[nestedName]?.Let(it => (T) it))
|
||||
? AutoGenConfigForm.ComparisonColors.UnchangedDefault
|
||||
: AutoGenConfigForm.ComparisonColors.Unchanged;
|
||||
|
||||
private static AutoGenConfigForm GetMainFormParent(Control c)
|
||||
public interface IConfigPropEditorUIGen
|
||||
{
|
||||
var parent = c.Parent;
|
||||
while (!(parent is AutoGenConfigForm)) parent = parent.Parent;
|
||||
return (AutoGenConfigForm) parent;
|
||||
bool MatchesBaseline(Control c, ConfigEditorMetadata metadata);
|
||||
|
||||
/// <returns>
|
||||
/// A <see cref="Control"/> with <see cref="Control.Name"/> set to the property name (including nesting) and <see cref="Control.Tag"/> set to a <see cref="ConfigPropEditorUITag"/>.<br/>
|
||||
/// Multiple <see cref="Control">Controls</see> may be needed, the returned value is always the topmost parent (probably a <see cref="FlowLayoutPanel"/>).
|
||||
/// </returns>
|
||||
/// <remarks><see cref="ConfigEditorMetadata.BaselineValues"/> in <paramref name="metadata"/> will be updated.</remarks>
|
||||
Control GenerateControl(string nesting, PropertyInfo pi, object config, ConfigEditorMetadata metadata);
|
||||
|
||||
/// <returns>value represented by <paramref name="c"/> or its nested <see cref="Control">Controls</see></returns>
|
||||
object? GetTValue(Control c);
|
||||
|
||||
string SerializeTValue(object? v);
|
||||
|
||||
/// <returns><see langword="true"/> iff equal</returns>
|
||||
bool TValueEquality(object? a, object? b);
|
||||
}
|
||||
|
||||
private static string GetPropertyNameDesc(PropertyInfo pi)
|
||||
=> pi.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault()
|
||||
?.Let(it => $"{pi.Name}: {((DescriptionAttribute) it).Description}")
|
||||
?? pi.Name;
|
||||
|
||||
public struct ComparisonColors
|
||||
public sealed class CheckBoxForBoolEditorUIGen : ConfigPropEditorUIGenValT<CheckBox, bool>
|
||||
{
|
||||
public Color Changed;
|
||||
public Color ChangedInvalid;
|
||||
public Color ChangedUnset;
|
||||
public Color Unchanged;
|
||||
public Color UnchangedDefault;
|
||||
}
|
||||
protected override void ControlEventHandler(object sender, EventArgs args)
|
||||
{
|
||||
var cb = (CheckBox) sender;
|
||||
cb.ForeColor = GetComparisonColor(cb.Name, cb.Checked, (ConfigPropEditorUITag) cb.Tag);
|
||||
}
|
||||
|
||||
public interface IConfigPropEditorUIGen<out TControl>
|
||||
where TControl : Control
|
||||
{
|
||||
TControl GenerateControl(string nesting, PropertyInfo pi, object config, IDictionary<string, object?> baselineValues);
|
||||
}
|
||||
|
||||
private class CheckBoxForBoolEditorUIGen : IConfigPropEditorUIGen<CheckBox>
|
||||
{
|
||||
private static bool BoolEquality(bool? a, bool? b) => a == b;
|
||||
|
||||
private static void CheckBoxClickHandler(object changedEventSender, EventArgs changedEventArgs)
|
||||
=> ((CheckBox) changedEventSender).Let(cb =>
|
||||
cb.ForeColor = GetComparisonColorValT<bool>(cb.Name, cb.Checked, GetMainFormParent(cb), BoolEquality)
|
||||
);
|
||||
|
||||
public CheckBox GenerateControl(string nesting, PropertyInfo pi, object config, IDictionary<string, object?> baselineValues)
|
||||
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}";
|
||||
baselineValues[nestedName] = baseline;
|
||||
metadata.BaselineValues[nestedName] = baseline;
|
||||
var tag = new ConfigPropEditorUITag(metadata, this);
|
||||
return new CheckBox
|
||||
{
|
||||
AutoSize = true,
|
||||
Checked = baseline,
|
||||
ForeColor = GetInitComparisonColorValT<bool>(nestedName, baseline, BoolEquality),
|
||||
ForeColor = GetUnchangedComparisonColor(nestedName, in baseline, tag),
|
||||
Name = nestedName,
|
||||
Tag = tag,
|
||||
Text = GetPropertyNameDesc(pi)
|
||||
}.Also(it => it.CheckedChanged += CheckBoxClickHandler);
|
||||
}.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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Holds computed data that, with reference to a specified config-containing type, cannot change during the lifetime of the program.</summary>
|
||||
public sealed class ConfigEditorCache
|
||||
{
|
||||
public readonly IDictionary<string, object?> DefaultValues = new Dictionary<string, object?>();
|
||||
|
||||
private readonly IReadOnlyDictionary<Type, IConfigPropEditorUIGen> 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<Type, IConfigPropEditorUIGen>? fallbackGenerators = null, IConfigPropEditorUIGen? finalFallbackGenerator = null)
|
||||
{
|
||||
FallbackGenerators = fallbackGenerators == null
|
||||
? DefaultFallbackGeneratorSet
|
||||
: new Dictionary<Type, IConfigPropEditorUIGen>().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<Type, IConfigPropEditorUIGen> DefaultFallbackGeneratorSet = new Dictionary<Type, IConfigPropEditorUIGen> {
|
||||
[typeof(bool)] = new CheckBoxForBoolEditorUIGen(),
|
||||
[typeof(int)] = new NumericUpDownForInt32EditorUIGen(),
|
||||
[typeof(string)] = new TextBoxForStringEditorUIGen()
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class ConfigEditorMetadata
|
||||
{
|
||||
public readonly IDictionary<string, object?> BaselineValues = new Dictionary<string, object?>();
|
||||
|
||||
/// <inheritdoc cref="ConfigEditorCache"/>
|
||||
public readonly ConfigEditorCache Cache;
|
||||
|
||||
public readonly ComparisonColors ComparisonColors;
|
||||
|
||||
/// <param name="colors">default of <see langword="null"/> uses <see cref="ConfigEditorUIGenerators.ComparisonColors.Defaults"/></param>
|
||||
public ConfigEditorMetadata(ConfigEditorCache cache, ComparisonColors? colors = null)
|
||||
{
|
||||
Cache = cache;
|
||||
ComparisonColors = colors ?? ComparisonColors.Defaults;
|
||||
}
|
||||
}
|
||||
|
||||
private class NumericUpDownForInt32EditorUIGen : IConfigPropEditorUIGen<FlowLayoutPanel>
|
||||
public sealed class ConfigPropEditorUITag
|
||||
{
|
||||
private static bool IntEquality(int? a, int? b) => a == b;
|
||||
public readonly IConfigPropEditorUIGen Generator;
|
||||
|
||||
private static void NumericUpDownChangedHandler(object changedEventSender, EventArgs changedEventArgs)
|
||||
=> ((NumericUpDown) changedEventSender).Let(nud =>
|
||||
nud.Parent.ForeColor = GetComparisonColorValT<int>(nud.Name, (int) nud.Value, GetMainFormParent(nud), IntEquality)
|
||||
);
|
||||
public readonly ConfigEditorMetadata Metadata;
|
||||
|
||||
public FlowLayoutPanel GenerateControl(string nesting, PropertyInfo pi, object config, IDictionary<string, object?> baselineValues)
|
||||
public ConfigPropEditorUITag(ConfigEditorMetadata metadata, IConfigPropEditorUIGen generator)
|
||||
{
|
||||
Metadata = metadata;
|
||||
Generator = generator;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ConfigPropEditorUIGen<TListed, TValue> : IConfigPropEditorUIGen
|
||||
where TListed : Control
|
||||
{
|
||||
protected abstract void ControlEventHandler(object sender, EventArgs args);
|
||||
|
||||
/// <inheritdoc cref="IConfigPropEditorUIGen.GenerateControl"/>
|
||||
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);
|
||||
|
||||
/// <inheritdoc cref="IConfigPropEditorUIGen.GetTValue"/>
|
||||
protected abstract TValue GetTValue(TListed c);
|
||||
|
||||
object? IConfigPropEditorUIGen.GetTValue(Control c) => GetTValue((TListed) c);
|
||||
|
||||
/// <remarks>
|
||||
/// Default implementation didn't play nice with <see langword="null"/>, so multiple behaviours are available for custom generators:
|
||||
/// inherit from <see cref="ConfigPropEditorUIGenRefT"/> or <see cref="ConfigPropEditorUIGenValT"/> instead.
|
||||
/// </remarks>
|
||||
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 CS8601
|
||||
string IConfigPropEditorUIGen.SerializeTValue(object? v) => SerializeTValue((TValue) v);
|
||||
#pragma warning restore CS8601
|
||||
|
||||
/// <inheritdoc cref="IConfigPropEditorUIGen.TValueEquality"/>
|
||||
protected abstract bool TValueEquality(TValue a, TValue b);
|
||||
|
||||
#pragma warning disable CS8601
|
||||
bool IConfigPropEditorUIGen.TValueEquality(object? a, object? b) => TValueEquality((TValue) a, (TValue) b);
|
||||
#pragma warning restore CS8601
|
||||
|
||||
protected const string NULL_SERIALIZATION = "(null)";
|
||||
|
||||
protected static Color GetComparisonColor<T>(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<T>(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<T>(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<T>(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<TListed, TValue> : ConfigPropEditorUIGen<TListed, TValue?>
|
||||
where TListed : Control
|
||||
where TValue : class
|
||||
{
|
||||
/// <remarks>
|
||||
/// Checked in <see cref="MatchesBaseline"/> in the case where the baseline value is <see cref="null"/>.
|
||||
/// If its implementation returns <see cref="false"/>, an exception will be thrown, otherwise <see cref="TValueEquality"/>' implementation will be called with <see cref="null"/>.
|
||||
/// </remarks>
|
||||
protected abstract bool AllowNull { get; }
|
||||
|
||||
/// <inheritdoc cref="ConfigPropEditorUIGen.MatchesBaseline"/>
|
||||
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<TListed, TValue> : ConfigPropEditorUIGen<TListed, TValue>
|
||||
where TListed : Control
|
||||
where TValue : struct
|
||||
{
|
||||
/// <inheritdoc cref="ConfigPropEditorUIGen.MatchesBaseline"/>
|
||||
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<FlowLayoutPanel, int>
|
||||
{
|
||||
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}";
|
||||
baselineValues[nestedName] = baseline;
|
||||
metadata.BaselineValues[nestedName] = baseline;
|
||||
var tag = new ConfigPropEditorUITag(metadata, this);
|
||||
return new FlowLayoutPanel {
|
||||
AutoSize = true,
|
||||
Controls = {
|
||||
|
@ -123,7 +313,6 @@ namespace BizHawk.Experiment.AutoGenConfig
|
|||
{
|
||||
Maximum = int.MaxValue,
|
||||
Minimum = int.MinValue,
|
||||
Name = nestedName,
|
||||
Size = new Size(72, 20),
|
||||
Value = baseline
|
||||
}.Also(it =>
|
||||
|
@ -133,43 +322,61 @@ namespace BizHawk.Experiment.AutoGenConfig
|
|||
it.Maximum = (int) range.Maximum;
|
||||
it.Minimum = (int) range.Minimum;
|
||||
}
|
||||
it.ValueChanged += NumericUpDownChangedHandler;
|
||||
it.ValueChanged += ControlEventHandler;
|
||||
})
|
||||
},
|
||||
ForeColor = GetInitComparisonColorValT<int>(nestedName, baseline, IntEquality)
|
||||
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;
|
||||
}
|
||||
|
||||
private class TextBoxForStringEditorUIGen : IConfigPropEditorUIGen<FlowLayoutPanel>
|
||||
public sealed class TextBoxForStringEditorUIGen : ConfigPropEditorUIGenRefT<FlowLayoutPanel, string>
|
||||
{
|
||||
private static readonly Func<string?, string?, bool> StringEquality = string.Equals;
|
||||
protected override bool AllowNull => true;
|
||||
|
||||
public FlowLayoutPanel GenerateControl(string nesting, PropertyInfo pi, object config, IDictionary<string, object?> baselineValues)
|
||||
protected override void ControlEventHandler(object sender, EventArgs args)
|
||||
{
|
||||
if (pi.PropertyType != typeof(string)) throw new Exception();
|
||||
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}";
|
||||
baselineValues[nestedName] = baseline;
|
||||
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, Name = nestedName, Text = baseline }.Also(it => it.TextChanged += TextBoxChangedHandler)
|
||||
new TextBox { AutoSize = true, Text = baseline }.Also(it => it.TextChanged += ControlEventHandler)
|
||||
},
|
||||
ForeColor = GetInitComparisonColorRefT(nestedName, baseline, StringEquality)
|
||||
ForeColor = GetUnchangedComparisonColor(nestedName, baseline, tag),
|
||||
Name = nestedName,
|
||||
Tag = tag
|
||||
};
|
||||
}
|
||||
|
||||
private static void TextBoxChangedHandler(object changedEventSender, EventArgs changedEventArgs)
|
||||
=> ((TextBox) changedEventSender).Let(tb =>
|
||||
tb.Parent.ForeColor = GetComparisonColorRefT(tb.Name, tb.Text, GetMainFormParent(tb), StringEquality)
|
||||
);
|
||||
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) => string.Equals(a, b) || string.IsNullOrEmpty(a) && string.IsNullOrEmpty(b);
|
||||
}
|
||||
|
||||
private class UnrepresentablePropEditorUIGen : IConfigPropEditorUIGen<GroupBox>
|
||||
public sealed class UnrepresentablePropEditorUIGen : ConfigPropEditorUIGen<GroupBox, object?>
|
||||
{
|
||||
public GroupBox GenerateControl(string nesting, PropertyInfo pi, object config, IDictionary<string, object?> baselineValues)
|
||||
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 = {
|
||||
|
@ -181,8 +388,16 @@ namespace BizHawk.Experiment.AutoGenConfig
|
|||
}
|
||||
},
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,6 @@ namespace BizHawk.Experiment.AutoGenConfig
|
|||
return receiver;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Let<T>(this T receiver, Action<T> func) where T : notnull => func(receiver);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static TReturn Let<TRec, TReturn>(this TRec receiver, Func<TRec, TReturn> func) where TRec : notnull => func(receiver);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue