Reference · Updated 2552.10.23.15.54

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
HelperTextthe screen's heading for the focused row (WBP_SL_Profiles → Set Helper Text)

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: "Player 1".."Player 4" for any claimed profile, the viewer's own included ("In use" if the holder is not a local player), 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.

HelperText is composed the same way and says what Ⓐ on the row would do: "Edit “Name”" for your own, "Set “Name” as your profile" for a free one, and "“Name” is Player 2's profile" when another player holds it (the press would be refused, so it must not offer to set it). Covered by SystemLink.Settings.ProfileRegistry.ListItems.

#5. Four 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.

  1. Set every per-item state both ways. A row that only calls Disable Button with Reason when
  2. bCanSelect is false keeps that disabled state when it is reused for a free profile: switch profiles and the rows you just left stay greyed out. The True branch calls Set Is Enabled (true), the function, not the bIsEnabled variable setter, which skips CommonUI's override. Fixed 2026-10-23.

#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.