main @ 284 LINES
[ HISTORY ] [ UP ]
┌─ QML ──────────────────────────────────────────────────────────────────────┐
│ pragma Singleton │
│ │
│ import QtQuick │
│ import QtQuick.Layouts │
│ import Quickshell │
│ import Quickshell.Wayland │
│ │
│ // audio popover anchored under the bar speaker icon. rendered inside a full-scree │
│ n │
│ // layer-shell overlay (same approach as TrayMenu, avoids the flicky xdg popup) wi │
│ th │
│ // click-away dismiss. output + input device switchers, volume sliders, mute toggl │
│ es │
│ Singleton { │
│ id: root │
│ │
│ property bool active: false │
│ │
│ function toggle() { │
│ root.active = !root.active; │
│ } │
│ function close() { │
│ root.active = false; │
│ } │
│ │
│ // one selectable device row (matches TrayMenu's MenuRow idiom) │
│ component DeviceRow: Rectangle { │
│ property var node: null │
│ property bool current: false │
│ signal chosen │
│ │
│ Layout.fillWidth: true │
│ implicitHeight: 24 │
│ radius: 4 │
│ color: dHover.hovered ? Config.trayMenuHover : "transparent" │
│ │
│ Row { │
│ anchors.left: parent.left │
│ anchors.right: parent.right │
│ anchors.leftMargin: 6 │
│ anchors.rightMargin: 6 │
│ anchors.verticalCenter: parent.verticalCenter │
│ spacing: 8 │
│ │
│ Rectangle { │
│ anchors.verticalCenter: parent.verticalCenter │
│ width: 9 │
│ height: 9 │
│ radius: 4.5 │
│ color: current ? Config.primary : "transparent" │
│ border.width: 1 │
│ border.color: current ? Config.primary : Config.border │
│ } │
│ │
│ Text { │
│ anchors.verticalCenter: parent.verticalCenter │
│ width: parent.width - 17 │
│ text: Audio.label(node) │
│ elide: Text.ElideRight │
│ font.family: Config.fontFamily │
│ font.pixelSize: Config.fontSize - 1 │
│ color: current ? Config.text : Qt.rgba(1, 1, 1, 0.7) │
│ } │
│ } │
│ │
│ HoverHandler { │
│ id: dHover │
│ } │
│ MouseArea { │
│ anchors.fill: parent │
│ onClicked: parent.chosen() │
│ } │
│ } │
│ │
│ // volume slider with % readout; drag or scroll │
│ component VolumeRow: Item { │
│ property real value: 0 │
│ property bool muted: false │
│ signal moved(real v) │
│ signal scrolled(real delta) │
│ │
│ Layout.fillWidth: true │
│ implicitHeight: 20 │
│ │
│ Text { │
│ id: rowPct │
│ anchors.right: parent.right │
│ anchors.verticalCenter: parent.verticalCenter │
│ width: 34 │
│ horizontalAlignment: Text.AlignRight │
│ text: Math.round(parent.value * 100) + "%" │
│ font.family: Config.fontFamily │
│ font.pixelSize: Config.fontSize - 2 │
│ color: parent.muted ? Config.urgent : Config.text │
│ } │
│ │
│ Item { │
│ anchors.left: parent.left │
│ anchors.right: rowPct.left │
│ anchors.rightMargin: 8 │
│ anchors.verticalCenter: parent.verticalCenter │
│ height: parent.height │
│ │
│ Rectangle { │
│ id: rowTrack │
│ anchors.verticalCenter: parent.verticalCenter │
│ width: parent.width │
│ height: 6 │
│ radius: 3 │
│ color: Config.border │
│ │
│ Rectangle { │
│ width: rowTrack.width * Math.max(0, Math.min(1, value)) │
│ height: parent.height │
│ radius: 3 │
│ color: muted ? Config.urgent : Config.primary │
│ } │
│ } │
│ │
│ MouseArea { │
│ id: rowMouse │
│ anchors.fill: parent │
│ onPressed: e => moved(e.x / rowMouse.width) │
│ onPositionChanged: e => { │
│ if (pressed) │
│ moved(e.x / rowMouse.width); │
│ } │
│ } │
│ WheelHandler { │
│ onWheel: e => scrolled(e.angleDelta.y > 0 ? 0.05 : -0.05) │
│ } │
│ } │
│ } │
│ │
│ // mute/unmute style toggle button │
│ component ToggleBtn: Rectangle { │
│ property bool on: false │
│ property string offText: "mute" │
│ property string onText: "unmute" │
│ signal toggled │
│ │
│ Layout.fillWidth: true │
│ implicitHeight: 24 │
│ radius: 4 │
│ color: tHover.hovered ? Config.trayMenuHover : "transparent" │
│ border.width: 1 │
│ border.color: Config.border │
│ │
│ Text { │
│ anchors.centerIn: parent │
│ text: parent.on ? parent.onText : parent.offText │
│ font.family: Config.fontFamily │
│ font.pixelSize: Config.fontSize - 1 │
│ color: parent.on ? Config.urgent : Config.text │
│ } │
│ │
│ HoverHandler { │
│ id: tHover │
│ } │
│ MouseArea { │
│ anchors.fill: parent │
│ onClicked: parent.toggled() │
│ } │
│ } │
│ │
│ component SectionLabel: Text { │
│ font.family: Config.fontFamily │
│ font.pixelSize: Config.fontSize - 2 │
│ color: Config.text │
│ opacity: 0.55 │
│ } │
│ │
│ PanelWindow { │
│ id: overlay │
│ visible: root.active │
│ color: "transparent" │
│ WlrLayershell.layer: WlrLayer.Overlay │
│ WlrLayershell.namespace: "ookshell-audio" │
│ │
│ anchors { │
│ top: true │
│ bottom: true │
│ left: true │
│ right: true │
│ } │
│ exclusiveZone: -1 // span the full output so coords match the tiled-window │
│ grid │
│ │
│ // backdrop: click anywhere outside the frame dismisses │
│ MouseArea { │
│ anchors.fill: parent │
│ onClicked: root.close() │
│ } │
│ │
│ Rectangle { │
│ id: frame │
│ // top-right corner aligned to the tiled-window corner: flush below th │
│ e │
│ // bar, inset by the hyprland gap on the right -- lines up with window │
│ s │
│ x: overlay.width - width - Config.dashGap │
│ y: Config.barMarginTop + Config.barHeight │
│ width: 264 │
│ height: col.implicitHeight + Config.trayMenuPadding * 2 │
│ color: Config.trayMenuBg │
│ radius: Config.frameRounding │
│ border.width: Config.frameBorderWidth │
│ border.color: Config.frameBorder │
│ clip: true │
│ │
│ MouseArea { │
│ anchors.fill: parent // swallow clicks so they don't hit the backd │
│ rop │
│ } │
│ │
│ ColumnLayout { │
│ id: col │
│ x: Config.trayMenuPadding │
│ y: Config.trayMenuPadding │
│ width: frame.width - Config.trayMenuPadding * 2 │
│ spacing: 4 │
│ │
│ SectionLabel { │
│ text: "output" │
│ } │
│ │
│ Repeater { │
│ model: Audio.sinks │
│ delegate: DeviceRow { │
│ required property var modelData │
│ node: modelData │
│ current: Audio.sink && Audio.sink.id === modelData.id │
│ onChosen: Audio.setSink(modelData) │
│ } │
│ } │
│ │
│ VolumeRow { │
│ Layout.topMargin: 4 │
│ value: Audio.volume │
│ muted: Audio.muted │
│ onMoved: v => Audio.setVolume(v) │
│ onScrolled: d => Audio.changeVolume(d) │
│ } │
│ │
│ ToggleBtn { │
│ Layout.topMargin: 2 │
│ on: Audio.muted │
│ onToggled: Audio.toggleMute() │
│ } │
│ │
│ Rectangle { │
│ Layout.fillWidth: true │
│ Layout.topMargin: 6 │
│ Layout.bottomMargin: 2 │
│ Layout.preferredHeight: 1 │
│ color: Config.border │
│ opacity: 0.5 │
│ } │
│ │
│ SectionLabel { │
│ text: "input" │
│ } │
│ │
│ Repeater { │
│ model: Audio.sources │
│ delegate: DeviceRow { │
│ required property var modelData │
│ node: modelData │
│ current: Audio.source && Audio.source.id === modelData.id │
│ onChosen: Audio.setSource(modelData) │
│ } │
│ } │
│ │
│ VolumeRow { │
│ Layout.topMargin: 4 │
│ value: Audio.sourceVolume │
│ muted: Audio.sourceMuted │
│ onMoved: v => Audio.setSourceVolume(v) │
│ onScrolled: d => Audio.changeSourceVolume(d) │
│ } │
│ │
│ ToggleBtn { │
│ Layout.topMargin: 2 │
│ on: Audio.sourceMuted │
│ onToggled: Audio.toggleSourceMute() │
│ } │
│ } │
│ } │
│ } │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ QML ────────────────────────────────┐
│ pragma Singleton │
│ │
│ import QtQuick │
│ import QtQuick.Layouts │
│ import Quickshell │
│ import Quickshell.Wayland │
│ │
│ // audio popover anchored under the bar spea │
│ ker icon. rendered inside a full-screen │
│ // layer-shell overlay (same approach as Tra │
│ yMenu, avoids the flicky xdg popup) with │
│ // click-away dismiss. output + input device │
│ switchers, volume sliders, mute toggles │
│ Singleton { │
│ id: root │
│ │
│ property bool active: false │
│ │
│ function toggle() { │
│ root.active = !root.active; │
│ } │
│ function close() { │
│ root.active = false; │
│ } │
│ │
│ // one selectable device row (matches Tr │
│ ayMenu's MenuRow idiom) │
│ component DeviceRow: Rectangle { │
│ property var node: null │
│ property bool current: false │
│ signal chosen │
│ │
│ Layout.fillWidth: true │
│ implicitHeight: 24 │
│ radius: 4 │
│ color: dHover.hovered ? Config.trayM │
│ enuHover : "transparent" │
│ │
│ Row { │
│ anchors.left: parent.left │
│ anchors.right: parent.right │
│ anchors.leftMargin: 6 │
│ anchors.rightMargin: 6 │
│ anchors.verticalCenter: parent.v │
│ erticalCenter │
│ spacing: 8 │
│ │
│ Rectangle { │
│ anchors.verticalCenter: pare │
│ nt.verticalCenter │
│ width: 9 │
│ height: 9 │
│ radius: 4.5 │
│ color: current ? Config.prim │
│ ary : "transparent" │
│ border.width: 1 │
│ border.color: current ? Conf │
│ ig.primary : Config.border │
│ } │
│ │
│ Text { │
│ anchors.verticalCenter: pare │
│ nt.verticalCenter │
│ width: parent.width - 17 │
│ text: Audio.label(node) │
│ elide: Text.ElideRight │
│ font.family: Config.fontFami │
│ ly │
│ font.pixelSize: Config.fontS │
│ ize - 1 │
│ color: current ? Config.text │
│ : Qt.rgba(1, 1, 1, 0.7) │
│ } │
│ } │
│ │
│ HoverHandler { │
│ id: dHover │
│ } │
│ MouseArea { │
│ anchors.fill: parent │
│ onClicked: parent.chosen() │
│ } │
│ } │
│ │
│ // volume slider with % readout; drag or │
│ scroll │
│ component VolumeRow: Item { │
│ property real value: 0 │
│ property bool muted: false │
│ signal moved(real v) │
│ signal scrolled(real delta) │
│ │
│ Layout.fillWidth: true │
│ implicitHeight: 20 │
│ │
│ Text { │
│ id: rowPct │
│ anchors.right: parent.right │
│ anchors.verticalCenter: parent.v │
│ erticalCenter │
│ width: 34 │
│ horizontalAlignment: Text.AlignR │
│ ight │
│ text: Math.round(parent.value * │
│ 100) + "%" │
│ font.family: Config.fontFamily │
│ font.pixelSize: Config.fontSize │
│ - 2 │
│ color: parent.muted ? Config.urg │
│ ent : Config.text │
│ } │
│ │
│ Item { │
│ anchors.left: parent.left │
│ anchors.right: rowPct.left │
│ anchors.rightMargin: 8 │
│ anchors.verticalCenter: parent.v │
│ erticalCenter │
│ height: parent.height │
│ │
│ Rectangle { │
│ id: rowTrack │
│ anchors.verticalCenter: pare │
│ nt.verticalCenter │
│ width: parent.width │
│ height: 6 │
│ radius: 3 │
│ color: Config.border │
│ │
│ Rectangle { │
│ width: rowTrack.width * │
│ Math.max(0, Math.min(1, value)) │
│ height: parent.height │
│ radius: 3 │
│ color: muted ? Config.ur │
│ gent : Config.primary │
│ } │
│ } │
│ │
│ MouseArea { │
│ id: rowMouse │
│ anchors.fill: parent │
│ onPressed: e => moved(e.x / │
│ rowMouse.width) │
│ onPositionChanged: e => { │
│ if (pressed) │
│ moved(e.x / rowMouse │
│ .width); │
│ } │
│ } │
│ WheelHandler { │
│ onWheel: e => scrolled(e.ang │
│ leDelta.y > 0 ? 0.05 : -0.05) │
│ } │
│ } │
│ } │
│ │
│ // mute/unmute style toggle button │
│ component ToggleBtn: Rectangle { │
│ property bool on: false │
│ property string offText: "mute" │
│ property string onText: "unmute" │
│ signal toggled │
│ │
│ Layout.fillWidth: true │
│ implicitHeight: 24 │
│ radius: 4 │
│ color: tHover.hovered ? Config.trayM │
│ enuHover : "transparent" │
│ border.width: 1 │
│ border.color: Config.border │
│ │
│ Text { │
│ anchors.centerIn: parent │
│ text: parent.on ? parent.onText │
│ : parent.offText │
│ font.family: Config.fontFamily │
│ font.pixelSize: Config.fontSize │
│ - 1 │
│ color: parent.on ? Config.urgent │
│ : Config.text │
│ } │
│ │
│ HoverHandler { │
│ id: tHover │
│ } │
│ MouseArea { │
│ anchors.fill: parent │
│ onClicked: parent.toggled() │
│ } │
│ } │
│ │
│ component SectionLabel: Text { │
│ font.family: Config.fontFamily │
│ font.pixelSize: Config.fontSize - 2 │
│ color: Config.text │
│ opacity: 0.55 │
│ } │
│ │
│ PanelWindow { │
│ id: overlay │
│ visible: root.active │
│ color: "transparent" │
│ WlrLayershell.layer: WlrLayer.Overla │
│ y │
│ WlrLayershell.namespace: "ookshell-a │
│ udio" │
│ │
│ anchors { │
│ top: true │
│ bottom: true │
│ left: true │
│ right: true │
│ } │
│ exclusiveZone: -1 // span the full o │
│ utput so coords match the tiled-window grid │
│ │
│ // backdrop: click anywhere outside │
│ the frame dismisses │
│ MouseArea { │
│ anchors.fill: parent │
│ onClicked: root.close() │
│ } │
│ │
│ Rectangle { │
│ id: frame │
│ // top-right corner aligned to t │
│ he tiled-window corner: flush below the │
│ // bar, inset by the hyprland ga │
│ p on the right -- lines up with windows │
│ x: overlay.width - width - Confi │
│ g.dashGap │
│ y: Config.barMarginTop + Config. │
│ barHeight │
│ width: 264 │
│ height: col.implicitHeight + Con │
│ fig.trayMenuPadding * 2 │
│ color: Config.trayMenuBg │
│ radius: Config.frameRounding │
│ border.width: Config.frameBorder │
│ Width │
│ border.color: Config.frameBorder │
│ clip: true │
│ │
│ MouseArea { │
│ anchors.fill: parent // swal │
│ low clicks so they don't hit the backdrop │
│ } │
│ │
│ ColumnLayout { │
│ id: col │
│ x: Config.trayMenuPadding │
│ y: Config.trayMenuPadding │
│ width: frame.width - Config. │
│ trayMenuPadding * 2 │
│ spacing: 4 │
│ │
│ SectionLabel { │
│ text: "output" │
│ } │
│ │
│ Repeater { │
│ model: Audio.sinks │
│ delegate: DeviceRow { │
│ required property va │
│ r modelData │
│ node: modelData │
│ current: Audio.sink │
│ && Audio.sink.id === modelData.id │
│ onChosen: Audio.setS │
│ ink(modelData) │
│ } │
│ } │
│ │
│ VolumeRow { │
│ Layout.topMargin: 4 │
│ value: Audio.volume │
│ muted: Audio.muted │
│ onMoved: v => Audio.setV │
│ olume(v) │
│ onScrolled: d => Audio.c │
│ hangeVolume(d) │
│ } │
│ │
│ ToggleBtn { │
│ Layout.topMargin: 2 │
│ on: Audio.muted │
│ onToggled: Audio.toggleM │
│ ute() │
│ } │
│ │
│ Rectangle { │
│ Layout.fillWidth: true │
│ Layout.topMargin: 6 │
│ Layout.bottomMargin: 2 │
│ Layout.preferredHeight: │
│ 1 │
│ color: Config.border │
│ opacity: 0.5 │
│ } │
│ │
│ SectionLabel { │
│ text: "input" │
│ } │
│ │
│ Repeater { │
│ model: Audio.sources │
│ delegate: DeviceRow { │
│ required property va │
│ r modelData │
│ node: modelData │
│ current: Audio.sourc │
│ e && Audio.source.id === modelData.id │
│ onChosen: Audio.setS │
│ ource(modelData) │
│ } │
│ } │
│ │
│ VolumeRow { │
│ Layout.topMargin: 4 │
│ value: Audio.sourceVolum │
│ e │
│ muted: Audio.sourceMuted │
│ onMoved: v => Audio.setS │
│ ourceVolume(v) │
│ onScrolled: d => Audio.c │
│ hangeSourceVolume(d) │
│ } │
│ │
│ ToggleBtn { │
│ Layout.topMargin: 2 │
│ on: Audio.sourceMuted │
│ onToggled: Audio.toggleS │
│ ourceMute() │
│ } │
│ } │
│ } │
│ } │
│ } │
└──────────────────────────────────────────────┘
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET