// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Widgets import Quickshell.Services.SystemTray import QtQuick import ".." import "Logic.js" as Logic // Controls icon, volume icon, tray, layout flag. The controls and volume // icons open their panels in this capsule; the tray is a plain tray; the // flag switches layout on scroll and ignores clicks. Capsule { id: pill required property QtObject sound // The island window, which tray menus are positioned against. required property QtObject window property string panel: "" signal openSound() signal openControls() signal openDrawer() // A drawer page, from a control tile's right-click. signal openPage(string page) // Emitted just before a tray menu opens; the menu cannot report its close. signal menuShown() expanded: panel === "sound" || panel === "controls" restWidth: restRow.implicitWidth + 24 panelWidth: 320 panelHeight: (panel === "controls" ? controlsPanel.implicitHeight : soundPanel.implicitHeight) + 28 // Edit mode never outlives its panel. onPanelChanged: controlsPanel.editing = false Row { id: restRow anchors.centerIn: parent spacing: 12 opacity: pill.expanded ? 0 : 1 visible: opacity > 0 Behavior on opacity { NumberAnimation { duration: 120 } } IconImage { anchors.verticalCenter: parent.verticalCenter implicitSize: 18 source: Quickshell.iconPath("preferences-system", true) MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: pill.openControls() } } IconImage { anchors.verticalCenter: parent.verticalCenter implicitSize: 18 source: Quickshell.iconPath(Logic.volumeIcon(pill.sound.volume, pill.sound.muted), true) MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: pill.openSound() } } Repeater { model: SystemTray.items.values delegate: IconImage { id: trayIcon required property SystemTrayItem modelData anchors.verticalCenter: parent.verticalCenter implicitSize: 16 source: modelData.icon MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton cursorShape: Qt.PointingHandCursor onClicked: mouse => { const item = trayIcon.modelData; const wantsMenu = mouse.button === Qt.RightButton || item.onlyMenu; if (!wantsMenu) { item.activate(); return; } if (!item.hasMenu) return; const p = trayIcon.mapToItem(null, 0, trayIcon.height + 6); pill.menuShown(); item.display(pill.window, p.x, p.y); } } } } Image { anchors.verticalCenter: parent.verticalCenter width: 18 height: 18 sourceSize: Qt.size(18, 18) source: Qt.resolvedUrl("assets/" + Keyboard.flag) // One switch per wheel notch (120). A touchpad sends many small // deltas, and switching on each made the count erratic. // A MouseArea, not a WheelHandler: the handler never fired here, // while MouseArea.onWheel is what SoundPage already relies on. MouseArea { anchors.fill: parent property real acc: 0 onWheel: wheel => { acc += wheel.angleDelta.y; if (Math.abs(acc) >= 120) { Keyboard.cycle(acc > 0 ? "prev" : "next"); acc = 0; } wheel.accepted = true; } } } } SoundPanel { id: soundPanel x: 14 y: 14 width: pill.panelWidth - 28 sound: pill.sound opacity: pill.panel === "sound" ? 1 : 0 visible: opacity > 0 Behavior on opacity { NumberAnimation { duration: 160 } } onOpenDrawer: pill.openDrawer() } ControlsPanel { id: controlsPanel x: 14 y: 14 width: pill.panelWidth - 28 sound: pill.sound opacity: pill.panel === "controls" ? 1 : 0 visible: opacity > 0 Behavior on opacity { NumberAnimation { duration: 160 } } onOpenDrawer: page => pill.openPage(page) } }