import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Services.Pipewire import Quickshell.Services.SystemTray import "../../config.js" as Config Rectangle { id: root property bool expanded: false implicitWidth: layout.implicitWidth + 24 implicitHeight: Config.blockHeight radius: Config.blockRadius gradient: Gradient { GradientStop { id: stop1 position: 0.0 color: Config.accentGreen } GradientStop { id: stop2 position: 0.33 color: Config.accentYellow } GradientStop { id: stop3 position: 0.66 color: Config.accentOrange } GradientStop { id: stop4 position: 1.0 color: Config.accentRed } } Behavior on implicitWidth { NumberAnimation { duration: 250 easing.type: Easing.OutCubic } } Component.onCompleted: { console.info("QuickControl: loaded") console.info( "QuickControl: tray items =", SystemTray.items.values.length ) if (Pipewire.defaultAudioSink) { console.info( "QuickControl: audio sink =", Pipewire.defaultAudioSink.name ) if (Pipewire.defaultAudioSink.audio) { console.info( "QuickControl: volume =", Math.round( Pipewire.defaultAudioSink.audio.volume * 100 ) + "%", "muted =", Pipewire.defaultAudioSink.audio.muted ) } } else { console.warn("QuickControl: no default audio sink") } } onExpandedChanged: { console.info( "QuickControl: expanded =", expanded ) } RowLayout { id: layout anchors { left: parent.left right: parent.right verticalCenter: parent.verticalCenter leftMargin: 12 rightMargin: 12 } spacing: 6 Text { id: volumeIcon text: Pipewire.defaultAudioSink?.audio?.muted ? "󰖁" : "󰕾" visible: root.expanded color: Config.textDark font.family: Config.fontFamily font.pixelSize: Config.fontSize font.bold: true MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { if (!Pipewire.defaultAudioSink?.audio) { console.warn( "QuickControl: no audio sink" ) return } var audio = Pipewire.defaultAudioSink.audio audio.muted = !audio.muted console.info( "QuickControl: mute =", audio.muted ) } } } Text { id: volumeText text: Pipewire.defaultAudioSink?.audio ? Math.round( Pipewire.defaultAudioSink.audio.volume * 100 ) + "%" : "0%" visible: root.expanded color: Config.textDark font.family: Config.fontFamily font.pixelSize: Config.fontSize font.bold: true MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { if (!Pipewire.defaultAudioSink?.audio) { console.warn( "QuickControl: no audio sink" ) return } var audio = Pipewire.defaultAudioSink.audio audio.volume = Math.min( audio.volume + 0.05, 1.0 ) console.info( "QuickControl: volume =", Math.round(audio.volume * 100) + "%" ) } } } Text { id: brightnessIcon text: "󰃠" visible: root.expanded color: Config.textDark font.family: Config.fontFamily font.pixelSize: Config.fontSize font.bold: true MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { console.info( "QuickControl: brightness clicked" ) Quickshell.execDetached([ "brightnessctl", "set", "+5%" ]) } } } Text { id: networkIcon text: { var item = SystemTray.items.values.find(function(item) { return item.id === "nm-applet" }) if (!item) return "󰤨" var title = (item.title || "").toLowerCase() if (title.indexOf("ethernet") !== -1 || title.indexOf("wired") !== -1 || title.indexOf("lan") !== -1) return "󰈀" return "󰤨" } visible: root.expanded color: Config.textDark font.family: Config.fontFamily font.pixelSize: Config.fontSize font.bold: true MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { console.info("QuickControl: network clicked") Quickshell.execDetached(["nm-connection-editor"]) } } } Repeater { model: root.expanded ? SystemTray.items : null delegate: Item { id: trayItem required property var modelData implicitWidth: 18 implicitHeight: 18 Layout.preferredWidth: 18 Layout.preferredHeight: 18 Component.onCompleted: { console.info( "QuickControl: tray item:", modelData.title, "id =", modelData.id, "hasMenu =", modelData.hasMenu, "onlyMenu =", modelData.onlyMenu ) } QsMenuAnchor { id: menuAnchor anchor.item: trayItem menu: modelData.menu onOpened: { console.info( "QuickControl: context menu opened:", modelData.title ) } onClosed: { console.info( "QuickControl: context menu closed:", modelData.title ) } } Image { anchors.fill: parent source: modelData.icon sourceSize.width: 18 sourceSize.height: 18 fillMode: Image.PreserveAspectFit } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton cursorShape: Qt.PointingHandCursor onClicked: function(mouse) { if (mouse.button === Qt.RightButton) { console.info( "QuickControl: right click:", modelData.title, "hasMenu =", modelData.hasMenu ) if (modelData.hasMenu) { console.info( "QuickControl: opening menu:", modelData.title ) menuAnchor.open() } else { console.info( "QuickControl: no context menu:", modelData.title ) } return } if (mouse.button === Qt.MiddleButton) { console.info( "QuickControl: middle click:", modelData.title ) modelData.secondaryActivate() return } console.info( "QuickControl: left click:", modelData.title ) if (modelData.onlyMenu && modelData.hasMenu) { console.info( "QuickControl: onlyMenu item, opening menu:", modelData.title ) menuAnchor.open() } else { modelData.activate() } } } } } Text { id: dots text: "•••" color: Config.textDark font.family: Config.fontFamily font.pixelSize: Config.fontSize font.bold: true Layout.alignment: Qt.AlignVCenter MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { root.expanded = !root.expanded console.info( "QuickControl: toggled, expanded =", root.expanded ) } } } } Connections { target: SystemTray.items function onValuesChanged() { console.info( "QuickControl: system tray changed, items =", SystemTray.items.values.length ) } } Connections { target: Pipewire.defaultAudioSink?.audio ?? null function onVolumeChanged() { if (Pipewire.defaultAudioSink?.audio) { console.info( "QuickControl: volume changed:", Math.round( Pipewire.defaultAudioSink.audio.volume * 100 ) + "%" ) } } function onMutedChanged() { if (Pipewire.defaultAudioSink?.audio) { console.info( "QuickControl: muted changed:", Pipewire.defaultAudioSink.audio.muted ) } } } SequentialAnimation { running: true loops: Animation.Infinite ParallelAnimation { ColorAnimation { target: stop1 property: "color" from: Config.accentGreen to: Config.accentRed duration: Config.gradientAnimationDuration } ColorAnimation { target: stop2 property: "color" from: Config.accentYellow to: Config.accentGreen duration: Config.gradientAnimationDuration } ColorAnimation { target: stop3 property: "color" from: Config.accentOrange to: Config.accentYellow duration: Config.gradientAnimationDuration } ColorAnimation { target: stop4 property: "color" from: Config.accentRed to: Config.accentOrange duration: Config.gradientAnimationDuration } } ParallelAnimation { ColorAnimation { target: stop1 property: "color" from: Config.accentRed to: Config.accentGreen duration: Config.gradientAnimationDuration } ColorAnimation { target: stop2 property: "color" from: Config.accentGreen to: Config.accentYellow duration: Config.gradientAnimationDuration } ColorAnimation { target: stop3 property: "color" from: Config.accentYellow to: Config.accentOrange duration: Config.gradientAnimationDuration } ColorAnimation { target: stop4 property: "color" from: Config.accentOrange to: Config.accentRed duration: Config.gradientAnimationDuration } } } }