using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; using BizHawk.Client.Common; using BizHawk.Client.EmuHawk; using static BizHawk.Experiment.AutoGenConfig.ConfigEditorUIGenerators; namespace BizHawk.Experiment.AutoGenConfig { [ExternalTool("AutoGenConfig")] public class AutoGenConfigForm : ToolFormBase, IExternalToolForm { public ApiContainer? _apiContainer { get; set; } private ApiContainer APIs => _apiContainer!; private static readonly WeakReference _cache = new WeakReference(new ConfigEditorCache(typeof(Config))); private static ConfigEditorCache Cache => _cache.TryGetTarget(out var c) ? c : new ConfigEditorCache(typeof(Config)).Also(_cache.SetTarget); private readonly IDictionary GroupUIs = new Dictionary(); private readonly ConfigEditorMetadata Metadata = new ConfigEditorMetadata(Cache); protected override string WindowTitleStatic => "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 = 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(GroupUIs[string.Empty] = new FlowLayoutPanel { Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, AutoScroll = true, FlowDirection = FlowDirection.TopDown, Location = new Point(4, 32), 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 = "Review and Save..." }.Also(it => it.Click += (clickEventSender, clickEventArgs) => { var state = GroupUIs.Values.SelectMany(group => group.Controls.Cast()) .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 ?? throw new Exception("never hit"), 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 }, 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 `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) { GroupUIs[nesting].Controls.Add(new GroupBox { Controls = { new FlowLayoutPanel { AutoScroll = true, AutoSize = true, Dock = DockStyle.Fill, FlowDirection = FlowDirection.TopDown, WrapContents = false }.Also(it => GroupUIs[$"{nesting}/{fi.Name}"] = it) }, Size = new Size(560, 300), Text = fi.Name }); } var config = (APIs.Emulation as EmulationApi ?? throw new Exception("required API wasn't fulfilled")).ForbiddenConfigReference; var groupings = new Dictionary { [string.Empty] = config }; void TraverseGroupings(object groupingObj, string parentNesting) { foreach (var (_, fi) in Metadata.Cache.Groups.Where(tuple => tuple.Item1 == parentNesting)) { var nesting = $"{parentNesting}/{fi.Name}"; TraverseGroupings(groupings[nesting] = fi.GetValue(groupingObj), nesting); } } TraverseGroupings(config, string.Empty); foreach (var (nesting, pi, gen) in Metadata.Cache.PropEditorUIGenerators) { GroupUIs[nesting].Controls.Add(gen.GenerateControl(nesting, pi, groupings[nesting], Metadata)); } discardButton.Select(); }; ResumeLayout(); } } }