fix(quest): QT5 — the rows were unclickable, and nothing showed which was selected

Two defects in the panel as committed, both found by checking the code against
the authored data rather than by a test.

The authored row template is a Type-3 generic container, which resolves through
DatWidgetFactory's fallback arm to UiDatElement — whose constructor sets
ClickThrough = true ("generic decoration; behavioral widgets opt back in").
Binding OnClick without clearing that compiles, reads correctly, and produces a
list in which nothing can be selected: every click sails past the row. The
skills page had already met this and left the precedent; I did not follow it.

And there was no selection highlight at all, so even once clicking worked the
player could not tell which row the detail pane was describing. UiTemplateListBox
has no selection mechanism of its own, so the page opts in directly and
re-PAINTS the highlight after a rebuild — a rebuild discards the row objects, so
remembering the selection is not enough to keep it visible.

The tests for both initially passed while the bugs were live, because the
fixture's row root was a UiPanel and its text started white. A UiPanel is not
click-through, so the first test was vacuous; white-on-white made the highlight
unobservable. The fixture now builds the same UiDatElement production does and
authors a non-white colour. This is the third time this campaign a fixture that
did not match the real widget hid a real defect.

Live mount confirmed against the installed dats in this session's client run:
"[UI] retail journal panel from LayoutDesc importer (0x2100006E slot
0x10000559)" with no bind failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-21 15:17:14 +02:00
parent ec6eeb120d
commit 56beeb720d
2 changed files with 175 additions and 3 deletions

View file

@ -72,7 +72,16 @@ public sealed class JournalContractsPageController
private readonly UiText? _description;
private readonly UiText? _timedValue;
/// <summary>
/// Retail's own selection highlight for a listbox row. UiTemplateListBox
/// has no generic selection mechanism (its class doc), so the page opts in
/// directly — the same precedent
/// <see cref="CharacterCreationSkillsPage"/> set.
/// </summary>
private static readonly Vector4 SelectedNameColor = Vector4.One;
private readonly List<uint> _rowContractIds = [];
private readonly List<(uint ContractId, UiText? Name, Vector4 Unselected)> _rows = [];
private long _renderedRevision = -1;
private uint _selectedContractId;
@ -138,6 +147,7 @@ public sealed class JournalContractsPageController
_selectedContractId = 0u;
_rowContractIds.Clear();
_rows.Clear();
_list?.FlushPreservingScroll();
foreach (ContractTracker tracker in contracts)
@ -152,7 +162,8 @@ public sealed class JournalContractsPageController
ContractEntry entry = catalog.Lookup(tracker.ContractId);
if (UiElement.FindDescendant(row, RowNameId) is UiText name)
var name = UiElement.FindDescendant(row, RowNameId) as UiText;
if (name is not null)
SetText(name, entry.ContractName);
if (UiElement.FindDescendant(row, RowStatusId) is UiText status)
{
@ -161,11 +172,23 @@ public sealed class JournalContractsPageController
tracker.ReceivedAt, entry, now));
}
// Captured AFTER SetText, which never touches DefaultColor (it is
// read lazily inside the provider closure), so this is the row's
// own authored colour to restore on deselect.
_rows.Add((tracker.ContractId, name, name?.DefaultColor ?? Vector4.One));
uint captured = tracker.ContractId;
if (row is UiDatElement clickable)
{
// A generic Type-3 container is click-THROUGH by default, so
// without this the handler below never fires and the list looks
// dead. UiDatElement carries the opt-in seam for exactly this.
clickable.ClickThrough = false;
clickable.OnClick = () => Select(captured);
}
}
ApplySelectionHighlight();
RefreshDetail();
}
@ -173,9 +196,26 @@ public sealed class JournalContractsPageController
public void Select(uint contractId)
{
_selectedContractId = contractId;
ApplySelectionHighlight();
RefreshDetail();
}
/// <summary>
/// Re-painted rather than merely remembered: a rebuild discards the old row
/// objects, so a preserved selection has to be applied to the new ones.
/// </summary>
private void ApplySelectionHighlight()
{
foreach ((uint contractId, UiText? name, Vector4 unselected) in _rows)
{
if (name is not null)
{
name.DefaultColor =
contractId == _selectedContractId ? SelectedNameColor : unselected;
}
}
}
private void RefreshDetail()
{
ContractCatalog catalog = _bindings.Catalog();