quickshell: open tray menus at full size, not empty-then-grown

The DBus menu populates asynchronously and Steam's is slow, so the
panel was sized from an empty body: menuItems.width is fixed, making
the width final immediately while fullHeight was only padding. The
panel shot out sideways, then lurched taller when the items arrived —
two motions instead of one.

contextMenu now loads the menu first and opens once it has items,
falling back to a 500ms timeout so a menu that never populates cannot
strand the click. Hovering a tray icon also prefetches its menu, which
covers the wait in practice since you cannot click without hovering.

Clicking the open menu's own icon still closes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-07-28 14:29:47 +01:00
parent 0a6a23aa8a
commit 60e39613d6

View file

@ -1849,23 +1849,24 @@ in
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
onEntered: { onEntered: {
if (bar.activeDropdown) { if (!modelData.hasMenu) return;
if (modelData.hasMenu && !(bar.activeDropdown === contextMenu && contextMenu.trayItem === modelData)) { // 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);
if (bar.activeDropdown === contextMenu) { if (bar.activeDropdown === contextMenu) {
// Same dropdown, just switch content // Same dropdown, just switch content
let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2);
contextMenu.dropdownY = pos.y; contextMenu.dropdownY = pos.y;
contextMenu.trayItem = modelData; contextMenu.trayItem = modelData;
menuOpener.menu = modelData.menu; menuOpener.menu = modelData.menu;
} else { } else {
bar.toggleDropdown(contextMenu, function() { contextMenu.openFor(modelData, pos.y);
let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2);
contextMenu.dropdownY = pos.y;
contextMenu.trayItem = modelData;
menuOpener.menu = modelData.menu;
});
}
}
} }
} }
} }
@ -1873,15 +1874,14 @@ in
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (event) => { onClicked: (event) => {
if (modelData.hasMenu) { if (!modelData.hasMenu) {
bar.toggleDropdown(contextMenu, function() {
let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2);
contextMenu.dropdownY = pos.y;
contextMenu.trayItem = modelData;
menuOpener.menu = modelData.menu;
});
} else {
modelData.activate(); modelData.activate();
} else if (bar.activeDropdown === contextMenu && contextMenu.trayItem === modelData) {
// Clicking the open menu's own icon still closes it
contextMenu.animateClose();
} else {
let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2);
contextMenu.openFor(modelData, pos.y);
} }
} }
} }
@ -2103,13 +2103,64 @@ in
fullHeight: menuItems.height + 2 * Theme.panelGap fullHeight: menuItems.height + 2 * Theme.panelGap
onVisibleChanged: { onVisibleChanged: {
if (!visible) menuOpener.menu = null; if (!visible) {
menuOpener.menu = null;
pendingItem = null;
_menuWait.stop();
}
} }
QsMenuOpener { QsMenuOpener {
id: menuOpener id: menuOpener
} }
// The DBus menu populates asynchronously and Steam's is
// slow. Opening on click sized the panel from an EMPTY
// body: menuItems.width is fixed, so the width was already
// final while fullHeight was just padding the panel shot
// out sideways, then lurched taller when the items landed.
// Two motions instead of one. Load first, open once there
// is something to show.
property var pendingItem: null
readonly property bool menuReady:
menuOpener.children && menuOpener.children.values.length > 0
function openFor(item, y) {
dropdownY = y;
trayItem = item;
menuOpener.menu = item.menu;
if (menuReady) {
pendingItem = null;
bar.toggleDropdown(contextMenu, null);
} else {
pendingItem = item;
_menuWait.restart();
}
}
function openPending() {
if (!pendingItem) return;
pendingItem = null;
_menuWait.stop();
if (bar.activeDropdown !== contextMenu)
bar.toggleDropdown(contextMenu, null);
}
// Never strand a click if the menu never populates.
Timer {
id: _menuWait
interval: 500
onTriggered: contextMenu.openPending()
}
Connections {
target: menuOpener.children
function onValuesChanged() {
if (contextMenu.pendingItem && contextMenu.menuReady)
contextMenu.openPending();
}
}
Card { Card {
id: menuItems id: menuItems
anchors.centerIn: parent anchors.centerIn: parent