quickshell: per-icon tray menu cache, kill the switch flash

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>
This commit is contained in:
rope 2026-07-28 15:24:55 +01:00
parent d866cae4f7
commit 5c71cc3a1f

View file

@ -1847,19 +1847,22 @@ in
color: Theme.base05
}
// The icon's own menu cache: assigned from the
// moment the icon exists, so the DBus fetch
// (slow for Steam) happens at startup, not
// mid-animation. contextMenu just points at
// this switching tray icons never reloads.
QsMenuOpener {
id: itemOpener
menu: modelData.menu
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onEntered: {
if (!modelData.hasMenu) return;
// Prefetch: you cannot click without
// hovering first, so start the DBus
// round-trip now and the menu is
// usually loaded before the click.
if (!bar.activeDropdown && menuOpener.menu !== modelData.menu) {
menuOpener.menu = modelData.menu;
}
if (!bar.activeDropdown) return;
if (bar.activeDropdown === contextMenu && contextMenu.trayItem === modelData) return;
let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2);
@ -1867,9 +1870,9 @@ in
// Same dropdown, just switch content
contextMenu.dropdownY = pos.y;
contextMenu.trayItem = modelData;
menuOpener.menu = modelData.menu;
contextMenu.activeOpener = itemOpener;
} else {
contextMenu.openFor(modelData, pos.y);
contextMenu.openFor(modelData, pos.y, itemOpener);
}
}
}
@ -1884,7 +1887,7 @@ in
contextMenu.animateClose();
} else {
let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2);
contextMenu.openFor(modelData, pos.y);
contextMenu.openFor(modelData, pos.y, itemOpener);
}
}
}
@ -2130,8 +2133,8 @@ in
// available the instant the data lands, so the height and
// its content update in the same binding pass.
readonly property real menuContentHeight: {
if (!menuOpener.children) return 0;
const items = menuOpener.children.values;
if (!menuModel) return 0;
const items = menuModel.values;
let h = 0;
for (let i = 0; i < items.length; i++)
h += items[i].isSeparator ? menuSepHeight : menuRowHeight;
@ -2148,26 +2151,23 @@ in
fullHeight: (menuContentHeight > 0 ? menuContentHeight : lastMenuHeight)
+ 2 * Theme.cardPad + 2 * Theme.panelGap
// Deliberately NOT cleared on close. Nulling the menu threw
// away the loaded items, and the only thing that refills
// them is a hover-enter which never fires if the cursor
// is still sitting on the icon you just clicked. Reopening
// then re-fetched over DBus and the panel grew to its empty
// height first, then jumped once the items landed. Keeping
// the handle assigned makes reopening the same item free;
// openFor() reassigns it when you pick a different one.
// Which tray item's opener feeds the menu. Every tray icon
// owns an always-assigned QsMenuOpener (see the tray
// Repeater), fetched when the icon appears so switching
// traytray swaps one POPULATED model for another in a
// single binding pass. The shared opener this replaces
// reloaded over DBus on every reassignment, and no sizing
// gate downstream could hide that the model itself went
// briefly empty (that gap was the flash).
property var activeOpener: null
readonly property var menuModel: activeOpener ? activeOpener.children : null
QsMenuOpener {
id: menuOpener
}
// Opens immediately never wait on the menu. Hovering the
// icon has already started the DBus fetch (see the tray
// MouseArea), so the size is normally settled by click.
function openFor(item, y) {
// Opens immediately the item's opener has been loaded
// since its icon appeared, so there is nothing to wait on.
function openFor(item, y, opener) {
dropdownY = y;
trayItem = item;
menuOpener.menu = item.menu;
activeOpener = opener;
bar.toggleDropdown(contextMenu, null);
}
@ -2178,7 +2178,7 @@ in
cardSpacing: 0
Repeater {
model: menuOpener.children
model: contextMenu.menuModel
Rectangle {
required property var modelData