234 lines
7.9 KiB
QML
234 lines
7.9 KiB
QML
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
|
||
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()
|
||
}
|
||
|
||
// Menu panel
|
||
Item {
|
||
id: panel
|
||
|
||
x: Math.max(0, Math.min(
|
||
Math.round(menuWindow.anchorX - menuStack.width / 2),
|
||
menuWindow.width - menuStack.width
|
||
))
|
||
y: 0
|
||
|
||
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: Math.max(M.Theme.barOpacity, 0.85)
|
||
topLeftRadius: 0
|
||
topRightRadius: 0
|
||
bottomLeftRadius: M.Theme.radius
|
||
bottomRightRadius: M.Theme.radius
|
||
}
|
||
|
||
StackView {
|
||
id: menuStack
|
||
|
||
width: currentItem ? currentItem.width : 0
|
||
height: currentItem ? currentItem.height : 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
|
||
width: page.width
|
||
height: 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
|
||
|
||
width: page.width
|
||
height: 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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|