diff --git a/src/AcDream.App/UI/Layout/KeyboardConfigController.cs b/src/AcDream.App/UI/Layout/KeyboardConfigController.cs index 0dab3574..83f86405 100644 --- a/src/AcDream.App/UI/Layout/KeyboardConfigController.cs +++ b/src/AcDream.App/UI/Layout/KeyboardConfigController.cs @@ -161,6 +161,11 @@ public sealed class KeyboardConfigController private const int HeaderTemplateIndex = 0; private const int RowTemplateIndex = 1; + /// The screen's own Type-8 tab control hosting the six ActionClass + /// pages (class doc §"Six ActionClass list boxes"). Its authored tab table + /// (dat property 0x2E) marks Movement (0x1000049D) as the default entry. + private const uint TabHostElementId = 0x1000049Bu; + // The row template's 3 key-button children, in "Mapping 1/2/3" column order. private static readonly uint[] KeyButtonIds = { 0x10000030u, 0x10000031u, 0x10000032u }; @@ -291,6 +296,25 @@ public sealed class KeyboardConfigController WireScreenButtons(layout, controller, bindings); + // Gate-2 screenshot finding (#375 residual): OP8 built all six pages + // but never ACTIVATED the screen's own tab control, so every page + // stayed Visible simultaneously — six stacked ListBoxes reading as + // "overlapping text", with six dead tab buttons above them. + // ActivateTabBehavior wires the authored tab clicks and performs the + // authored default-entry switch (Movement, IsDefault=true in the DAT + // table), hiding the other five pages — the exact same call + // OptionsPanelController makes on ITS Type-8 host at construction. + if (layout.FindElement(TabHostElementId) is UiTabPanel tabHost) + { + tabHost.ActivateTabBehavior(); + } + else + { + Console.WriteLine( + $"[D.2b] KeyboardConfigController: tab host 0x{TabHostElementId:X8} not found " + + "(or not a UiTabPanel) — all six ActionClass pages will render stacked."); + } + return controller; } diff --git a/tests/AcDream.App.Tests/UI/Layout/KeyboardConfigControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/KeyboardConfigControllerTests.cs index 1d8857bc..894c7f56 100644 --- a/tests/AcDream.App.Tests/UI/Layout/KeyboardConfigControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/KeyboardConfigControllerTests.cs @@ -128,6 +128,53 @@ public sealed class KeyboardConfigControllerTests Assert.Equal(3, controller.Page.Rows.Count); } + [Fact] + public void Bind_ActivatesTheTabControl_MovementDefaultShown_OtherPagesHidden() + { + // Gate-2 screenshot finding (#375 residual): OP8 never activated the + // screen's own Type-8 tab control, so all six ActionClass pages stayed + // Visible simultaneously — six stacked ListBoxes reading as + // "overlapping text", with dead tab buttons. Bind must run the + // authored default-entry switch (Movement, IsDefault=true) exactly + // like OptionsPanelController does for its own host. + var snapshot = new RetailActionMapSnapshot(new[] + { + Row(0x4, 0x29, RetailActionClass.Movement), + Row(0x5, 0x33, RetailActionClass.Camera), + }); + + ImportedLayout layout = FixtureLoader.LoadKeyboardConfig(); + var fake = new FakeBindings(); + KeyboardConfigController controller = KeyboardConfigController.Bind( + layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!; + Assert.NotNull(controller); + + UiTabPanel tabHost = Assert.IsType(layout.FindElement(0x1000049Bu)); + Assert.True(tabHost.BehaviorActive); + Assert.Equal(0x1000049Du, tabHost.ActivePageElementId); // Movement, the authored default + + // Exactly one page visible: Movement; the other five hidden. + foreach ((uint pageId, bool expectVisible) in new[] + { + (0x1000049Du, true), // Movement + (0x1000049Fu, false), // Camera + (0x100004A1u, false), // Combat + (0x100004A3u, false), // UI + (0x10000211u, false), // CharacterSettings + (0x100004A5u, false), // Emote + }) + { + UiElement? page = UiElement.FindDescendant(tabHost, pageId); + Assert.NotNull(page); + Assert.Equal(expectVisible, page!.Visible); + } + + // The tab buttons are live: switching to Camera flips exclusivity. + tabHost.SwitchTo(0x1000049Fu); + Assert.False(UiElement.FindDescendant(tabHost, 0x1000049Du)!.Visible); + Assert.True(UiElement.FindDescendant(tabHost, 0x1000049Fu)!.Visible); + } + [Fact] public void Bind_MapsKnownActionsAndLeavesUnknownOnesUnmapped() {