155 lines
4.9 KiB
QML
155 lines
4.9 KiB
QML
// The level of the device a card is about, written straight through to
|
|
// PipeWire -- there is nothing to apply, so there is nothing to commit and
|
|
// nothing to debounce.
|
|
//
|
|
// The range is a property rather than a constant because over-amplification
|
|
// extends it to 150%. The track keeps its full length either way, so the same
|
|
// slider position always means the same level; the region past 100% is marked
|
|
// in the warning tone, because that is the part that can distort.
|
|
//
|
|
// Not a SettingRow: the control stacks below the label in a narrow window
|
|
// exactly as SliderRow and GradientSliderRow do, which SettingRow's fixed
|
|
// trailing column cannot.
|
|
|
|
import QtQuick
|
|
import Quickshell.Services.Pipewire
|
|
import qs.config
|
|
import qs.widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var node: null
|
|
property string label: "Volume"
|
|
property string detail: ""
|
|
property bool divider: true
|
|
property real maximum: 1
|
|
|
|
readonly property bool available: !!root.node?.audio
|
|
readonly property real volume: root.node?.audio?.volume ?? 0
|
|
readonly property bool muted: root.node?.audio?.muted ?? false
|
|
readonly property bool amplified: root.volume > 1.001
|
|
|
|
readonly property bool inline: root.width >= 520
|
|
readonly property int controlSpan: 300
|
|
|
|
width: parent ? parent.width : 620
|
|
implicitHeight: root.inline
|
|
? Math.max(56, copy.implicitHeight + 20)
|
|
: copy.implicitHeight + 32 + 30
|
|
opacity: root.available ? 1 : 0.45
|
|
|
|
// Reading or writing audio state on an untracked node silently returns
|
|
// zero, so the row binds the node it speaks for even though the device list
|
|
// above it is already tracking the same object.
|
|
PwObjectTracker {
|
|
objects: root.node ? [root.node] : []
|
|
}
|
|
|
|
function setVolume(ratio: real): void {
|
|
if (!root.node?.audio)
|
|
return;
|
|
const value = Math.max(0, Math.min(root.maximum, ratio * root.maximum));
|
|
// Moving the slider is also how you unmute, same as GNOME.
|
|
root.node.audio.muted = false;
|
|
root.node.audio.volume = value;
|
|
}
|
|
|
|
Column {
|
|
id: copy
|
|
|
|
x: 0
|
|
y: root.inline ? (root.height - height) / 2 : 10
|
|
width: root.inline ? root.width - root.controlSpan - 20 : root.width
|
|
spacing: 3
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: root.label
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
font.weight: Font.Medium
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
visible: root.detail !== ""
|
|
text: root.detail
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: control
|
|
|
|
width: root.inline ? root.controlSpan : root.width
|
|
height: 32
|
|
x: root.inline ? root.width - width : 0
|
|
y: root.inline ? (root.height - height) / 2 : copy.y + copy.height + 10
|
|
|
|
ValueSlider {
|
|
id: slider
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: readout.left
|
|
anchors.rightMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
enabled: root.available
|
|
value: root.muted ? 0 : root.volume / root.maximum
|
|
onMoved: ratio => root.setVolume(ratio)
|
|
}
|
|
|
|
// Everything above 100%. Drawn over the track rather than into it, so
|
|
// the slider itself stays the one the rest of the shell uses.
|
|
Rectangle {
|
|
anchors.right: slider.right
|
|
anchors.verticalCenter: slider.verticalCenter
|
|
width: root.maximum > 1 ? slider.width * (1 - 1 / root.maximum) : 0
|
|
height: 10
|
|
radius: 2
|
|
visible: root.maximum > 1
|
|
color: Theme.alpha(Theme.warn, 0.30)
|
|
border.width: 0
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.verticalCenter: slider.verticalCenter
|
|
x: slider.x + slider.width / root.maximum - 1
|
|
width: 2
|
|
height: 16
|
|
radius: 1
|
|
visible: root.maximum > 1
|
|
color: Theme.alpha(Theme.warn, 0.7)
|
|
border.width: 0
|
|
}
|
|
|
|
Text {
|
|
id: readout
|
|
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 46
|
|
horizontalAlignment: Text.AlignRight
|
|
text: Math.round(root.volume * 100) + "%"
|
|
color: root.amplified ? Theme.warn : Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.features: Theme.tabularFigures
|
|
font.pixelSize: Theme.fontSize
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
height: 1
|
|
visible: root.divider
|
|
color: Theme.alpha(Theme.fg, 0.065)
|
|
}
|
|
}
|