Reference · Updated 2552.09.10.16.15
Command Bar — Back & Accept (Hardcoded Approach)
How SystemLink's on-screen prompt strip (◀ Back, Ⓐ Select) is built. Decision (2026-07-20): hardcode it. The bar only ever needs Back and maybe Accept, so we skip CommonUI's dynami
How SystemLink's on-screen prompt strip (◀ Back, Ⓐ Select) is built. Decision (2026-07-20): hardcode it. The bar only ever needs Back and maybe Accept, so we skip CommonUI's dynamic CommonBoundActionBar and build a fixed strip. The dynamic bar is reframed as a MenuKit-product upgrade (see the last section).
### ⚠ Update 2026-08-28 — two corrections and one open contradiction
1.CD_SL_KBMexists and is registered.Config/DefaultGame.inilists bothCD_SL_Xboxand
CD_SL_KBMunder[CommonInputPlatformSettings_Windows …], with a comment recording that the
2026-07-20 gamepad-only decision was reversed — the bar is meant to show on both input types, with
CommonActionWidget swapping between the Xbox glyphs and the KBM key-caps. Every "gamepad-only" and
"no CD_SL_KBM is needed" statement below predates that reversal.
2. ⚠ The Blueprint has not caught up. WBP_SL_CommandBar still contains the KBM-hiding graph
(CommonInputSubsystem→OnInputMethodChanged→ collapse). So the config says "show on both" and the
widget says "gamepad only". Resolve this before trusting either. If the reversal stands, delete the
collapse logic; if hardcoding to gamepad stands, drop CD_SL_KBM from the ini.
3. WBP_SL_BoundActionButton no longer exists — see Cleanup.
#1. The key fact: the bar is display-only
Back and Accept already work without any command bar:
Bcloses the menu viabIsBackHandler(set in theUSLScreenWidgetconstructor).
Aclicks the focused button via CommonUI's default click action.
The command bar changes nothing about behavior — it only shows the player which buttons do what. That's why hardcoding the display is safe: we're not reimplementing input, just drawing two labels + glyphs.
#2. Design: two building blocks
| Asset | Type | Role |
|---|---|---|
WBP_SL_CommandPrompt | UserWidget | One prompt = glyph + label. Reusable, two exposed vars. |
WBP_SL_CommandBar | UserWidget | A HorizontalBox holding the prompts (Back, Accept). |
Use a CommonActionWidget for the glyph — not a plain image or the letter "B". The CommonActionWidget auto-resolves the correct controller glyph for the current gamepad from the input action. That is the one thing worth keeping from CommonUI; everything else (the router, dynamic spawning) we drop.
The command bar is gamepad-only (decision 2026-07-20). On mouse/keyboard the whole prompt strip is
hidden (WBP_SL_CommandBar collapses its prompts when the active device is KBM), so no keyboard glyphs are
needed — there is noCD_SL_KBM. See "Gamepad-only visibility" below. (We briefly authored aCD_SL_KBM
key-cap set, then removed it once the bar was hidden on KBM — the glyphs would never render.)
#Step 1 — Build WBP_SL_CommandPrompt (the reusable prompt)
- Create
WBP_SL_CommandPrompt(/Game/SystemLink/UI/Menus/, parentUserWidget).
- Tree:
HorizontalBox (root)
├─ Glyph [CommonActionWidget]
├─ Spacer (size ~8)
└─ Label [CommonTextBlock] (use Style_… text style to taste)
- Add two instance-editable variables (eye icon open):
InputAction— typeInput Action(object reference, i.e.UInputAction).PromptLabel— typeText.
- In the PreConstruct graph:
Glyph → Set Enhanced Input Action (InputAction)— resolves the glyph brush.Label → Set Text (PromptLabel).
- Compile + Save.
Doing this in PreConstruct means the designer preview updates per instance, and the runtime widget sets
itself up on construction — see the timing note in Step 3.
Why noDT_SL_InputActionsrow?CommonActionWidgetresolves the glyph either from an
EnhancedInputAction(aUInputAction) or from the legacyInputActionsDataTable array — never both.
UCommonActionWidget::GetIcon() is a ternary on both conditions:
```cpp
return EnhancedInputAction && CommonUI::IsEnhancedInputSupportEnabled()
? GetIconForEnhancedInputAction(Subsystem, EnhancedInputAction)
: GetIconForInputActions(Subsystem, InputActions);
```
So a setEnhancedInputActionwins and theInputActionsarray is ignored — if you ever see a prompt
with both set, the row is inert. But the DataTable branch is the fallback, not dead code: leave
EnhancedInputActionnull andDT_SL_InputActionsresolves the glyph normally.
Both paths are live in the project as of 2026-08-28. Prompts here use the UInputAction path;
WBP_SL_Button.TriggeringInputActionandUSLModalWidget.ConfirmInputActionuse the DataTable path
(DT_SL_InputActions :: Confirm). Neither is wrong — just never set both on one widget.
#Step 2 — Build WBP_SL_CommandBar (the strip)
- Open
WBP_SL_CommandBar(already exists as the old placeholder — replace its contents).
2. Delete the placeholder TextBlocks (`B BACK | = OPTIONS | R SOCIAL`). |
|---|
- Tree:
HorizontalBox (root, Right-aligned or Fill to taste)
├─ WBP_SL_CommandPrompt "Accept" InputAction = IA_SL_UI_Confirm, PromptLabel = "Select"
├─ Spacer (size ~24)
└─ WBP_SL_CommandPrompt "Back" InputAction = IA_SL_UI_Back, PromptLabel = "Back"
- Order right→left is a Halo convention (Back rightmost). Flip if you prefer.
- Set each prompt's two exposed vars in the Details panel per the table above.
- Style the root (padding, background) with the angled Halo panel material if desired.
- Compile + Save.
Labels:IA_SL_UI_Back.ActionDescriptionis"Back"andIA_SL_UI_Confirm.ActionDescriptionis
"Select". You can either type the label intoPromptLabel(simplest, done above) or drive it from the
action's description later. Hardcoding the text here is fine.
#Step 3 — Place it so it appears with a menu
The bar must be present/constructed while a menu is up (that's when the glyph can resolve — see the footgun). Two options; A is recommended for reliability and simplicity.
#Option A (recommended) — embed in each screen
Drop WBP_SL_CommandBar into the bottom of each screen's tree (WBP_SL_PauseMenu, WBP_SL_Settings, …), anchored bottom-center or bottom-right.
- Pro: constructed exactly when the screen activates → the UI input context (
IMC_SL_UI) is already
applied, so the glyph resolves correctly. Visible iff the screen is visible — no visibility logic.
- Pro: per-screen freedom — a confirm modal can show
Select + Back, a pure info screen justBack.
- Con: one instance per screen (trivial — it's a single dropped widget; the vars are already set inside
WBP_SL_CommandBar).
#Option B — one shared bar in the layout
Place WBP_SL_CommandBar in WBP_SL_PrimaryGameLayout's root Overlay (Overlay_54), as the last child (top z-order), Vertical = Bottom. Then in the layout graph:
- Bind
MenuStack → On Displayed Widget Changed: setWBP_SL_CommandBarvisibility toVisiblewhen a widget
is active, Collapsed when none.
- Also re-resolve the glyph on show (call the prompt's
Glyph → Set Enhanced Input Actionagain, or a
small RefreshGlyph function) — a bar constructed before IMC_SL_UI is applied may cache an empty glyph. This extra refresh is the price of the shared approach, and the reason A is recommended.
#Step 4 — Verify in PIE (the gate)
- PIE with a gamepad.
- Press Start → pause menu opens.
- Expected: the strip shows
Ⓐ SelectandⒷ Backat the bottom.
- Press B → menu closes → strip goes away (with the screen, Option A).
- On keyboard/mouse (move the mouse to flip the active device): see the note below — this step is what
the 2026-08-28 contradiction is about. As written, the strip hides; as configured, it should swap to key-caps.
Pass criteria: strip appears with the menu on a gamepad (both glyphs render) and B closes. The KBM half of the criteria is unsettled — resolve the contradiction at the top of this doc first.
#Gamepad-only visibility
⚠ Superseded in config, still present in the Blueprint (2026-08-28). The ini reversed this decision and
registeredCD_SL_KBMso the bar can show key-caps on mouse/keyboard, butWBP_SL_CommandBarstill runs the
collapse graph described here. This section documents what the widget currently does, not what the project
has decided it should do. Read it as the thing to delete if the reversal stands.
The original reasoning: the command bar is meaningless on mouse/keyboard (the player clicks directly and Esc is obvious), so it's hidden whenever the active device is KBM. This is driven off the UCommonInputSubsystem — not the input actions or the CommonActionWidget:
OnInputMethodChanged— a BlueprintAssignable delegate on the subsystem; fires with the new
ECommonInputType when the device switches.
GetCurrentInputType()— reads the device right now (for the initial state).
WBP_SL_CommandBar graph (Event Construct):
Get Owning Local Player→Get Local Player Subsystem(Class =CommonInputSubsystem). *(That node is
hidden if the palette's Context Sensitive box is checked — uncheck it, or hold Ctrl while searching.)*
Is Validguard →Bind Event to On Input Method Changed→ a custom event.
- Both the custom event and a one-time
Get Current Input Typeon construct funnel through a single
SetInputType/refresh function that sets the prompt visibility: Gamepad → Visible, everything else → Collapsed. Call it from both paths so the initial state is right (the delegate only fires on change).
Do the bind on Event Construct, not Pre-Construct — Pre-Construct also runs in the designer with no valid
local player. Make the refresh a symmetric if/else (re-shows on returning to a pad, not just hide-on-KBM).
Because the bar never draws on KBM, no No longer true. CD_SL_KBM glyph set is needed — only CD_SL_Xbox is registered.CD_SL_KBM was re-authored and both controller data assets are registered in Config/DefaultGame.ini. If the collapse graph above is removed, the KBM key-caps resolve on their own — CommonActionWidget picks the glyph set from the active input type with no extra wiring.
#Troubleshooting / footguns
- Glyph blank on gamepad too → the
CommonActionWidgetresolved beforeIMC_SL_UIwas live. CommonUI
resolves glyphs via QueryKeysMappedToAction, which only sees currently-applied input contexts. Option A (embed in screen) avoids this because the screen applies Menu input on activate before its children resolve. If on Option B, add the RefreshGlyph call from Step 3B.
- Designer preview is not proof.
UCommonActionWidget::GetIcon()has an editor-only branch that renders
DesignTimeKey and bypasses the runtime path — it will show a perfect glyph even when the runtime one is broken. Validate in PIE only.
- Glyph is the wrong button → check the prompt's
InputActionvar points at the rightIA_SL_UI_*action,
and that action is mapped in IMC_SL_UI.
- Nothing shows at all (Option B) → visibility binding not firing, or the bar is under an opaque menu
background (it's not the last child of the root Overlay).
- Don't wire click/behavior into these prompts. They are pure display. B and A already function via
bIsBackHandler / the default click action. A command prompt is not a button.
#Cleanup
- ~~
WBP_SL_BoundActionButton(built for the dynamic bar) is not used by this approach. Keep it parked
for the MenuKit upgrade below.~~ It is gone — no asset by that name exists under Content/ as of 2026-08-28. The MenuKit upgrade below has to rebuild it; see the note in that section for what it must be.
- The old placeholder text in
WBP_SL_CommandBaris replaced in Step 2.
#Later: the dynamic bar as a MenuKit upgrade
For SystemLink the game, hardcoded Back/Accept is the right call — simpler, and it moves us toward the Settings-menu gate instead of re-fighting the CommonUI glyph/router plumbing.
For MenuKit the Fab product, the dynamic CommonBoundActionBar is the more valuable, expected form — buyers' screens have varied actions and want the bar to populate itself. Swapping the hardcoded strip for the dynamic bar is a self-contained later task, not a rewrite. Treat it as a MenuKit feature, not a SystemLink blocker.
What the upgrade actually costs (WBP_SL_BoundActionButton is gone, so this is from scratch):
UCommonBoundActionBaris aUDynamicEntryBoxBase. It asks CommonUI's action router what is bound right
now and spawns one entry per binding registered with bDisplayInActionBar = true, rebuilding on binding changes and on input-device changes.
- Its
ActionButtonClasscarriesmeta = (MustImplement = CommonBoundActionButtonInterface), so the entry
must derive from UCommonBoundActionButton — a plain UserWidget like WBP_SL_CommandPrompt will not compile into that slot. The entry needs a Text_ActionName (CommonTextBlock) BindWidget; the bar calls SetRepresentedAction() on it to fill in glyph and label.
- Useful defaults:
bDisplayOwningPlayerActionsOnly(true — split-screen hygiene) and
bIgnoreDuplicateActions.
The rule of thumb for when it's worth it: hardcode while a screen's action set is fixed forever (a two-button confirm dialog), go dynamic when actions appear and disappear at runtime (a Settings page whose Reset prompt only exists while something is dirty).
Prior revision of this doc described that dynamic-bar wiring in full; it's preserved in git history if the
MenuKit upgrade needs it.