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()
{