nova-shell/modules/TrayMenu.qml
2026-04-12 11:35:03 +02:00

235 lines
8.1 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 closed()
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.closed()
}
// Menu panel
Item {
id: panel
x: Math.max(0, Math.min(
Math.round(menuWindow.anchorX - menuStack.implicitWidth / 2),
menuWindow.width - menuStack.implicitWidth
))
y: M.Theme.barHeight
implicitWidth: menuStack.implicitWidth
implicitHeight: menuStack.implicitHeight
width: implicitWidth
height: implicitHeight
// 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
radius: M.Theme.radius
border.color: M.Theme.base02
border.width: 1
}
StackView {
id: menuStack
implicitWidth: currentItem ? currentItem.implicitWidth : 0
implicitHeight: 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
implicitWidth: 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.closed();
}
}
}
}
}
}
}
}