import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Services.SystemTray import Quickshell.Widgets import "../../config" as Cfg Item { id: root property real panelRadius: Cfg.Config.radius / 2 property int borderWidth: Cfg.Config.panelBorderWidth property var targetScreen: null property string barSide: "right" property bool isTop: true readonly property var t: Cfg.Config.theme property bool expanded: false property bool manuallyExpanded: false readonly property bool effectiveExpanded: expanded || trayMenuPopup.popupOpen implicitWidth: bgRect.width implicitHeight: bgRect.height readonly property color tmBg: t.trayMenuBg readonly property color tmBorder: t.trayMenuBorder readonly property color tmText: t.trayMenuText readonly property color tmDim: t.trayMenuDim readonly property color tmHover: t.trayMenuHover readonly property color tmCheck: t.trayMenuCheck readonly property color tmSeparator: t.trayMenuSeparator Rectangle { id: bgRect anchors.verticalCenter: parent.verticalCenter height: parent.height width: mainRow.width radius: root.panelRadius color: bgHover.hovered ? root.t.trayBgHover : root.t.trayBg border.color: bgHover.hovered ? root.t.trayBorderHover : root.t.trayBorder border.width: root.borderWidth Behavior on color { ColorAnimation { duration: 150 } } Behavior on border.color { ColorAnimation { duration: 150 } } HoverHandler { id: bgHover onHoveredChanged: { if (!root.manuallyExpanded) { root.expanded = hovered; } } } Row { id: mainRow anchors.verticalCenter: parent.verticalCenter leftPadding: root.effectiveExpanded && root.barSide === "right" ? 8 : 0 rightPadding: root.effectiveExpanded && root.barSide === "left" ? 8 : 0 Behavior on leftPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } Behavior on rightPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } layoutDirection: root.barSide === "left" ? Qt.LeftToRight : Qt.RightToLeft spacing: root.effectiveExpanded ? 4 : 0 Behavior on spacing { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } Item { id: toggleArea width: Cfg.Config.trayToggleSize height: Cfg.Config.trayToggleSize z: 10 TapHandler { onTapped: { root.manuallyExpanded = !root.manuallyExpanded; root.expanded = root.manuallyExpanded; } } Text { anchors.centerIn: parent text: root.effectiveExpanded ? (root.barSide === "left" ? "󰅁" : "󰅂") : (root.barSide === "left" ? "󰅂" : "󰅁") color: root.t.trayIcon font.pixelSize: Cfg.Config.trayToggleSize * 0.55 } } Row { id: iconsRow spacing: 6 clip: true anchors.verticalCenter: parent.verticalCenter width: root.effectiveExpanded ? implicitWidth : 0 opacity: root.effectiveExpanded ? 1 : 0 Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } Behavior on opacity { NumberAnimation { duration: 150 } } Repeater { model: SystemTray.items delegate: MouseArea { id: iconDelegate required property SystemTrayItem modelData property alias item: iconDelegate.modelData implicitWidth: Cfg.Config.trayIconSize implicitHeight: Cfg.Config.trayIconSize width: Cfg.Config.trayIconSize height: Cfg.Config.trayIconSize acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton hoverEnabled: true onClicked: function(mouse) { if (mouse.button === Qt.LeftButton) { item.activate() } else if (mouse.button === Qt.MiddleButton) { item.secondaryActivate() } else if (mouse.button === Qt.RightButton) { if (item.hasMenu) { trayMenuPopup.open(item, iconDelegate) } } } onWheel: function(wheel) { wheel.accepted = true item.scroll(wheel.angleDelta.y / 120, false) } IconImage { anchors.fill: parent source: item.icon } } } } } } PanelWindow { id: trayMenuPopup screen: root.targetScreen visible: false color: "transparent" property bool popupOpen: false property bool _pendingOpen: false property var activeTrayItem: null property var activeDelegate: null function open(item, delegate) { if (popupOpen) close() hideTimer.stop() _pendingOpen = false activeTrayItem = item activeDelegate = delegate rootMenuOpener.menu = item.menu visible = true if (menuCol.height > 0) { popupOpen = true } else { _pendingOpen = true openFallbackTimer.restart() } } function close() { _pendingOpen = false openFallbackTimer.stop() popupOpen = false hideTimer.restart() } Connections { target: menuCol function onHeightChanged() { if (trayMenuPopup._pendingOpen && menuCol.height > 0) { trayMenuPopup._pendingOpen = false openFallbackTimer.stop() trayMenuPopup.popupOpen = true } } } Timer { id: openFallbackTimer interval: 400 onTriggered: { if (trayMenuPopup._pendingOpen) { trayMenuPopup._pendingOpen = false trayMenuPopup.popupOpen = true } } } Timer { id: hideTimer interval: Cfg.Config.popupAnimDuration + 70 onTriggered: { trayMenuPopup.visible = false rootMenuOpener.menu = null trayMenuPopup.activeTrayItem = null trayMenuPopup.activeDelegate = null } } anchors { top: true; bottom: true; left: true; right: true } WlrLayershell.layer: WlrLayer.Top WlrLayershell.namespace: "main-shell-tray-menu" WlrLayershell.exclusiveZone: -1 mask: popupOpen ? null : _trayMenuNoInput Region { id: _trayMenuNoInput } TapHandler { onTapped: trayMenuPopup.close() } QsMenuOpener { id: rootMenuOpener } Component { id: trayMenuItemComponent Item { id: menuItem required property var modelData readonly property bool isSep: modelData.isSeparator width: parent.width height: isSep ? 5 : 32 Rectangle { visible: isSep anchors { left: parent.left; right: parent.right verticalCenter: parent.verticalCenter } anchors.leftMargin: 8; anchors.rightMargin: 8 height: 1 color: root.tmSeparator } Rectangle { visible: !isSep anchors.fill: parent anchors.margins: 2 radius: 10 readonly property bool active: menuMouse.containsMouse || (submenuCard.visible && trayMenuPopup._submenuParentRef === menuItem) color: active ? root.tmHover : "transparent" Behavior on color { ColorAnimation { duration: 120 } } RowLayout { anchors.fill: parent anchors.leftMargin: 8; anchors.rightMargin: 8 spacing: 8 Item { Layout.preferredWidth: 16 Layout.preferredHeight: 16 visible: !isSep Text { anchors.centerIn: parent visible: modelData.buttonType === QsMenuButtonType.CheckBox && modelData.checkState === Qt.Checked text: "✓"; color: root.tmCheck font.pixelSize: 14 } Text { anchors.centerIn: parent visible: modelData.buttonType === QsMenuButtonType.RadioButton && modelData.checkState === Qt.Checked text: "●"; color: root.tmCheck font.pixelSize: 9 } Text { anchors.centerIn: parent visible: modelData.buttonType === QsMenuButtonType.RadioButton && modelData.checkState !== Qt.Checked text: "○"; color: root.tmDim font.pixelSize: 11 } } Image { visible: !isSep && modelData.icon !== "" source: modelData.icon width: 16; height: 16 sourceSize: Qt.size(16, 16) fillMode: Image.PreserveAspectFit } Text { text: modelData.text color: modelData.enabled ? root.tmText : root.tmDim font.pixelSize: 13 Layout.fillWidth: true elide: Text.ElideRight } Text { visible: !isSep && modelData.hasChildren text: "▶" color: root.tmDim font.pixelSize: 9 } } MouseArea { id: menuMouse anchors.fill: parent hoverEnabled: true enabled: !isSep && modelData.enabled onClicked: { if (modelData.hasChildren) { closeSubmenuTimer.stop() submenuDelayTimer.itemRef = menuItem submenuDelayTimer.restart() } else { modelData.triggered() trayMenuPopup.close() } } onEntered: { if (modelData.hasChildren) { closeSubmenuTimer.stop() submenuDelayTimer.itemRef = menuItem submenuDelayTimer.restart() } else { closeSubmenuTimer.stop() submenuCard.visible = false } } onExited: { if (modelData.hasChildren) closeSubmenuTimer.restart() } } } } } Item { id: menuCardWrapper readonly property int cardW: 220 readonly property int pad: 10 x: { if (!trayMenuPopup.activeDelegate) return (parent.width - cardW) / 2 var gp = trayMenuPopup.activeDelegate.mapToGlobal(0, 0) var cx = gp.x + trayMenuPopup.activeDelegate.width / 2 return Math.max(pad, Math.min(cx - cardW / 2, parent.width - cardW - pad)) } width: cardW clip: true anchors.top: root.isTop ? parent.top : undefined anchors.bottom: root.isTop ? undefined : parent.bottom anchors.topMargin: root.isTop ? (Cfg.Config.barHeight + 8) : 0 anchors.bottomMargin: root.isTop ? 0 : (Cfg.Config.barHeight + 8) height: parent.height - Cfg.Config.barHeight - 8 Rectangle { id: menuCard width: parent.width height: menuCol.height + 12 anchors.top: root.isTop ? parent.top : undefined anchors.bottom: root.isTop ? undefined : parent.bottom transform: Translate { y: trayMenuPopup.popupOpen ? 0 : (root.isTop ? -menuCard.height : menuCard.height) Behavior on y { NumberAnimation { duration: Cfg.Config.popupAnimDuration easing.type: Easing.OutExpo } } } opacity: trayMenuPopup.popupOpen ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 200 } } color: root.tmBg radius: Cfg.Config.popupRadius border.color: root.tmBorder border.width: Cfg.Config.popupBorderWidth Column { id: menuCol anchors { left: parent.left; right: parent.right; top: parent.top } anchors.margins: 6 spacing: 0 Repeater { model: rootMenuOpener.children delegate: trayMenuItemComponent } } } } property var _submenuParentRef: null QsMenuOpener { id: subMenuOpener } Timer { id: submenuDelayTimer interval: 250 property var itemRef: null onTriggered: { if (!itemRef) return var entry = itemRef.modelData if (!entry || !entry.hasChildren) return subMenuOpener.menu = entry trayMenuPopup._submenuParentRef = itemRef submenuCard.visible = true } } Timer { id: closeSubmenuTimer interval: 300 onTriggered: { if (!submenuCard.containsMouse) { submenuCard.visible = false subMenuOpener.menu = null trayMenuPopup._submenuParentRef = null } } } Rectangle { id: submenuCard visible: false property bool containsMouse: false property int subW: Math.max(submenuCol.width, 180) x: { if (!trayMenuPopup._submenuParentRef) return 0 var pItem = trayMenuPopup._submenuParentRef var gp = pItem.mapToGlobal(0, 0) var lp = trayMenuPopup.contentItem.mapFromGlobal(gp.x, gp.y) var fromLeft = lp.x + pItem.width + 4 if (fromLeft + subW + 10 > trayMenuPopup.contentItem.width) return Math.max(2, lp.x - subW - 4) return fromLeft } y: { if (!trayMenuPopup._submenuParentRef) return 0 var pItem = trayMenuPopup._submenuParentRef var gp = pItem.mapToGlobal(0, 0) var lp = trayMenuPopup.contentItem.mapFromGlobal(gp.x, gp.y) return Math.max(2, lp.y - 2) } width: subW height: submenuCol.height + 12 color: root.tmBg radius: Cfg.Config.popupRadius border.color: root.tmBorder border.width: Cfg.Config.popupBorderWidth HoverHandler { onHoveredChanged: { submenuCard.containsMouse = hovered if (!hovered) closeSubmenuTimer.restart() } } Column { id: submenuCol anchors { left: parent.left; right: parent.right; top: parent.top } anchors.margins: 6 spacing: 0 Repeater { model: subMenuOpener.children delegate: trayMenuItemComponent } } } } }