Files
home/.local/share/plasma/plasmoids/KdeControlStation/contents/ui/pages/VolumePage.qml

389 lines
15 KiB
QML
Raw Normal View History

2025-10-08 10:35:48 +02:00
import QtQuick 2.15
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.15
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components as PlasmaComponents
import org.kde.plasma.components as PC3
import org.kde.kirigami as Kirigami
import org.kde.kcmutils as KCM// KCMLauncher
import org.kde.config as KConfig // KAuthorized.authorizeControlModule
import org.kde.ksvg as KSvg
import org.kde.plasma.extras as PlasmaExtras
import org.kde.plasma.private.volume
import "../lib" as Lib
import "components/volume" as VolumeComponents
PageTemplate {
id: volumePage
sectionTitle: i18n("Audio Volume")
property int contentItemHeight: contentItem.implicitHeight
GlobalConfig {
id: config
}
property bool volumeFeedback: config.audioFeedback
property bool globalMute: config.globalMute
property string displayName: i18n("Audio Volume")
property QtObject draggedStream: null
property bool showVirtualDevices: true
property string currentTab: "devices"
// DEFAULT_SINK_NAME in module-always-sink.c
readonly property string dummyOutputName: "auto_null"
readonly property string noDevicePlaceholderMessage: i18n("No output or input devices found")
function nodeName(pulseObject) {
const nodeNick = pulseObject.pulseProperties["node.nick"]
if (nodeNick) {
return nodeNick
}
if (pulseObject.description) {
return pulseObject.description
}
if (pulseObject.name) {
return pulseObject.name
}
return i18n("Device name not found")
}
function isDummyOutput(output) {
return output && output.name === dummyOutputName;
}
function volumePercent(volume) {
return Math.round(volume / PulseAudio.NormalVolume * 100.0);
}
function playFeedback(sinkIndex) {
if (!volumeFeedback) {
return;
}
if (sinkIndex == undefined) {
sinkIndex = PreferredDevice.sink.index;
}
feedback.play(sinkIndex);
}
// Output devices
readonly property SinkModel paSinkModel: SinkModel { id: paSinkModel }
// Input devices
readonly property SourceModel paSourceModel: SourceModel { id: paSourceModel }
// Confusingly, Sink Input is what PulseAudio calls streams that send audio to an output device
readonly property SinkInputModel paSinkInputModel: SinkInputModel { id: paSinkInputModel }
// Confusingly, Source Output is what PulseAudio calls streams that take audio from an input device
readonly property SourceOutputModel paSourceOutputModel: SourceOutputModel { id: paSourceOutputModel }
// active output devices
readonly property PulseObjectFilterModel paSinkFilterModel: PulseObjectFilterModel {
id: paSinkFilterModel
filterOutInactiveDevices: true
filterVirtualDevices: !volumePage.showVirtualDevices
sourceModel: paSinkModel
}
// active input devices
readonly property PulseObjectFilterModel paSourceFilterModel: PulseObjectFilterModel {
id: paSourceFilterModel
filterOutInactiveDevices: true
filterVirtualDevices: !volumePage.showVirtualDevices
sourceModel: paSourceModel
}
// non-virtual streams going to output devices
readonly property PulseObjectFilterModel paSinkInputFilterModel: PulseObjectFilterModel {
id: paSinkInputFilterModel
filters: [ { role: "VirtualStream", value: false } ]
sourceModel: paSinkInputModel
}
// non-virtual streams coming from input devices
readonly property PulseObjectFilterModel paSourceOutputFilterModel: PulseObjectFilterModel {
id: paSourceOutputFilterModel
filters: [ { role: "VirtualStream", value: false } ]
sourceModel: paSourceOutputModel
}
readonly property CardModel paCardModel: CardModel {
id: paCardModel
function indexOfCardNumber(cardNumber) {
const indexRole = KItemModels.KRoleNames.role("Index");
for (let idx = 0; idx < count; ++idx) {
if (data(index(idx, 0), indexRole) === cardNumber) {
return index(idx, 0);
}
}
return index(-1, 0);
}
}
VolumeFeedback {
id: feedback
}
ColumnLayout {
id: contentItem
spacing: Kirigami.Units.gridUnit
anchors.fill: parent
RowLayout {
//anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
PC3.TabBar {
id: tabBar
Layout.fillWidth: true
//Layout.fillHeight: true
currentIndex: {
switch (volumePage.currentTab) {
case "devices":
return devicesTab.PC3.TabBar.index;
case "streams":
return streamsTab.PC3.TabBar.index;
}
}
KeyNavigation.down: contentView.currentItem.contentItem.upperListView.itemAtIndex(0)
onCurrentIndexChanged: {
switch (currentIndex) {
case devicesTab.PC3.TabBar.index:
volumePage.currentTab = "devices";
break;
case streamsTab.PC3.TabBar.index:
volumePage.currentTab = "streams";
break;
}
}
PC3.TabButton {
id: devicesTab
text: i18n("Devices")
KeyNavigation.up: fullRep.KeyNavigation.up
}
PC3.TabButton {
id: streamsTab
text: i18n("Applications")
KeyNavigation.up: fullRep.KeyNavigation.up
}
}
PC3.ToolButton {
id: globalMuteCheckbox
visible: true// !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading)
icon.name: "audio-volume-muted"
onClicked: {
GlobalService.globalMute()
}
checked: globalMute
Accessible.name: i18n("Force mute all playback devices")
PC3.ToolTip {
text: i18n("Force mute all playback devices")
}
}
PC3.ToolButton {
visible: true // !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading)
icon.name: "configure"
onClicked: KCM.KCMLauncher.openSystemSettings("kcm_pulseaudio")
Accessible.name: i18n("Configure Audio Devices")
PC3.ToolTip {
text: i18n("Configure Audio Devices")
}
}
}
VolumeComponents.HorizontalStackView {
id: contentView
initialItem: volumePage.currentTab === "streams" ? streamsView : devicesView
movementTransitionsEnabled: currentItem !== null
Layout.fillHeight: true
Layout.fillWidth: true
TwoPartView {
id: devicesView
upperModel: paSinkFilterModel
upperType: "sink"
lowerModel: paSourceFilterModel
lowerType: "source"
iconName: "audio-volume-muted"
placeholderText: volumePage.noDevicePlaceholderMessage
upperDelegate: VolumeComponents.DeviceListItem {
width: ListView.view.width
type: devicesView.upperType
}
lowerDelegate: VolumeComponents.DeviceListItem {
width: ListView.view.width
type: devicesView.lowerType
}
}
// NOTE: Don't unload this while dragging and dropping a stream
// to a device or else the D&D operation will be cancelled.
TwoPartView {
id: streamsView
upperModel: paSinkInputFilterModel
upperType: "sink-input"
lowerModel: paSourceOutputFilterModel
lowerType: "source-output"
iconName: "edit-none"
placeholderText: i18n("No applications playing or recording audio")
upperDelegate: VolumeComponents.StreamListItem {
width: ListView.view.width
type: streamsView.upperType
devicesModel: paSinkFilterModel
}
lowerDelegate: VolumeComponents.StreamListItem {
width: ListView.view.width
type: streamsView.lowerType
devicesModel: paSourceFilterModel
}
}
Connections {
target: tabBar
function onCurrentIndexChanged() {
if (tabBar.currentItem === devicesTab) {
contentView.reverseTransitions = false
contentView.replace(devicesView)
} else if (tabBar.currentItem === streamsTab) {
contentView.reverseTransitions = true
contentView.replace(streamsView)
}
}
}
}
component TwoPartView : PC3.ScrollView {
id: scrollView
required property PulseObjectFilterModel upperModel
required property string upperType
required property Component upperDelegate
required property PulseObjectFilterModel lowerModel
required property string lowerType
required property Component lowerDelegate
property string iconName: ""
property string placeholderText: ""
// HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
PC3.ScrollBar.horizontal.policy: PC3.ScrollBar.AlwaysOff
Loader {
//parent: scrollView
anchors.fill: parent
// width: parent.width - Kirigami.Units.gridUnit * 4
active: visible
visible: scrollView.placeholderText.length > 0 && !upperSection.visible && !lowerSection.visible
sourceComponent: PlasmaExtras.PlaceholderMessage {
iconName: scrollView.iconName
text: scrollView.placeholderText
}
}
contentItem: Flickable {
contentHeight: layout.implicitHeight
clip: true
property ListView upperListView: upperSection.visible ? upperSection : lowerSection
property ListView lowerListView: lowerSection.visible ? lowerSection : upperSection
ColumnLayout {
id: layout
anchors.fill: parent
spacing: 0
ListView {
id: upperSection
visible: count //&& !fullRep.hiddenTypes.includes(scrollView.upperType)
interactive: false
Layout.fillWidth: true
implicitHeight: contentHeight
model: scrollView.upperModel
delegate: scrollView.upperDelegate
focus: visible
Keys.onDownPressed: event => {
if (currentIndex < count - 1) {
incrementCurrentIndex();
currentItem.forceActiveFocus();
} else if (lowerSection.visible) {
lowerSection.currentIndex = 0;
lowerSection.currentItem.forceActiveFocus();
} else {
raiseMaximumVolumeCheckbox.forceActiveFocus(Qt.TabFocusReason);
}
event.accepted = true;
}
Keys.onUpPressed: event => {
if (currentIndex > 0) {
decrementCurrentIndex();
currentItem.forceActiveFocus();
} else {
tabBar.currentItem.forceActiveFocus(Qt.BacktabFocusReason);
}
event.accepted = true;
}
}
KSvg.SvgItem {
imagePath: "widgets/line"
elementId: "horizontal-line"
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.smallSpacing * 2
Layout.rightMargin: Layout.leftMargin
Layout.topMargin: Kirigami.Units.smallSpacing
visible: upperSection.visible && lowerSection.visible
}
ListView {
id: lowerSection
visible: count //&& !fullRep.hiddenTypes.includes(scrollView.lowerType)
interactive: false
Layout.fillWidth: true
implicitHeight: contentHeight
model: scrollView.lowerModel
delegate: scrollView.lowerDelegate
focus: visible && !upperSection.visible
Keys.onDownPressed: event => {
if (currentIndex < count - 1) {
incrementCurrentIndex();
currentItem.forceActiveFocus();
} else {
raiseMaximumVolumeCheckbox.forceActiveFocus(Qt.TabFocusReason);
}
event.accepted = true;
}
Keys.onUpPressed: event => {
if (currentIndex > 0) {
decrementCurrentIndex();
currentItem.forceActiveFocus();
} else if (upperSection.visible) {
upperSection.currentIndex = upperSection.count - 1;
upperSection.currentItem.forceActiveFocus();
} else {
tabBar.currentItem.forceActiveFocus(Qt.BacktabFocusReason);
}
event.accepted = true;
}
}
}
}
}
}
}