extract shared PopupPanel for animated tray/power menus

This commit is contained in:
Damocles 2026-04-12 16:24:33 +02:00
parent 77ce83462d
commit b8ec39f2c9
6 changed files with 249 additions and 302 deletions

View file

@ -1,204 +1,137 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "." as M
// Per-icon context menu popup window.
// Covers the screen on the Overlay layer so clicking anywhere outside
// the menu panel dismisses it. Created on demand by Tray.qml delegates.
PanelWindow {
M.PopupPanel {
id: menuWindow
required property var handle
required property var screen
required property real anchorX
signal menuClosed
// Current menu level swapped when entering/leaving submenus
property var _currentHandle: handle
property var _handleStack: []
visible: true
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
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()
QsMenuOpener {
id: opener
menu: menuWindow._currentHandle
}
// Menu panel
// Back button (submenus only)
Item {
id: panel
x: Math.max(0, Math.min(Math.round(menuWindow.anchorX - menuCol.width / 2), menuWindow.width - menuCol.width))
y: 0
width: menuCol.width
height: menuCol.height
// Eat clicks inside the panel
MouseArea {
anchors.fill: parent
}
visible: menuWindow._handleStack.length > 0
width: menuWindow.panelWidth
height: visible ? 28 : 0
Rectangle {
anchors.fill: parent
color: M.Theme.base01
opacity: Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
anchors.leftMargin: 4
anchors.rightMargin: 4
color: backArea.containsMouse ? M.Theme.base02 : "transparent"
radius: M.Theme.radius
}
Column {
id: menuCol
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 12
text: "\u2039 Back"
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
}
width: 220
topPadding: 4
bottomPadding: 4
spacing: 2
MouseArea {
id: backArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
const stack = menuWindow._handleStack.slice();
menuWindow._currentHandle = stack.pop();
menuWindow._handleStack = stack;
}
}
}
QsMenuOpener {
id: opener
menu: menuWindow._currentHandle
Repeater {
model: opener.children
delegate: Item {
id: entryItem
required property QsMenuEntry modelData
width: menuWindow.panelWidth
height: modelData.isSeparator ? 9 : 28
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
}
// Back button (submenus only)
Item {
visible: menuWindow._handleStack.length > 0
width: menuCol.width
height: visible ? 28 : 0
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: "\u2039 Back"
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
}
MouseArea {
id: backArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
const stack = menuWindow._handleStack.slice();
menuWindow._currentHandle = stack.pop();
menuWindow._handleStack = stack;
}
}
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
}
Repeater {
model: opener.children
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
}
delegate: Item {
id: entryItem
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
}
required property QsMenuEntry modelData
Text {
id: entryChevron
visible: !entryItem.modelData.isSeparator && entryItem.modelData.hasChildren
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 12
text: "\u203A"
color: entryItem.modelData.enabled ? M.Theme.base05 : M.Theme.base03
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
}
width: menuCol.width
height: modelData.isSeparator ? 9 : 28
// Separator
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: "\u203A"
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) {
menuWindow._handleStack = menuWindow._handleStack.concat([menuWindow._currentHandle]);
menuWindow._currentHandle = entryItem.modelData;
} else {
entryItem.modelData.triggered();
menuWindow.menuClosed();
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
enabled: !entryItem.modelData.isSeparator && entryItem.modelData.enabled
onClicked: {
if (entryItem.modelData.hasChildren) {
menuWindow._handleStack = menuWindow._handleStack.concat([menuWindow._currentHandle]);
menuWindow._currentHandle = entryItem.modelData;
} else {
entryItem.modelData.triggered();
menuWindow.dismiss();
}
}
}