import Quickshell import QtQuick import Quickshell.Services.SystemTray import Quickshell.Widgets import qs.Commons import qs.Ui import "TrayModel.js" as TrayModel BarWidget { id: root moduleName: "omarchy.tray" property bool expanded: false property bool managePopupOpen: false property bool trayMenuOpen: false property var activeTrayItem: null property var activeTrayAnchor: null readonly property color foreground: bar ? bar.foreground : Color.foreground readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family readonly property var pinnedIds: Array.isArray(settings.pinned) ? settings.pinned : [] readonly property var hiddenIds: Array.isArray(settings.hidden) ? settings.hidden : [] readonly property var pinnedItems: bucket("pinned") readonly property var drawerItems: bucket("drawer") readonly property var allItems: bucket("all") readonly property int drawerCount: drawerItems.length readonly property int trayItemExtent: Style.space(16) readonly property int trayItemGap: Style.space(9) readonly property int trayJoinGap: Style.space(4) readonly property int drawerExtent: drawerCount > 0 ? drawerCount * trayItemExtent + (drawerCount - 1) * trayItemGap : 0 // Match Waybar's group/tray-expander drawer transition-duration. readonly property int animationDuration: 600 property real revealProgress: expanded ? 1 : 0 readonly property real revealExtent: drawerExtent * revealProgress function close() { managePopupOpen = false trayMenuOpen = false } function openTrayMenu(item, anchorItem, mouse) { if (!item || !item.menu) { var point = anchorItem.QsWindow.contentItem.mapFromItem(anchorItem, mouse.x, mouse.y) item.display(anchorItem.QsWindow.window, point.x, point.y) return } activeTrayItem = item activeTrayAnchor = anchorItem trayMenuOpen = true } function trayIconSource(icon) { var value = String(icon || "") var marker = "?path=" var markerIndex = value.indexOf(marker) if (markerIndex === -1) return value var name = value.substring(0, markerIndex).split("/").pop() var iconPath = value.substring(markerIndex + marker.length).split("&")[0] return Util.fileUrl(iconPath + "/hicolor/16x16/status/" + name + ".png") } function trayTooltip(item) { return item.tooltipTitle || item.title || item.id || "" } function classifyItem(item) { var iid = String(item.id || "") if (hiddenIds.indexOf(iid) !== -1) return "hidden" if (pinnedIds.indexOf(iid) !== -1) return "pinned" return "drawer" } function ownedByDedicatedWidget(item) { var layout = root.bar && root.bar.layoutConfig ? root.bar.layoutConfig : null return TrayModel.ownedByDedicatedWidget(item, layout) } function bucket(category) { var values = SystemTray.items.values var result = [] for (var i = 0; i < values.length; i++) { var item = values[i] if (item.status === Status.Passive) continue if (ownedByDedicatedWidget(item)) continue if (category === "all") { result.push(item) continue } if (classifyItem(item) === category) result.push(item) } return result } function persistTrayState(pinned, hidden) { if (!root.bar || !root.bar.shell || typeof root.bar.shell.updateEntryInline !== "function") return var id = root.moduleName || "omarchy.tray" root.bar.shell.updateEntryInline(id, { id: id, pinned: pinned, hidden: hidden }) } function togglePin(iid) { var p = pinnedIds.slice(), h = hiddenIds.slice() var idx = p.indexOf(iid) if (idx !== -1) p.splice(idx, 1) else { p.push(iid) var hi = h.indexOf(iid) if (hi !== -1) h.splice(hi, 1) } persistTrayState(p, h) } function toggleHide(iid) { var p = pinnedIds.slice(), h = hiddenIds.slice() var idx = h.indexOf(iid) if (idx !== -1) h.splice(idx, 1) else { h.push(iid) var pi = p.indexOf(iid) if (pi !== -1) p.splice(pi, 1) } persistTrayState(p, h) } visible: pinnedItems.length > 0 || drawerCount > 0 clip: false implicitWidth: root.vertical ? root.barSize : trayContent.implicitWidth implicitHeight: root.vertical ? trayContent.implicitHeight : root.barSize Behavior on revealProgress { NumberAnimation { duration: root.animationDuration; easing.type: Easing.OutCubic } } Loader { id: trayContent anchors.fill: parent sourceComponent: root.vertical ? verticalTray : horizontalTray } Component { id: horizontalTray Item { id: horizontalTrayRoot readonly property int pinnedWidth: pinnedRow.implicitWidth readonly property int drawerBlockWidth: root.allItems.length > 0 ? expandIcon.implicitWidth + root.drawerExtent : 0 implicitWidth: pinnedWidth + drawerBlockWidth implicitHeight: root.barSize // Mask out the empty area the collapsed drawer reserves for its slide-in, // so hovering it doesn't trigger expand and clicks pass through. containmentMask: QtObject { function contains(point: point): bool { if (point.y < 0 || point.y > horizontalTrayRoot.height) return false // Drawer reveals leftward; chevron sits at the right end when collapsed // and slides left as it opens. The visible region starts at the chevron. var chevronX = root.drawerExtent - root.revealExtent if (point.x >= chevronX && point.x <= horizontalTrayRoot.drawerBlockWidth) return true // Pinned items, placed to the right of the drawer block. var pinnedStart = horizontalTrayRoot.drawerBlockWidth return point.x >= pinnedStart && point.x <= horizontalTrayRoot.implicitWidth } } Item { id: drawerArea x: 0 width: horizontalTrayRoot.drawerBlockWidth height: root.barSize visible: root.allItems.length > 0 HoverHandler { onHoveredChanged: root.expanded = hovered } WidgetButton { id: expandIcon bar: root.bar width: implicitWidth height: implicitHeight x: root.drawerExtent - root.revealExtent text: "\uf053" horizontalMargin: 9 verticalPadding: 6 onPressed: function(button) { if (button === Qt.RightButton) root.managePopupOpen = !root.managePopupOpen } } Item { id: trayClip x: expandIcon.width anchors.verticalCenter: parent.verticalCenter width: root.drawerExtent height: root.barSize clip: true Row { id: trayIcons x: root.drawerExtent - root.revealExtent anchors.verticalCenter: parent.verticalCenter spacing: root.trayItemGap layer.enabled: true Repeater { model: root.drawerItems TrayItem {} } } } } Row { id: pinnedRow x: drawerArea.x + horizontalTrayRoot.drawerBlockWidth anchors.verticalCenter: parent.verticalCenter spacing: root.trayItemGap leftPadding: root.pinnedItems.length > 0 && root.allItems.length > 0 ? root.trayJoinGap : 0 Repeater { model: root.pinnedItems TrayItem {} } } } } Component { id: verticalTray Item { id: verticalTrayRoot readonly property int pinnedHeight: pinnedCol.implicitHeight readonly property int drawerBlockHeight: root.allItems.length > 0 ? expandIcon.implicitHeight + root.drawerExtent : 0 implicitWidth: root.barSize implicitHeight: pinnedHeight + drawerBlockHeight containmentMask: QtObject { function contains(point: point): bool { if (point.x < 0 || point.x > verticalTrayRoot.width) return false var chevronY = root.drawerExtent - root.revealExtent if (point.y >= chevronY && point.y <= verticalTrayRoot.drawerBlockHeight) return true var pinnedStart = verticalTrayRoot.drawerBlockHeight return point.y >= pinnedStart && point.y <= verticalTrayRoot.implicitHeight } } Item { id: drawerArea y: 0 width: root.barSize height: verticalTrayRoot.drawerBlockHeight visible: root.allItems.length > 0 HoverHandler { onHoveredChanged: root.expanded = hovered } WidgetButton { id: expandIcon bar: root.bar width: implicitWidth height: implicitHeight y: root.drawerExtent - root.revealExtent text: "\uf053" textRotation: 90 horizontalMargin: 9 verticalPadding: 6 onPressed: function(button) { if (button === Qt.RightButton) root.managePopupOpen = !root.managePopupOpen } } Item { id: trayClip y: expandIcon.height anchors.horizontalCenter: parent.horizontalCenter width: root.barSize height: root.drawerExtent clip: true Column { id: trayIcons y: root.drawerExtent - root.revealExtent anchors.horizontalCenter: parent.horizontalCenter spacing: root.trayItemGap layer.enabled: true Repeater { model: root.drawerItems TrayItem {} } } } } Column { id: pinnedCol y: drawerArea.y + verticalTrayRoot.drawerBlockHeight anchors.horizontalCenter: parent.horizontalCenter spacing: root.trayItemGap topPadding: root.pinnedItems.length > 0 && root.allItems.length > 0 ? root.trayJoinGap : 0 Repeater { model: root.pinnedItems TrayItem {} } } } } PopupCard { id: managePopup anchorItem: root owner: root bar: root.bar open: root.managePopupOpen contentWidth: managePopup.fittedContentWidth(Style.space(300)) contentHeight: managePopup.fittedContentHeight(manageColumn.implicitHeight) Column { id: manageColumn anchors.fill: parent spacing: Style.space(8) Text { text: "Tray icons" color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.body font.bold: true } Text { text: "Pinned icons stay visible. Hidden icons never show." color: Qt.darker(root.foreground, 1.4) font.family: root.fontFamily font.pixelSize: Style.font.caption wrapMode: Text.WordWrap width: parent.width } Text { visible: root.allItems.length === 0 text: "No tray items reporting." color: Qt.darker(root.foreground, 1.5) font.family: root.fontFamily font.pixelSize: Style.font.bodySmall font.italic: true } Repeater { model: root.allItems delegate: Item { id: rowRoot required property var modelData required property int index width: manageColumn.width implicitHeight: 28 readonly property string itemId: String(modelData.id || "") readonly property string displayName: { var t = String(modelData.title || "").trim() if (t) return t var tt = String(modelData.tooltipTitle || "").trim() if (tt) return tt var id = String(modelData.id || "") var slash = id.lastIndexOf("/") return slash !== -1 ? id.substring(slash + 1) : (id || "Unknown") } readonly property bool isPinned: root.pinnedIds.indexOf(itemId) !== -1 readonly property bool isHidden: root.hiddenIds.indexOf(itemId) !== -1 IconImage { id: rowIcon anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left implicitSize: 16 width: 16 height: 16 source: root.trayIconSource(rowRoot.modelData.icon) } Text { anchors.verticalCenter: parent.verticalCenter anchors.left: rowIcon.right anchors.leftMargin: Style.space(10) anchors.right: rowHideBtn.left anchors.rightMargin: Style.space(8) text: rowRoot.displayName color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall elide: Text.ElideRight } Button { id: rowPinBtn anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right iconText: "\uf08d" text: rowRoot.isPinned ? "Unpin" : "Pin" foreground: root.foreground horizontalPadding: 8 verticalPadding: 3 iconSize: Style.font.bodySmall fontSize: Style.font.bodySmall onClicked: root.togglePin(rowRoot.itemId) } Button { id: rowHideBtn anchors.verticalCenter: parent.verticalCenter anchors.right: rowPinBtn.left anchors.rightMargin: Style.space(6) iconText: "\uf06e" text: rowRoot.isHidden ? "Show" : "Hide" foreground: root.foreground horizontalPadding: 8 verticalPadding: 3 iconSize: Style.font.bodySmall fontSize: Style.font.bodySmall onClicked: root.toggleHide(rowRoot.itemId) } } } } } QsMenuOpener { id: trayMenuOpener menu: root.activeTrayItem ? root.activeTrayItem.menu : null } PopupCard { id: trayMenuPopup anchorItem: root.activeTrayAnchor || root owner: root bar: root.bar open: root.trayMenuOpen padding: Style.space(8) borderColor: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.45) contentWidth: trayMenuPopup.fittedContentWidth(Style.space(232)) contentHeight: trayMenuPopup.fittedContentHeight(trayMenuColumn.implicitHeight, Style.space(420)) Column { id: trayMenuColumn anchors.fill: parent spacing: 0 Repeater { model: trayMenuOpener.children delegate: Item { id: menuRow required property var modelData required property int index readonly property string rowText: String(modelData.text || "") readonly property string activeTitle: root.activeTrayItem ? String(root.activeTrayItem.title || root.activeTrayItem.id || "") : "" readonly property bool rootTitleEntry: index === 0 && modelData.hasChildren && rowText.toLowerCase() === activeTitle.toLowerCase() readonly property bool leadingSeparator: modelData.isSeparator && index <= 1 readonly property bool hiddenRow: rootTitleEntry || leadingSeparator visible: !hiddenRow width: trayMenuColumn.width implicitHeight: hiddenRow ? 0 : (modelData.isSeparator ? Style.space(11) : Style.space(30)) opacity: modelData.enabled ? 1.0 : 0.45 Rectangle { visible: menuRow.modelData.isSeparator anchors.left: parent.left anchors.leftMargin: Style.space(10) anchors.right: parent.right anchors.rightMargin: Style.space(10) anchors.verticalCenter: parent.verticalCenter height: 1 color: Color.popups.border opacity: 0.45 } Rectangle { visible: !menuRow.modelData.isSeparator anchors.fill: parent radius: Math.max(2, Style.cornerRadius) color: rowMouse.containsMouse && menuRow.modelData.enabled ? Style.hoverFillFor(root.foreground, root.foreground) : "transparent" } Text { visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left width: Style.space(22) horizontalAlignment: Text.AlignHCenter text: menuRow.modelData.checkState === Qt.Checked ? "\uf00c" : "" color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall } IconImage { id: menuIcon visible: !menuRow.modelData.isSeparator && String(menuRow.modelData.icon || "") !== "" anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: Style.space(24) implicitSize: Style.space(16) width: Style.space(16) height: Style.space(16) source: menuRow.modelData.icon } Text { visible: !menuRow.modelData.isSeparator anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: menuIcon.visible ? Style.space(46) : Style.space(28) anchors.right: submenuGlyph.left anchors.rightMargin: Style.space(8) text: menuRow.rowText color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall elide: Text.ElideRight } Text { id: submenuGlyph visible: !menuRow.modelData.isSeparator && menuRow.modelData.hasChildren anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: Style.space(10) text: "\u203a" color: root.foreground font.family: root.fontFamily font.pixelSize: Style.font.bodySmall } MouseArea { id: rowMouse anchors.fill: parent hoverEnabled: true enabled: !menuRow.modelData.isSeparator && menuRow.modelData.enabled cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: { if (menuRow.modelData.hasChildren) { var point = menuRow.QsWindow.contentItem.mapFromItem(menuRow, menuRow.width, menuRow.height / 2) menuRow.modelData.display(menuRow.QsWindow.window, point.x, point.y) } else { menuRow.modelData.triggered() root.close() } } } } } } } component TrayItem: Item { id: trayItemRoot required property var modelData visible: modelData.status !== Status.Passive implicitWidth: visible ? root.trayItemExtent : 0 implicitHeight: visible ? root.trayItemExtent : 0 function displayMenu(mouse) { root.openTrayMenu(trayItemRoot.modelData, trayItemRoot, mouse) } IconImage { anchors.centerIn: parent implicitSize: Style.space(12) width: Style.space(12) height: Style.space(12) source: root.trayIconSource(trayItemRoot.modelData.icon) } MouseArea { id: mouseArea anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: if (root.bar) root.bar.showTooltip(trayItemRoot, root.trayTooltip(modelData)) onExited: if (root.bar) root.bar.hideTooltip(trayItemRoot) onPressed: function(mouse) { if (mouse.button === Qt.RightButton) { trayItemRoot.displayMenu(mouse) mouse.accepted = true } } onClicked: function(mouse) { if (mouse.button === Qt.RightButton) { mouse.accepted = true } else if (mouse.button === Qt.MiddleButton) { trayItemRoot.modelData.secondaryActivate() } else if (trayItemRoot.modelData.onlyMenu) { trayItemRoot.displayMenu(mouse) } else { trayItemRoot.modelData.activate() } } onWheel: function(wheel) { trayItemRoot.modelData.scroll(wheel.angleDelta.y, false) } } readonly property bool tooltipHovered: visible && opacity > 0 && mouseArea.containsMouse } }