import QtQuick import Quickshell import Quickshell.Services.SystemTray import Quickshell.Widgets import "../.." Item { id: root width: parent ? parent.width : 1280 height: parent ? parent.height : 820 focus: true property ShellStateManager manager: null // ITEM/APPS-like layout constants property int menuTop: 140 property int lineHeight: 79 property int iconSize: 36 property int textStartX: 239 property int textRightPadding: 96 property int visibleRows: Math.max(1, Math.floor((height - menuTop - 64) / lineHeight)) property bool inSubmenu: false property int traySelection: 0 property int submenuSelection: 0 property int scrollOffset: 0 property var menuStack: [] property var currentMenuHandle: null property var submenuSourceTrayItem: null property int submenuPollTicks: 0 readonly property int activeSelection: inSubmenu ? submenuSelection : traySelection ListModel { id: submenuData } QsMenuOpener { id: opener menu: root.currentMenuHandle } // Raw source entries from current DBus menu (hidden) Repeater { id: submenuSourceRepeater model: opener.children delegate: Item { visible: false width: 0 height: 0 property var entry: modelData } } Timer { id: submenuPollTimer interval: 120 repeat: true running: false onTriggered: { if (!root.currentMenuHandle && root.submenuSourceTrayItem && root.submenuSourceTrayItem.menu) root.currentMenuHandle = root.submenuSourceTrayItem.menu; root.rebuildSubmenuData(); root.submenuPollTicks = root.submenuPollTicks + 1; if (submenuData.count > 0 || root.submenuPollTicks >= 20) stop(); } } function closeAll() { if (manager && manager.closeShell) manager.closeShell(); } function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } function trayCount() { return trayRepeater ? trayRepeater.count : 0; } function submenuCount() { return submenuData.count; } function currentCount() { return inSubmenu ? submenuCount() : trayCount(); } function wrapIndex(i) { const length = currentCount(); if (length === 0) return 0; return (i + length) % length; } function selectedTrayDelegate() { if (trayCount() <= 0) return null; return trayRepeater.itemAt(traySelection); } function selectedTrayItem() { const delegate = selectedTrayDelegate(); return delegate ? delegate.trayItem : null; } function selectedMenuEntry() { if (submenuSelection < 0 || submenuSelection >= submenuData.count) return null; const row = submenuData.get(submenuSelection); return row ? row.entry : null; } function normalizeSelection() { const length = currentCount(); if (length <= 0) { if (inSubmenu) submenuSelection = 0; else traySelection = 0; scrollOffset = 0; return; } if (inSubmenu) submenuSelection = clamp(submenuSelection, 0, length - 1); else traySelection = clamp(traySelection, 0, length - 1); const currentSelection = activeSelection; const maxOffset = Math.max(0, length - visibleRows); if (currentSelection < scrollOffset) scrollOffset = currentSelection; else if (currentSelection >= scrollOffset + visibleRows) scrollOffset = Math.max(0, currentSelection - visibleRows + 1); scrollOffset = clamp(scrollOffset, 0, maxOffset); } function rebuildSubmenuData() { submenuData.clear(); const count = submenuSourceRepeater ? submenuSourceRepeater.count : 0; for (let i = 0; i < count; i++) { const sourceItem = submenuSourceRepeater.itemAt(i); const entry = sourceItem ? sourceItem.entry : null; if (!entry || entry.isSeparator) continue; submenuData.append({ entry: entry }); } submenuSelection = clamp(submenuSelection, 0, Math.max(0, submenuData.count - 1)); normalizeSelection(); } function enterSubmenu(menuHandle) { currentMenuHandle = menuHandle; inSubmenu = true; submenuSelection = 0; scrollOffset = 0; submenuData.clear(); submenuPollTicks = 0; Qt.callLater(function () { rebuildSubmenuData(); submenuPollTimer.restart(); }); } function openSelectedTraySubmenu() { const trayItem = selectedTrayItem(); if (!trayItem) return; if (!trayItem.hasMenu) { trayItem.activate(); closeAll(); return; } menuStack = []; submenuSourceTrayItem = trayItem; enterSubmenu(trayItem.menu); } function triggerSelectedSubmenuEntry() { const entry = selectedMenuEntry(); if (!entry) return; if (entry.hasChildren) { const stackCopy = menuStack.slice(0); stackCopy.push(currentMenuHandle); menuStack = stackCopy; submenuSourceTrayItem = null; enterSubmenu(entry); return; } entry.triggered(); closeAll(); } function backAction() { if (inSubmenu) { if (menuStack.length > 0) { const stackCopy = menuStack.slice(0); const parentHandle = stackCopy.pop(); menuStack = stackCopy; submenuSourceTrayItem = null; enterSubmenu(parentHandle); } else { inSubmenu = false; currentMenuHandle = null; submenuSourceTrayItem = null; submenuData.clear(); submenuPollTimer.stop(); scrollOffset = 0; normalizeSelection(); } return; } if (manager && manager.closePowerMenu) manager.closePowerMenu(); } function handleKey(key) { switch (key) { case Qt.Key_Up: if (inSubmenu) submenuSelection = wrapIndex(submenuSelection - 1); else traySelection = wrapIndex(traySelection - 1); normalizeSelection(); return true; case Qt.Key_Down: if (inSubmenu) submenuSelection = wrapIndex(submenuSelection + 1); else traySelection = wrapIndex(traySelection + 1); normalizeSelection(); return true; case Qt.Key_Z: case Qt.Key_Return: case Qt.Key_Enter: if (inSubmenu) triggerSelectedSubmenuEntry(); else openSelectedTraySubmenu(); return true; case Qt.Key_X: case Qt.Key_Shift: case Qt.Key_Escape: backAction(); return true; } return false; } Connections { target: manager ignoreUnknownSignals: true function onPowerMenuOpened() { root.inSubmenu = false; root.currentMenuHandle = null; root.submenuSourceTrayItem = null; root.menuStack = []; root.submenuPollTicks = 0; submenuPollTimer.stop(); submenuData.clear(); root.scrollOffset = 0; root.traySelection = 0; root.submenuSelection = 0; root.normalizeSelection(); } } Connections { target: SystemTray.items ignoreUnknownSignals: true function onCountChanged() { root.normalizeSelection(); } function onModelReset() { root.normalizeSelection(); } function onRowsInserted() { root.normalizeSelection(); } function onRowsRemoved() { root.normalizeSelection(); } } Connections { target: opener.children ignoreUnknownSignals: true function onCountChanged() { root.rebuildSubmenuData(); } function onModelReset() { root.rebuildSubmenuData(); } function onRowsInserted() { root.rebuildSubmenuData(); } function onRowsRemoved() { root.rebuildSubmenuData(); } } Text { text: inSubmenu ? "OPTIONS" : "POWER" font.family: "8bitoperator JVE" font.pixelSize: 71 renderType: Text.NativeRendering font.hintingPreference: Font.PreferNoHinting smooth: false antialiasing: false anchors.horizontalCenter: parent.horizontalCenter color: "#ffffff" y: 32 } Repeater { id: trayRepeater model: SystemTray.items delegate: Item { width: root.width height: root.lineHeight x: 0 y: root.menuTop + (index - root.scrollOffset) * root.lineHeight visible: !root.inSubmenu && index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows property var trayItem: modelData Text { x: root.textStartX y: 0 text: trayItem && trayItem.tooltipTitle ? trayItem.tooltipTitle : (trayItem ? trayItem.title : "") width: root.width - root.textStartX - root.textRightPadding font.family: "8bitoperator JVE" font.pixelSize: 71 font.letterSpacing: 1 renderType: Text.NativeRendering font.hintingPreference: Font.PreferNoHinting smooth: false antialiasing: false wrapMode: Text.NoWrap elide: Text.ElideRight color: root.activeSelection == index ? "#fefe00" : "#ffffff" } IconImage { id: trayIcon x: 182 y: 8 + 14 implicitSize: root.iconSize source: trayItem ? trayItem.icon : "" asynchronous: true } Image { source: "../QuickSettings/soul.png" width: 36 height: 36 x: trayIcon.x y: trayIcon.y opacity: 0.7 visible: root.activeSelection == index } MouseArea { anchors.fill: parent hoverEnabled: true onEntered: { root.traySelection = index; root.normalizeSelection(); } onClicked: { root.traySelection = index; root.normalizeSelection(); root.openSelectedTraySubmenu(); } onWheel: function (wheel) { if (!trayItem) return; const horizontal = Math.abs(wheel.angleDelta.x) > Math.abs(wheel.angleDelta.y); const delta = horizontal ? wheel.angleDelta.x : wheel.angleDelta.y; trayItem.scroll(delta, horizontal); wheel.accepted = true; } } } } Repeater { id: submenuRepeater model: submenuData delegate: Item { property var entry: model.entry width: root.width height: root.lineHeight x: 0 y: root.menuTop + (index - root.scrollOffset) * root.lineHeight visible: root.inSubmenu && index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows Text { x: root.textStartX y: 0 width: root.width - root.textStartX - root.textRightPadding text: { if (!entry) return ""; let prefix = ""; if (entry.buttonType === QsMenuButtonType.CheckBox) prefix = entry.checkState === Qt.Checked ? "[x] " : "[ ] "; else if (entry.buttonType === QsMenuButtonType.RadioButton) prefix = entry.checkState === Qt.Checked ? "(x) " : "( ) "; return prefix + String(entry.text || "") + (entry.hasChildren ? " >" : ""); } font.family: "8bitoperator JVE" font.pixelSize: 71 font.letterSpacing: 1 renderType: Text.NativeRendering font.hintingPreference: Font.PreferNoHinting smooth: false antialiasing: false wrapMode: Text.NoWrap elide: Text.ElideRight color: root.activeSelection == index ? "#fefe00" : "#ffffff" } Image { source: "../QuickSettings/soul.png" width: 36 height: 36 x: 182 y: 8 + 14 opacity: 0.7 visible: root.activeSelection == index } MouseArea { anchors.fill: parent hoverEnabled: true onEntered: { root.submenuSelection = index; root.normalizeSelection(); } onClicked: { root.submenuSelection = index; root.normalizeSelection(); root.triggerSelectedSubmenuEntry(); } } } } WheelHandler { acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad onWheel: function (event) { const count = root.currentCount(); if (count <= 0) return; const delta = event.angleDelta.y > 0 ? -1 : 1; if (root.inSubmenu) root.submenuSelection = root.wrapIndex(root.submenuSelection + delta); else root.traySelection = root.wrapIndex(root.traySelection + delta); root.normalizeSelection(); event.accepted = true; } } Text { visible: !root.inSubmenu && trayCount() === 0 text: "NO TRAY ITEMS" font.family: "8bitoperator JVE" font.pixelSize: 54 font.letterSpacing: 1 renderType: Text.NativeRendering font.hintingPreference: Font.PreferNoHinting smooth: false antialiasing: false color: "#ffffff" anchors.horizontalCenter: parent.horizontalCenter y: 302 } Text { visible: root.inSubmenu && !submenuPollTimer.running && submenuCount() === 0 text: "NO OPTIONS" font.family: "8bitoperator JVE" font.pixelSize: 54 font.letterSpacing: 1 renderType: Text.NativeRendering font.hintingPreference: Font.PreferNoHinting smooth: false antialiasing: false color: "#ffffff" anchors.horizontalCenter: parent.horizontalCenter y: 302 } Component.onCompleted: normalizeSelection() }