From 5c71cc3a1fb0eb80e4e02c3535747819e72b7bac Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 28 Jul 2026 15:24:55 +0100 Subject: [PATCH] quickshell: per-icon tray menu cache, kill the switch flash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- settings/quickshell.nix | 60 ++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 646e7d8..1f88d66 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -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 + // tray→tray 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