Files
nixos-dotfiles/modules/desktop/quickshell/qml/Widgets/ControlPanel/components/system/TrayMenu.qml
2025-07-13 22:17:13 +08:00

231 lines
6.7 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "root:/Data" as Data
// System tray context menu
Rectangle {
id: root
width: parent.width
height: visible ? calculatedHeight : 0
visible: false
enabled: visible
clip: true
color: Data.ThemeManager.bgColor
border.color: Data.ThemeManager.accentColor
border.width: 2
radius: 20
required property var menu
required property var systemTrayY
required property var systemTrayHeight
property bool containsMouse: trayMenuMouseArea.containsMouse
property bool menuJustOpened: false
property point triggerPoint: Qt.point(0, 0)
property Item originalParent
property int totalCount: opener.children ? opener.children.values.length : 0
signal hideRequested
MouseArea {
id: trayMenuMouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
}
onVisibleChanged: {
if (visible) {
menuJustOpened = true;
Qt.callLater(function () {
menuJustOpened = false;
});
}
}
function toggle() {
visible = !visible;
if (visible) {
menuJustOpened = true;
Qt.callLater(function () {
menuJustOpened = false;
});
}
}
function show(point, parentItem) {
visible = true;
menuJustOpened = true;
Qt.callLater(function () {
menuJustOpened = false;
});
}
function hide() {
visible = false;
menuJustOpened = false;
// Small delay before notifying hide to prevent control panel flicker
Qt.callLater(function () {
hideRequested();
});
}
// Smart positioning to avoid screen edges
y: {
var preferredY = systemTrayY + systemTrayHeight + 10;
var availableSpace = parent.height - preferredY - 20;
if (calculatedHeight > availableSpace) {
return systemTrayY - height - 10;
}
return preferredY;
}
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
// Dynamic height calculation based on menu item count and types
property int calculatedHeight: {
if (totalCount === 0)
return 40;
var separatorCount = 0;
var regularItemCount = 0;
if (opener.children && opener.children.values) {
for (var i = 0; i < opener.children.values.length; i++) {
if (opener.children.values[i].isSeparator) {
separatorCount++;
} else {
regularItemCount++;
}
}
}
// Calculate total height: separators + grid rows + margins
var separatorHeight = separatorCount * 12;
var regularItemRows = Math.ceil(regularItemCount / 2);
var regularItemHeight = regularItemRows * 32;
return Math.max(80, 35 + separatorHeight + regularItemHeight + 40);
}
// Menu opener handles the native menu integration
QsMenuOpener {
id: opener
menu: root.menu
}
// Grid layout for menu items (2 columns)
GridView {
id: gridView
anchors.fill: parent
anchors.margins: 20
cellWidth: width / 2
cellHeight: 32
interactive: false
flow: GridView.FlowLeftToRight
layoutDirection: Qt.LeftToRight
model: ScriptModel {
values: opener.children ? [...opener.children.values] : []
}
delegate: Item {
id: entry
required property var modelData
required property int index
width: gridView.cellWidth - 4
height: modelData.isSeparator ? 12 : 30
// Separator line
Rectangle {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
anchors.topMargin: 4
anchors.bottomMargin: 4
visible: modelData.isSeparator
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width * 0.8
height: 1
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
opacity: 0.6
}
}
// Regular menu item
Rectangle {
id: itemBackground
anchors.fill: parent
anchors.margins: 2
visible: !modelData.isSeparator
color: "transparent"
radius: 6
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 6
Image {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
Text {
Layout.fillWidth: true
color: mouseArea.containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
text: modelData?.text ?? ""
font.pixelSize: 11
font.family: "monospace"
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && root.visible && !modelData.isSeparator
onEntered: itemBackground.color = Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
onExited: itemBackground.color = "transparent"
onClicked: {
modelData.triggered();
root.hide();
}
}
}
}
}
// Empty state indicator
Item {
anchors.centerIn: gridView
visible: gridView.count === 0
Label {
anchors.centerIn: parent
text: "No tray items available"
color: Qt.darker(Data.ThemeManager.fgColor, 2)
font.pixelSize: 14
font.family: "monospace"
}
}
}