Reference · Updated 2552.09.10.16.15
Building the Profile Row Widget
The row inside the profile picker's UCommonListView. Companion: PlayerProfiles.md §8, which covers what a profile is and what the registry hands you.
The row inside the profile picker's UCommonListView. Companion: PlayerProfiles.md §8, which covers what a profile is and what the registry hands you.
Asset: WBP_SL_ProfileRow in Content/SystemLink/UI/Menus/.
#1. Parent class
Parent to USLListEntryWidget. It is UCommonUserWidget + IUserObjectListEntry, which is what lets a list view assign it an item and recycle it. Do not use a plain UUserWidget — the list cannot drive it.
#2. Widget tree
The row itself is not focusable (USLListEntryWidget is not a button). Put an SL button inside it and let that be the interactive element:
WBP_SL_ProfileRow (USLListEntryWidget)
└── Overlay
└── WBP_SL_ButtonMainMenu <- the focusable, clickable part
├── Text_ProfileName
└── Text_Status <- bind to StatusText (see §4)
This reuses everything already working on buttons: the focus coordinator that keeps exactly one control lit, the Ⓐ glyph, and the navigation sound. Gamepad navigation then walks the rows for free, because the buttons are the focusable things.
⚠ The alternative — making the row itself focusable and using the list's own selection — means re-authoring the highlight and losing the glyph and sound. Not worth it.
#3. Graph
Two events, and they do different jobs.
Event Construct
└── Bind Event to OnClicked (the button) <- ONCE. See trap 2.
On List Item Object Set (inherited)
└── Cast to USLProfileListItem (guard the failure pin — see trap 1)
└── populate the fields below
<OnClicked handler>
└── Get Entry Item -> Cast to USLProfileListItem <- read HERE, not from a cached variable
└── Load Profile / open rename / whatever the row does
#4. Field wiring
| Field | Row does |
|---|---|
ProfileName | the label |
bCanSelect | Set Is Enabled on the button |
bIsClaimedByViewer | mark as yours — a tick or accent, not greyed out |
ClaimedByPlayerIndex | when claimed by someone else: "Player {Index + 1} is using this" (-1 = free) |
bIsReserved | hide rename/delete affordances — this is Default |
StatusText | set Text_Status directly — already worded and localised |
bIsClaimed is rarely read directly; bCanSelect and bIsClaimedByViewer already encode the useful cases.
There is no "status" to look up — StatusText is composed for you by BuildProfileListItems: "Playing" for the viewer's own profile, "Player 2 is using this" when another local player holds it, and empty when free. Bind it straight to a text block; let the empty value collapse it rather than branching in the row. It is built in C++ so the wording cannot drift between rows and there is one place to localise — the same reason GetSessionResultText and GetProfileNameResultText live there.
#5. Three traps
On List Item Object Setalso fires with NULL when the row is released back to the pool. Guard the
cast or you get an Accessed-None storm while scrolling.
- Bind
OnClickedinConstruct, not inOn List Item Object Set. The row object survives recycling,
so binding per item-set stacks duplicate bindings and one click fires N times.
- Never cache the item.
UListViewpools rows and reassigns them, so a variable set in
On List Item Object Set goes stale the moment the row is reused — the click would act on whichever profile the row used to show. Always read Get Entry Item at click time. Same class of bug as BUG-027.
#6. List view settings
On the UCommonListView in the picker:
- Entry Widget Class →
WBP_SL_ProfileRow
- Selection Mode →
Noneif the inner buttons carry the interaction (recommended),Singleif you also
want list-level selection
- Populate with
Build Profile List Items(Viewer =Get Owning Player) →Set List Items
- Rebuild the whole array on
OnProfileListChanged. The items are a snapshot; holding rows across a
refresh leaves the list describing profiles that were renamed or deleted.
#Done when
Rows list every profile, a controller can walk them, the claimed one shows who has it and cannot be selected, and your own reads as yours. Scroll a list longer than the viewport and click a row — if it acts on the wrong profile, that is trap 3.