pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Wayland import "." as M // Per-icon context menu popup window. // Covers the full screen on the Overlay layer so clicking anywhere outside // the menu panel dismisses it. Created on demand by Tray.qml delegates. PanelWindow { id: menuWindow required property QsMenuHandle handle required property var screen // Global x of the icon center (from mapToGlobal), used to position the panel required property real anchorX signal menuClosed() visible: true color: "transparent" WlrLayershell.layer: WlrLayer.Overlay // -1 = ignore exclusive zones so this covers the full screen including the bar WlrLayershell.exclusiveZone: -1 WlrLayershell.namespace: "nova-traymenu" anchors.top: true anchors.left: true anchors.right: true anchors.bottom: true // Click outside the menu panel → dismiss MouseArea { anchors.fill: parent onClicked: menuWindow.menuClosed() } // Menu panel Item { id: panel x: Math.max(0, Math.min( Math.round(menuWindow.anchorX - menuStack.width / 2), menuWindow.width - menuStack.width )) y: M.Theme.barHeight width: menuStack.width height: menuStack.height // Eat clicks inside the panel so they don't reach the dismiss area above MouseArea { anchors.fill: parent } Rectangle { anchors.fill: parent color: M.Theme.base01 opacity: M.Theme.barOpacity topLeftRadius: 0 topRightRadius: 0 bottomLeftRadius: M.Theme.radius bottomRightRadius: M.Theme.radius } StackView { id: menuStack width: currentItem ? currentItem.width : 0 height: currentItem ? currentItem.implicitHeight : 0 // Push the root page once the stack is ready Component.onCompleted: { menuStack.push(menuPageComp, { handle: menuWindow.handle, isRoot: true }, StackView.Immediate); } pushEnter: Transition {} pushExit: Transition {} popEnter: Transition {} popExit: Transition {} } } // Reusable menu page component — used for both root and submenus Component { id: menuPageComp Column { id: page required property QsMenuHandle handle property bool isRoot: false width: 220 topPadding: 4 bottomPadding: 4 spacing: 2 QsMenuOpener { id: opener menu: page.handle } // Back button (shown only in submenus) Item { visible: !page.isRoot implicitWidth: page.implicitWidth implicitHeight: 28 Rectangle { anchors.fill: parent anchors.leftMargin: 4 anchors.rightMargin: 4 color: backArea.containsMouse ? M.Theme.base02 : "transparent" radius: M.Theme.radius } Text { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 12 text: "‹ " + qsTr("Back") color: M.Theme.base05 font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily } MouseArea { id: backArea anchors.fill: parent hoverEnabled: true onClicked: menuStack.pop() } } Repeater { model: opener.children delegate: Item { id: entryItem required property QsMenuEntry modelData implicitWidth: page.implicitWidth implicitHeight: modelData.isSeparator ? 9 : 28 // Separator line Rectangle { visible: entryItem.modelData.isSeparator anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: 8 anchors.rightMargin: 8 height: 1 color: M.Theme.base03 } // Hover highlight Rectangle { visible: !entryItem.modelData.isSeparator anchors.fill: parent anchors.leftMargin: 4 anchors.rightMargin: 4 color: rowArea.containsMouse && entryItem.modelData.enabled ? M.Theme.base02 : "transparent" radius: M.Theme.radius } // Icon Image { id: entryIcon visible: !entryItem.modelData.isSeparator && entryItem.modelData.icon !== "" anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 12 width: M.Theme.fontSize height: M.Theme.fontSize source: entryItem.modelData.icon fillMode: Image.PreserveAspectFit } // Label Text { visible: !entryItem.modelData.isSeparator anchors.verticalCenter: parent.verticalCenter anchors.left: entryIcon.visible ? entryIcon.right : parent.left anchors.leftMargin: entryIcon.visible ? 6 : 12 anchors.right: entryChevron.visible ? entryChevron.left : parent.right anchors.rightMargin: entryChevron.visible ? 4 : 12 text: entryItem.modelData.text color: entryItem.modelData.enabled ? M.Theme.base05 : M.Theme.base03 font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily elide: Text.ElideRight } // Submenu chevron Text { id: entryChevron visible: !entryItem.modelData.isSeparator && entryItem.modelData.hasChildren anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 12 text: "›" color: entryItem.modelData.enabled ? M.Theme.base05 : M.Theme.base03 font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily } MouseArea { id: rowArea anchors.fill: parent hoverEnabled: true enabled: !entryItem.modelData.isSeparator && entryItem.modelData.enabled onClicked: { if (entryItem.modelData.hasChildren) { menuStack.push(menuPageComp, { handle: entryItem.modelData, isRoot: false }, StackView.Immediate); } else { entryItem.modelData.triggered(); menuWindow.menuClosed(); } } } } } } } }