220 lines
7.1 KiB
QML
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|