diff options
Diffstat (limited to 'desktop/modules/sound/SoundPage.qml')
| -rw-r--r-- | desktop/modules/sound/SoundPage.qml | 191 |
1 files changed, 167 insertions, 24 deletions
diff --git a/desktop/modules/sound/SoundPage.qml b/desktop/modules/sound/SoundPage.qml index d530e9d..7a757c9 100644 --- a/desktop/modules/sound/SoundPage.qml +++ b/desktop/modules/sound/SoundPage.qml @@ -22,16 +22,38 @@ Column { spacing: 16 - // Output and input, each with its own slider. + // A device's human name, most readable first. + function devLabel(n) { + if (!n) return "—"; + return n.description || n.nickname || n.name || ("node " + n.id); + } + + // The state glyph for a level row, the same shapes the OSD uses: a + // microphone for the input, a speaker whose fill follows the level for the + // output, red when muted. + function stateIcon(audio, input) { + if (input) return audio?.muted ? "\uf131" : "\uf130"; + if (!audio || audio.muted || audio.volume <= 0) return "\uf026"; + return audio.volume < 0.5 ? "\uf027" : "\uf028"; + } + + // Output and input. Each shows the current device, a slider for it, and an + // in-place device list that sets the default. The list expands within the + // page rather than opening a popup: a QtQuick.Controls popup is a separate + // window and does not position reliably on this layer surface. Repeater { model: [ - { label: "Output", node: page.service.sink }, - { label: "Input", node: page.service.source }, + { label: "Output", node: page.service.sink, devices: page.service.outputs, + setDefault: n => Pipewire.preferredDefaultAudioSink = n }, + { label: "Input", node: page.service.source, devices: page.service.inputs, + setDefault: n => Pipewire.preferredDefaultAudioSource = n }, ] Column { + id: section required property var modelData readonly property var audio: modelData.node?.audio ?? null + property bool expanded: false width: page.width spacing: 6 @@ -48,42 +70,163 @@ Column { color: Theme.text } - Text { - anchors.right: parent.right - text: !audio ? "—" : audio.muted ? "muted" : Math.round(audio.volume * 100) + "%" - font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1 } - color: audio?.muted ? Theme.red : Theme.subtext + // State glyph and level, clickable to mute/unmute. + Row { + id: levelRow + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + spacing: 6 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: page.stateIcon(audio, modelData.label === "Input") + font { family: Theme.fontFamily; pixelSize: Theme.fontSize } + color: audio?.muted ? Theme.red : Theme.accent + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: !audio ? "—" : audio.muted ? "muted" : Math.round(audio.volume * 100) + "%" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1 } + color: audio?.muted ? Theme.red : Theme.subtext + } + } + + MouseArea { + anchors.fill: levelRow + enabled: audio !== null + cursorShape: Qt.PointingHandCursor + onClicked: if (audio) audio.muted = !audio.muted + } + } + + // The current device; click to open the device list. + Item { + width: parent.width + implicitHeight: devRow.implicitHeight + + Row { + id: devRow + width: parent.width + spacing: 6 + + Text { + width: parent.width - chevron.implicitWidth - parent.spacing + elide: Text.ElideRight + text: page.devLabel(modelData.node) + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.text + } + + Text { + id: chevron + text: section.expanded ? "\uf077" : "\uf078" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.overlay + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: section.expanded = !section.expanded } } - Text { + // The devices, expanded in place. The current one is highlighted; + // clicking a row makes it the default and folds the list away. + Column { width: parent.width - elide: Text.ElideRight - text: modelData.node?.description ?? modelData.node?.name ?? "" - font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } - color: Theme.overlay + spacing: 2 + visible: section.expanded + + Repeater { + model: section.modelData.devices + + Rectangle { + required property var modelData + readonly property bool current: section.modelData.node === modelData + + width: section.width + implicitHeight: devText.implicitHeight + 8 + radius: 6 + color: devArea.containsMouse ? Qt.alpha(Theme.accent, 0.18) + : current ? Qt.alpha(Theme.accent, 0.10) : "transparent" + + Text { + id: devText + anchors { + left: parent.left; right: parent.right + verticalCenter: parent.verticalCenter + leftMargin: 8; rightMargin: 8 + } + elide: Text.ElideRight + text: page.devLabel(modelData) + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3; bold: current } + color: current ? Theme.accent : Theme.text + } + + MouseArea { + id: devArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + section.modelData.setDefault(modelData); + section.expanded = false; + } + } + } + } } - // Click or drag anywhere on the bar to set the level. - Rectangle { + // The meter. The hit area is taller than the 8px bar so it is easy + // to grab; preventStealing stops the page's Flickable taking the + // press, and the wheel is accepted here so the page does not scroll + // instead. Click sets, drag scrubs, wheel steps, wheel-up unmutes + // (the same thing the volume keybind does). + Item { + id: meter width: parent.width - height: 8 - radius: 4 - color: Theme.surface + height: 20 + + readonly property real level: Math.min(audio?.volume ?? 0, 1) Rectangle { - height: parent.height - radius: parent.radius - width: parent.width * Math.min(audio?.volume ?? 0, 1) - color: audio?.muted ? Theme.red : Theme.accent - opacity: audio?.muted ? 0.5 : 1 + anchors.verticalCenter: parent.verticalCenter + width: parent.width + height: 8 + radius: 4 + color: Theme.surface + + Rectangle { + height: parent.height + radius: parent.radius + width: parent.width * meter.level + color: audio?.muted ? Theme.red : Theme.accent + opacity: audio?.muted ? 0.5 : 1 + } } MouseArea { anchors.fill: parent enabled: audio !== null - onPositionChanged: mouse => set(mouse.x) + preventStealing: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => set(mouse.x) + onPositionChanged: mouse => { if (pressed) set(mouse.x); } + + onWheel: wheel => { + if (!audio || wheel.angleDelta.y === 0) return; + if (wheel.angleDelta.y > 0) { + audio.muted = false; + audio.volume = Math.min(1, audio.volume + 0.05); + } else { + audio.volume = Math.max(0, audio.volume - 0.05); + } + wheel.accepted = true; + } + function set(x) { if (audio) audio.volume = Math.max(0, Math.min(1, x / width)); } |
