pragma ComponentBehavior: Bound import QtQuick import Quickshell import "Util.js" as Util Flickable { id: root required property Theme theme required property Tray trayService readonly property var panelState: trayService ? Util.parseJson(trayService.stateJson(), {}) : ({}) readonly property var items: panelState.items || [] readonly property bool hasItems: items.length > 0 function primary(id, window, x, y) { if (trayService) trayService.primaryActivate(id, window, x, y); } function menu(id, window, x, y) { if (trayService) trayService.displayMenu(id, window, x, y); } function secondary(id) { if (trayService) trayService.secondaryActivate(id); } width: parent ? parent.width : 0 contentWidth: width contentHeight: trayCol.height clip: true interactive: contentHeight > height boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.VerticalFlick StableListModel { id: itemModel rows: root.items } Column { id: trayCol objectName: "trayItems" width: root.width spacing: 6 Repeater { model: itemModel delegate: PopupCard { id: itemCell required property var rowData readonly property bool needsAttention: rowData.status === "NeedsAttention" readonly property string title: rowData.title || rowData.tooltipTitle || rowData.id readonly property string tooltipTitle: rowData.tooltipTitle || "" readonly property string description: rowData.tooltipDescription || "" readonly property string statusText: String(rowData.status || "passive").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase() readonly property string categoryText: String(rowData.category || "application status").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase() readonly property string menuText: rowData.onlyMenu ? "menu only" : (rowData.hasMenu ? "menu available" : "") readonly property string metadata: [tooltipTitle !== title ? tooltipTitle : "", description, categoryText, statusText, menuText].filter(value => value.length > 0).join("\n") function openMenu(x, y) { if (nativeMenu.menu) { nativeMenu.anchor.rect = Qt.rect(Math.round(x), Math.round(y), 1, 1); nativeMenu.open(); } else { const point = itemCell.mapToItem(null, x, y); root.menu(rowData.id, itemCell.QsWindow.window, point.x, point.y); } } function activate() { if (rowData.onlyMenu) { openMenu(width / 2, height / 2); } else { const point = itemCell.mapToItem(null, width / 2, height / 2); root.primary(rowData.id, itemCell.QsWindow.window, point.x, point.y); } } theme: root.theme padding: 0 attention: needsAttention interactive: true accessibleName: `${rowData.onlyMenu ? "open menu for" : "activate"} ${title}` Accessible.description: metadata tooltip: `${title}\n${metadata}` width: root.width height: 60 onControlFocusedChanged: if (controlFocused) Util.revealIn(this, root) onClicked: activate() QsMenuAnchor { id: nativeMenu menu: root.trayService ? root.trayService.menuFor(itemCell.rowData.id) : null anchor.item: itemCell } Image { id: itemIcon anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter width: 26 height: 26 sourceSize.width: 26 sourceSize.height: 26 fillMode: Image.PreserveAspectFit source: itemCell.rowData.icon || "" } Column { anchors.left: itemIcon.right anchors.leftMargin: 12 anchors.right: actionRail.left anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter spacing: 3 NuguText { width: parent.width theme: root.theme text: itemCell.title elide: Text.ElideRight } NuguText { width: parent.width theme: root.theme variant: "meta" selected: itemCell.needsAttention text: itemCell.needsAttention ? `needs attention${itemCell.description ? " ยท " + itemCell.description : ""}` : (itemCell.description || itemCell.statusText) elide: Text.ElideRight } } Row { id: actionRail anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter spacing: 6 PopupButton { theme: root.theme width: 34 icon: "\uF0E7" accessibleName: `secondary action for ${itemCell.title}` tooltip: `secondary action for ${itemCell.title} (middle click)` onControlFocusedChanged: if (controlFocused) Util.revealIn(this, root) onClicked: root.secondary(itemCell.rowData.id) } PopupButton { theme: root.theme width: 34 icon: "\uF142" enabled: itemCell.rowData.hasMenu accessibleName: `menu for ${itemCell.title}` tooltip: enabled ? `menu for ${itemCell.title} (right click)` : "no menu available" onControlFocusedChanged: if (controlFocused) Util.revealIn(this, root) onClicked: itemCell.openMenu(itemCell.width - 12, itemCell.height) } } // Primary pointer and keyboard activation share PopupCard. // Keep only the documented additional mouse buttons here. MouseArea { anchors.fill: parent acceptedButtons: Qt.MiddleButton | Qt.RightButton onClicked: event => { if (event.button === Qt.RightButton) itemCell.openMenu(event.x, event.y); else root.secondary(itemCell.rowData.id); } } } } Item { objectName: "trayEmptyState" visible: !root.hasItems width: parent.width height: 56 Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter font.family: "Symbols Nerd Font Mono" font.pixelSize: 26 color: root.theme.ui.minor text: "\uF01C" Accessible.ignored: true } Column { anchors.left: parent.left anchors.leftMargin: 40 anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 3 NuguText { theme: root.theme variant: "title" text: "no tray items" } NuguText { width: parent.width theme: root.theme variant: "meta" text: "apps with tray controls appear here" color: root.theme.ui.minor wrapMode: Text.WordWrap } } } } }