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

244 lines
8.3 KiB
QML

// Sound.
//
// One sectioned page, top to bottom: what plays, what listens, what each
// application is doing, what the machine says when it wants attention, and the
// card profiles underneath all of it. There are no tabs and nothing to apply --
// every control here writes to PipeWire or to a desktop key that other
// applications already honour, and lands the moment it is moved.
//
// The page used to end in a button that opened GNOME's Sound panel for device
// profiles. It does not any more: SoundCards reads and writes them natively, so
// there is nothing left on this subject that Panama hands to someone else.
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
title: "Sound"
lede: "Live PipeWire — every change lands immediately, nothing to apply."
readonly property var currentOutput: AudioDevices.current(true)
readonly property var currentInput: AudioDevices.current(false)
// Over-amplification is the whole range, not a second slider: the maximum
// moves and the region past 100% is marked.
readonly property real volumeMaximum: Settings.overAmplification ? 1.5 : 1
// The three services that read the machine rather than the graph do it once
// per page open, the way the Displays page refreshes hardware brightness.
Component.onCompleted: {
SoundFeedback.refresh();
SoundDefaults.refresh();
SoundCards.refresh();
}
SettingsCard {
title: "Output"
subtitle: root.currentOutput
? AudioDevices.label(root.currentOutput)
: "No output device"
SoundDeviceList {
width: parent.width
output: true
}
SoundVolumeRow {
label: "Volume"
node: root.currentOutput
maximum: root.volumeMaximum
}
AudioBalance {
node: root.currentOutput
}
ToggleRow {
setting: "overAmplification"
divider: false
}
// Turning over-amplification off has to bring the level down with it.
// The preference only moves the top of the range; PipeWire keeps
// whatever was set, so a device left at 140% stays there with a slider
// that now ends at 100 and shows itself pinned at full. Nothing the
// page can do afterwards expresses the difference, and the reading is
// wrong until something else happens to move the volume.
Connections {
target: Settings
function onOverAmplificationChanged(): void {
if (Settings.overAmplification)
return;
const audio = root.currentOutput?.audio ?? null;
if (audio && audio.volume > 1)
audio.volume = 1;
}
}
}
SettingsCard {
title: "Input"
subtitle: root.currentInput
? AudioDevices.label(root.currentInput)
: "No input device"
SoundDeviceList {
width: parent.width
output: false
}
SoundVolumeRow {
label: "Input volume"
node: root.currentInput
}
// Recording and playing back is the only honest microphone test: the
// level meter above says something is arriving, not that it is you.
ActionRow {
label: "Test your microphone"
// A test that fails does it by being silent, which is the same
// thing a broken microphone does. The reason takes the detail's
// place so the two can be told apart.
detail: SoundTest.lastError !== ""
? SoundTest.lastError
: "Record a few seconds and play it back through the selected output"
divider: captureRow.visible
action: {
if (SoundTest.micTestState === "recording")
return "Recording…";
if (SoundTest.micTestState === "playing")
return "Playing back…";
return "Record & play back";
}
enabled: SoundTest.micTestState === "idle" && !!root.currentInput
onTriggered: SoundTest.startMicTest(root.currentInput, root.currentOutput)
}
SoundCaptureRow {
id: captureRow
divider: false
}
}
SettingsCard {
title: "Applications"
subtitle: SoundRouting.lastError !== ""
? SoundRouting.lastError
: "Each application's own level, and where it plays. Applications playing sound appear here on their own."
ApplicationMixer {
width: parent.width
}
}
// ── What the machine says when it wants attention ───────────────────────
Text {
width: parent.width
text: "Alerts & feedback"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.7
topPadding: 6
}
SettingsCard {
title: "Alerts"
subtitle: SoundFeedback.lastError
SoundThemeRow {}
SettingRow {
label: "Event sounds"
detail: "Interface sounds from GTK and GNOME applications, and Panama's notification chime"
controlWidth: 48
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: SoundFeedback.eventSounds
enabled: !SoundFeedback.busy
onToggled: checked => SoundFeedback.setEventSounds(checked)
}
}
ToggleRow {
setting: "volumeChangeBlip"
}
SettingRow {
label: "Input feedback"
detail: "Key clicks and input event sounds in GTK applications"
controlWidth: 48
divider: false
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: SoundFeedback.inputFeedback
enabled: !SoundFeedback.busy
onToggled: checked => SoundFeedback.setInputFeedback(checked)
}
}
}
// ── The hardware underneath the devices ─────────────────────────────────
Text {
width: parent.width
text: "Advanced"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.7
topPadding: 6
}
SettingsCard {
title: "Device profiles"
subtitle: SoundCards.lastError !== ""
? SoundCards.lastError
: "What each sound card is configured to do. Ports that are not physically connected are named, not hidden."
Repeater {
model: SoundCards.cards ?? []
OptionPickerRow {
required property var modelData
required property int index
label: String(modelData.description ?? modelData.name ?? "")
detail: String(modelData.portHint ?? "")
enabled: !SoundCards.busy
divider: index < (SoundCards.cards?.length ?? 0) - 1
current: modelData.activeProfile
options: (modelData.profiles ?? []).map(profile => ({
value: profile.name,
label: String(profile.description ?? profile.name),
detail: profile.available === false ? "Not available right now" : ""
}))
onPicked: value => SoundCards.setProfile(String(modelData.name), value)
}
}
Text {
width: parent.width
visible: (SoundCards.cards?.length ?? 0) === 0
text: SoundCards.busy ? "Reading device profiles…" : "No sound cards reported"
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
horizontalAlignment: Text.AlignHCenter
topPadding: 12
bottomPadding: 12
}
}
}