One shared QsMenuOpener meant tray-to-tray switching reassigned the
handle and reloaded over DBus — the model went briefly empty, and no
sizing gate downstream could hide that. Each tray icon now owns its
opener, assigned when the icon appears, so every menu (Steam's slow
one included) is fetched once at startup and held. Switching swaps one
populated model for another in a single binding pass; contextMenu just
repoints activeOpener. Hover prefetch deleted — nothing left to
prefetch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every previous attempt read the panel height off menuItems, which by
definition lags the data: the Repeater must build delegates and the
Column re-measure before the Card reports its real size. Reading it
during that gap is the flash, and no amount of gating on the model
closes it, because the gap is between the model and the layout.
fullHeight now sums the rows from menuOpener.children directly. That is
exact — cardSpacing is 0, so the sum plus 2*cardPad is precisely what
the Card lays out — and it lands in the same binding pass as the data,
so size and content never disagree.
Row metrics moved to menuRowHeight/menuSepHeight, shared by the sum and
the delegate so they cannot drift. The previous-height hold stays for
the DBus fetch itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gating the held height on the model alone was not enough. The model
populating and the layout catching up are different frames: the
Repeater still has to build delegates and the Column re-measure, so
menuLoaded went true while the Card was still empty-sized and the panel
collapsed for a frame before regrowing.
Only tray-to-tray showed it, because that is the one path that
reassigns the menu handle — coming from a non-tray widget leaves the
handle unchanged, so nothing reloads.
menuReady now also requires the height to have grown past an empty Card
(2*cardPad = 16; the smallest real row is 28, so it cannot misfire).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Switching to a tray menu whose items were not currently loaded
collapsed fullHeight at the instant of the switch, so the chrome
morphed DOWN to a stub and replayed the vertical grow when the items
landed, rather than moving across from the previous panel.
An empty Card is not height 0 but 2*cardPad, so loaded-ness has to come
from the model: menuLoaded tracks menuOpener.children. While a newly
assigned menu is loading, fullHeight holds the last loaded height, so
the morph stays continuous and the correction on arrival is small —
nil when it is the same menu as last time.
ui.nix radius knob at 20.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>