Files
z-bar-qt/Modules/TrayMenu.qml
T
2025-11-17 16:14:08 +01:00

324 lines
11 KiB
QML

pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.DBusMenu
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Hyprland
import QtQml
PanelWindow {
id: root
signal menuActionTriggered()
signal finishedLoading()
required property QsMenuHandle trayMenu
required property point trayItemRect
required property PanelWindow bar
property var menuStack: []
property real scaleValue: 0
property alias focusGrab: grab.active
property int entryHeight: 30
property int biggestWidth: 0
property int menuItemCount: menuOpener.children.values.length
QsMenuOpener {
id: menuOpener
menu: root.trayMenu
}
// onTrayMenuChanged: {
// listLayout.forceLayout();
// }
visible: true
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region { id: mask; item: menuRect }
function goBack() {
if ( root.menuStack.length > 0 ) {
menuChangeAnimation.start();
root.biggestWidth = 0;
root.trayMenu = root.menuStack.pop();
listLayout.positionViewAtBeginning();
backEntry.visible = false;
}
}
function updateMask() {
root.mask.changed();
}
onVisibleChanged: {
if ( visible ) {
scaleValue = 0;
scaleAnimation.start();
} else {
root.menuStack.pop();
backEntry.visible = false;
}
}
NumberAnimation {
id: scaleAnimation
target: root
property: "scaleValue"
from: 0
to: 1
duration: 150
easing.type: Easing.OutCubic
onStopped: {
root.updateMask();
}
}
HyprlandFocusGrab {
id: grab
windows: [ root ]
active: false
onCleared: {
root.visible = false;
}
}
SequentialAnimation {
id: menuChangeAnimation
ParallelAnimation {
NumberAnimation {
duration: MaterialEasing.standardTime / 2
easing.bezierCurve: MaterialEasing.expressiveEffects
from: 0
property: "x"
target: translateAnim
to: -listLayout.width / 2
}
NumberAnimation {
duration: MaterialEasing.standardTime / 2
easing.bezierCurve: MaterialEasing.standard
from: 1
property: "opacity"
target: columnLayout
to: 0
}
}
PropertyAction {
property: "menu"
target: columnLayout
}
ParallelAnimation {
NumberAnimation {
duration: MaterialEasing.standardTime / 2
easing.bezierCurve: MaterialEasing.standard
from: 0
property: "opacity"
target: columnLayout
to: 1
}
NumberAnimation {
duration: MaterialEasing.standardTime / 2
easing.bezierCurve: MaterialEasing.expressiveEffects
from: listLayout.width / 2
property: "x"
target: translateAnim
to: 0
}
}
}
onMenuActionTriggered: {
if ( root.menuStack.length > 0 ) {
backEntry.visible = true;
}
}
Rectangle {
id: menuRect
x: Math.round( root.trayItemRect.x - ( menuRect.implicitWidth / 2 ) + 11 )
y: Math.round( root.trayItemRect.y - 5 )
implicitWidth: listLayout.contentWidth + 10
implicitHeight: listLayout.contentHeight + ( root.menuStack.length > 0 ? root.entryHeight + 10 : 10 )
color: "#80151515"
radius: 8
border.color: "#40FFFFFF"
clip: true
transform: [
Scale {
origin.x: menuRect.width / 2
origin.y: 0
xScale: root.scaleValue
yScale: root.scaleValue
}
]
Behavior on implicitWidth {
NumberAnimation {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
}
ColumnLayout {
id: columnLayout
anchors.fill: parent
anchors.margins: 5
spacing: 0
transform: [
Translate {
id: translateAnim
x: 0
y: 0
}
]
ListView {
id: listLayout
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
spacing: 2
contentWidth: root.biggestWidth
contentHeight: contentItem.childrenRect.height
model: menuOpener.children
delegate: Rectangle {
id: menuItem
required property int index
required property QsMenuEntry modelData
property var child: QsMenuOpener {
menu: menuItem.modelData
}
property bool containsMouseAndEnabled: mouseArea.containsMouse && menuItem.modelData.enabled
property bool containsMouseAndNotEnabled: mouseArea.containsMouse && !menuItem.modelData.enabled
width: widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20
anchors.left: parent.left
anchors.right: parent.right
height: menuItem.modelData.isSeparator ? 1 : root.entryHeight
color: menuItem.modelData.isSeparator ? "#20FFFFFF" : containsMouseAndEnabled ? "#15FFFFFF" : containsMouseAndNotEnabled ? "#08FFFFFF" : "transparent"
radius: 4
visible: true
Component.onCompleted: {
var biggestWidth = root.biggestWidth;
var currentWidth = widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20;
if ( currentWidth > biggestWidth ) {
root.biggestWidth = currentWidth;
}
if ( menuItem.index === menuOpener.children.values.length - 1 )
root.finishedLoading();
}
TextMetrics {
id: widthMetrics
text: menuItem.modelData.text
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: true
acceptedButtons: Qt.LeftButton
onClicked: {
if ( !menuItem.modelData.hasChildren ) {
if ( menuItem.modelData.enabled ) {
menuItem.modelData.triggered();
root.visible = false;
}
} else {
root.menuStack.push(root.trayMenu);
menuChangeAnimation.start();
root.biggestWidth = 0;
root.trayMenu = menuItem.modelData;
listLayout.positionViewAtBeginning();
root.menuActionTriggered();
}
}
}
RowLayout {
anchors.fill: parent
Text {
id: menuText
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: 10
text: menuItem.modelData.text
color: menuItem.modelData.enabled ? "white" : "gray"
}
Image {
id: iconImage
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.rightMargin: 10
Layout.maximumWidth: 20
Layout.maximumHeight: 20
source: menuItem.modelData.icon
sourceSize.width: width
sourceSize.height: height
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: ColorOverlay {
color: menuItem.modelData.enabled ? "white" : "gray"
}
}
Text {
id: textArrow
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.rightMargin: 10
Layout.bottomMargin: 5
Layout.maximumWidth: 20
Layout.maximumHeight: 20
text: ""
color: menuItem.modelData.enabled ? "white" : "gray"
visible: menuItem.modelData.hasChildren ?? false
}
}
}
}
Rectangle {
id: backEntry
visible: false
Layout.fillWidth: true
Layout.preferredHeight: root.entryHeight
color: mouseAreaBack.containsMouse ? "#15FFFFFF" : "transparent"
radius: 4
MouseArea {
id: mouseAreaBack
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.goBack();
}
}
Text {
anchors.fill: parent
anchors.leftMargin: 10
verticalAlignment: Text.AlignVCenter
text: "Back "
color: "white"
}
}
}
}
}