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

FieldRow does
ProfileNamethe label
bCanSelectSet Is Enabled on the button
bIsClaimedByViewermark as yours — a tick or accent, not greyed out
ClaimedByPlayerIndexwhen claimed by someone else: "Player {Index + 1} is using this" (-1 = free)
bIsReservedhide rename/delete affordances — this is Default
StatusTextset 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

  1. On List Item Object Set also fires with NULL when the row is released back to the pool. Guard the
  2. cast or you get an Accessed-None storm while scrolling.

  1. Bind OnClicked in Construct, not in On List Item Object Set. The row object survives recycling,
  2. so binding per item-set stacks duplicate bindings and one click fires N times.

  1. Never cache the item. UListView pools rows and reassigns them, so a variable set in
  2. 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 → None if the inner buttons carry the interaction (recommended), Single if 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.