more flyout stuff, tray menus

This commit is contained in:
Damocles 2026-04-12 11:35:03 +02:00
parent 3a548930e2
commit 59458cade3
4 changed files with 401 additions and 1 deletions

235
modules/TrayMenu.qml Normal file
View file

@ -0,0 +1,235 @@
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();
}
}
}
}
}
}
}
}