import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Services.SystemTray import qs.Commons import qs.Services import qs.Widgets // A compact grid panel listing all tray items, opened from the Tray widget NPanel { id: root objectName: "trayDropdownPanel" // Widget info for menu functionality property string widgetSection: "" property int widgetIndex: -1 // Trigger refresh when settings change property int settingsVersion: 0 // Read favorites directly from settings for reactivity readonly property var favoritesList: { // Reference settingsVersion to force recalculation when it changes var _ = root.settingsVersion if (widgetSection === "" || widgetIndex < 0) return [] var widgets = Settings.data.bar.widgets[widgetSection] if (!widgets || widgetIndex >= widgets.length) return [] var widgetSettings = widgets[widgetIndex] if (!widgetSettings || widgetSettings.id !== "Tray") return [] return widgetSettings.favorites || [] } function wildCardMatch(str, rule) { if (!str || !rule) return false let escaped = rule.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') let pattern = '^' + escaped.replace(/\\\*/g, '.*') + '$' try { return new RegExp(pattern, 'i').test(str) } catch(e) { return false } } function isFavorite(item) { if (!favoritesList || favoritesList.length === 0) return false const title = item?.tooltipTitle || item?.name || item?.id || "" for (var i = 0; i < favoritesList.length; i++) { if (wildCardMatch(title, favoritesList[i])) return true } return false } // Dynamic sizing based on item count // Show items that are NOT favorites (non-favorites go to dropdown) readonly property var trayValuesAll: (SystemTray.items && SystemTray.items.values) ? SystemTray.items.values : [] readonly property var trayValues: trayValuesAll.filter(function(it){ return !root.isFavorite(it) }) readonly property int itemCount: trayValues.length readonly property int maxColumns: 8 readonly property real cellSize: Math.round(Style.capsuleHeight * 0.65) readonly property real outerPadding: Style.marginM readonly property real innerSpacing: Style.marginM readonly property int columns: Math.max(1, Math.min(maxColumns, itemCount)) readonly property int rows: Math.max(1, Math.ceil(itemCount / Math.max(1, columns))) // Add 2*gap margins around the grid preferredWidth: (columns * cellSize) + ((columns - 1) * innerSpacing) + (2 * outerPadding) preferredHeight: (rows * cellSize) + ((rows - 1) * innerSpacing) + (2 * outerPadding) // Positioning is handled automatically by NPanel when toggle(buttonItem) is called // Watch for settings changes to refresh the dropdown Connections { target: Settings function onSettingsSaved() { // Force refresh by incrementing settingsVersion, which triggers recalculation of favoritesList root.settingsVersion++ } } panelContent: Item { id: content Grid { id: grid anchors.fill: parent anchors.margins: outerPadding spacing: innerSpacing columns: root.columns rowSpacing: innerSpacing columnSpacing: innerSpacing Repeater { id: repeater model: root.trayValues delegate: Item { width: root.cellSize height: root.cellSize IconImage { id: trayIcon anchors.fill: parent asynchronous: true backer.fillMode: Image.PreserveAspectFit source: { let icon = modelData?.icon || "" if (!icon) return "" if (icon.includes("?path=")) { const chunks = icon.split("?path=") const name = chunks[0] const path = chunks[1] const fileName = name.substring(name.lastIndexOf("/") + 1) return `file://${path}/${fileName}` } return icon } layer.enabled: true layer.effect: ShaderEffect { property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant property real colorizeMode: 1.0 fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") } MouseArea { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onClicked: (mouse) => { if (!modelData) return if (mouse.button === Qt.RightButton && modelData.hasMenu && modelData.menu && trayMenu.item) { trayMenu.item.menu = modelData.menu trayMenu.item.screen = root.screen trayMenu.item.trayItem = modelData trayMenu.item.widgetSection = root.widgetSection trayMenu.item.widgetIndex = root.widgetIndex const menuX = (root.columns > 1) ? (trayIcon.width / 2) : 0 const menuY = trayIcon.height trayMenu.item.showAt(trayIcon, menuX, menuY) } else if (mouse.button === Qt.LeftButton) { modelData.activate?.() // Close the dropdown after activation PanelService.getPanel("trayDropdownPanel", root.screen)?.close() } else if (mouse.button === Qt.MiddleButton) { modelData.secondaryActivate?.() PanelService.getPanel("trayDropdownPanel", root.screen)?.close() } } onWheel: (wheel) => { if (wheel.angleDelta.y > 0) modelData?.scrollUp?.() else if (wheel.angleDelta.y < 0) modelData?.scrollDown?.() } onEntered: TooltipService.show(Screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection()) onExited: TooltipService.hide() } } } } } // Tray menu host Loader { id: trayMenu asynchronous: false active: true source: "TrayMenu.qml" } } }