Files
Panama/config/dot/quickshell/modules/settings/ApplicationVolumeRow.qml
T

220 lines
7.1 KiB
QML

// One application's own level, and where it plays.
//
// The destination is part of the row because it is part of the same question:
// an application is too loud, or it is coming out of the wrong speakers, and
// both are answered here rather than in a mixer somebody else ships. Picking a
// device moves every stream the application owns; "System default" hands it
// back to following whatever the Output card says.
import Quickshell.Services.Pipewire
import QtQuick
import qs.config
import qs.modules.quicksettings
import qs.services
import qs.widgets
Rectangle {
id: root
required property var application
// Open only while a destination is being chosen. The row is otherwise one
// line tall, because the list of sinks is longer than the mixer usually is.
property bool picking: false
readonly property real volume: AudioDevices.applicationVolume(root.application)
readonly property bool muted: AudioDevices.applicationMuted(root.application)
// "" means the application follows the default sink rather than naming one.
readonly property string sinkName: String(SoundRouting.currentSinkFor(root.application) ?? "")
readonly property string sinkLabel: {
if (root.sinkName === "")
return "System default";
const node = AudioDevices.outputs.find(candidate =>
String(candidate?.name ?? "") === root.sinkName);
return node ? AudioDevices.label(node) : root.sinkName;
}
width: parent ? parent.width : 620
implicitHeight: 78 + (root.picking ? sinkMenu.implicitHeight + 6 : 0)
radius: Theme.cardRadius
color: Theme.alpha(Theme.fg, 0.025)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.07)
PwObjectTracker {
objects: root.application?.nodes ?? []
}
ThemedIcon {
id: applicationIcon
anchors.left: parent.left
anchors.leftMargin: 12
anchors.top: parent.top
anchors.topMargin: 11
size: 20
icon: root.application?.icon ?? "audio-x-generic-symbolic"
iconFallback: "audio-x-generic-symbolic"
tint: Theme.fg
}
SettingsButton {
id: sinkButton
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: applicationIcon.verticalCenter
text: root.sinkLabel + (root.picking ? " ▴" : " ▾")
enabled: !SoundRouting.busy
onClicked: root.picking = !root.picking
}
Column {
anchors.left: applicationIcon.right
anchors.leftMargin: 10
anchors.right: sinkButton.left
anchors.rightMargin: 12
anchors.verticalCenter: applicationIcon.verticalCenter
spacing: 1
Text {
width: parent.width
text: root.application?.label ?? "Unknown application"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.Medium
elide: Text.ElideRight
}
Text {
width: parent.width
visible: (root.application?.nodes?.length ?? 0) > 1
text: `${root.application?.nodes?.length ?? 0} audio streams`
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
IconButton {
id: muteButton
anchors.left: parent.left
anchors.leftMargin: 8
anchors.top: parent.top
anchors.topMargin: 42
size: 30
iconSize: 17
icon: root.muted || root.volume <= 0.001
? "audio-volume-muted-symbolic"
: "audio-volume-high-symbolic"
iconFallback: "audio-volume-high-symbolic"
onClicked: AudioDevices.setApplicationMuted(root.application, !root.muted)
}
ValueSlider {
anchors.left: muteButton.right
anchors.leftMargin: 7
anchors.right: volumeText.left
anchors.rightMargin: 10
anchors.verticalCenter: muteButton.verticalCenter
value: root.muted ? 0 : root.volume
onMoved: value => AudioDevices.setApplicationVolume(root.application, value)
}
Text {
id: volumeText
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: muteButton.verticalCenter
width: 38
text: Math.round(root.volume * 100) + "%"
color: Theme.fgDim
font.family: Theme.fontMono
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignRight
}
// The destinations: following the default, or any present output by name.
Column {
id: sinkMenu
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.top: parent.top
anchors.topMargin: 78
visible: root.picking
spacing: 0
Repeater {
model: [{
name: "",
label: "System default"
}].concat(AudioDevices.outputs.map(node => ({
name: String(node?.name ?? ""),
label: AudioDevices.label(node)
})))
Rectangle {
id: option
required property var modelData
readonly property bool chosen: String(option.modelData.name) === root.sinkName
width: sinkMenu.width
implicitHeight: 30
radius: 8
color: optionHover.hovered
? Theme.alpha(Theme.fg, 0.07)
: "transparent"
border.width: 0
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: mark.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: String(option.modelData.label)
color: option.chosen ? Theme.fg : Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
Text {
id: mark
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
visible: option.chosen
text: "✓"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
HoverHandler {
id: optionHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
if (option.modelData.name === "")
SoundRouting.routeToDefault(root.application);
else
SoundRouting.moveApplication(root.application, String(option.modelData.name));
root.picking = false;
}
}
}
}
}
}