Warning, /plasma/plasma-pa/src/kcm/ui/main.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
0003     SPDX-FileCopyrightText: 2016 David Rosca <nowrep@gmail.com>
0004     SPDX-FileCopyrightText: 2019 Sefa Eyeoglu <contact@scrumplex.net>
0005     SPDX-FileCopyrightText: 2020 Nicolas Fella <nicolas.fella@gmx.de>
0006     SPDX-FileCopyrightText: 2021 Ismael Asensio <isma.af@gmail.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0009 */
0010 
0011 import QtQuick
0012 import QtQuick.Layouts
0013 import QtQuick.Controls
0014 
0015 import org.kde.coreaddons as KCoreAddons
0016 import org.kde.kcmutils as KCM
0017 import org.kde.kitemmodels as KItemModels
0018 import org.kde.kirigami as Kirigami
0019 import org.kde.kirigamiaddons.components as KirigamiComponents
0020 import org.kde.plasma.private.volume
0021 
0022 KCM.ScrollViewKCM {
0023 
0024     property int maxVolumeValue: PulseAudio.NormalVolume // the applet supports changing this value. We will just assume 65536 (100%)
0025 
0026     implicitHeight: Kirigami.Units.gridUnit * 28
0027     implicitWidth: Kirigami.Units.gridUnit * 28
0028 
0029     GlobalConfig {
0030         id: config
0031     }
0032 
0033     SinkModel {
0034         id: paSinkModel
0035     }
0036 
0037     SourceModel {
0038         id: paSourceModel
0039     }
0040 
0041     CardModel {
0042         id: paCardModel
0043 
0044         function indexOfCardNumber(cardNumber) {
0045             for (var idx = 0; idx < count; idx++) {
0046                 if (data(index(idx, 0), KItemModels.KRoleNames.role("Index")) == cardNumber) {
0047                     return index(idx, 0);
0048                 }
0049             }
0050             return index(-1, 0);
0051         }
0052     }
0053 
0054     PulseObjectFilterModel {
0055         id: paSinkFilterModel
0056 
0057         sortRoleName: "SortByDefault"
0058         sortOrder: Qt.DescendingOrder
0059         filterOutInactiveDevices: true
0060         sourceModel: paSinkModel
0061     }
0062 
0063     PulseObjectFilterModel {
0064         id: paSourceFilterModel
0065 
0066         sortRoleName: "SortByDefault"
0067         sortOrder: Qt.DescendingOrder
0068         filterOutInactiveDevices: true
0069         sourceModel: paSourceModel
0070     }
0071 
0072     ModuleManager {
0073         id: moduleManager
0074     }
0075 
0076     actions: [
0077         Kirigami.Action {
0078             id: inactiveDevicesSwitch
0079             icon.name: "view-visible"
0080             visible: (paSourceModel.count != paSourceFilterModel.count) || (paSinkModel.count != paSinkFilterModel.count)
0081 
0082             displayComponent: Switch {
0083                 text: i18nd("kcm_pulseaudio", "Show Inactive Devices")
0084 
0085                 onToggled: inactiveDevicesSwitch.checked = checked
0086             }
0087         },
0088         Kirigami.Action {
0089             icon.name: "configure"
0090             text: i18nd("kcm_pulseaudio", "Configure Volume Controlsā€¦")
0091             onTriggered: kcm.push("VolumeControlsConfig.qml", { "config": config })
0092         },
0093         Kirigami.Action {
0094             id: configureButton
0095             visible: moduleManager.settingsSupported
0096             enabled: moduleManager.configModuleLoaded
0097             text: i18nd("kcm_pulseaudio", "Configureā€¦")
0098             icon.name: "configure"
0099 
0100             tooltip: i18nd("kcm_pulseaudio", "Requires %1 PulseAudio module", moduleManager.configModuleName)
0101 
0102             Kirigami.Action {
0103                 text: i18nd("kcm_pulseaudio", "Add virtual output device for simultaneous output on all local sound cards")
0104                 checkable: true
0105                 checked: moduleManager.combineSinks
0106                 onToggled: moduleManager.combineSinks = checked
0107             }
0108             Kirigami.Action {
0109                 text: i18nd("kcm_pulseaudio", "Automatically switch all running streams when a new output becomes available")
0110                 checkable: true
0111                 checked: moduleManager.switchOnConnect
0112                 onToggled: moduleManager.switchOnConnect = checked
0113             }
0114         }
0115     ]
0116 
0117     view: Flickable {
0118         id: flickable
0119 
0120         contentWidth: column.width
0121         contentHeight: column.height
0122         clip: true
0123 
0124         ColumnLayout {
0125             id: column
0126             width: flickable.width
0127 
0128             // Only show labels if both port/profile comboboxes are visible
0129             readonly property var comboBoxLabelsVisible: {
0130                 for (var i = 0; i < sinks.count; ++i) {
0131                     let delegate = sinks.itemAtIndex(i)
0132                     if (delegate != null && delegate.portVisible)
0133                         return true
0134                 }
0135                 for (var i = 0; i < sources.count; ++i) {
0136                     let delegate = sources.itemAtIndex(i)
0137                     if (delegate != null && delegate.portVisible)
0138                         return true
0139                 }
0140                 return false
0141             }
0142 
0143             Kirigami.ListSectionHeader {
0144                 Layout.fillWidth: true
0145                 visible: sinks.visible
0146                 text: i18nd("kcm_pulseaudio", "Playback Devices")
0147             }
0148 
0149             ListView {
0150                 id: sinks
0151                 visible: count > 0
0152                 Layout.fillWidth: true
0153                 Layout.leftMargin: Kirigami.Units.largeSpacing
0154                 Layout.rightMargin: Kirigami.Units.largeSpacing
0155                 Layout.topMargin: Kirigami.Units.smallSpacing
0156                 Layout.bottomMargin: Kirigami.Units.smallSpacing
0157                 Layout.preferredHeight: contentHeight
0158                 interactive: false
0159                 spacing: Kirigami.Units.largeSpacing
0160                 model: inactiveDevicesSwitch.checked || !inactiveDevicesSwitch.visible ? paSinkModel : paSinkFilterModel
0161                 delegate: DeviceListItem {
0162                     isPlayback: true
0163                     comboBoxLabelsVisible: column.comboBoxLabelsVisible
0164                 }
0165             }
0166 
0167             Kirigami.ListSectionHeader {
0168                 Layout.fillWidth: true
0169                 visible: sources.visible
0170                 text: i18nd("kcm_pulseaudio", "Recording Devices")
0171             }
0172 
0173             ListView {
0174                 id: sources
0175                 visible: count > 0
0176                 Layout.fillWidth: true
0177                 Layout.leftMargin: Kirigami.Units.largeSpacing
0178                 Layout.rightMargin: Kirigami.Units.largeSpacing
0179                 Layout.topMargin: Kirigami.Units.smallSpacing
0180                 Layout.bottomMargin: Kirigami.Units.smallSpacing
0181                 Layout.preferredHeight: contentHeight
0182                 interactive: false
0183                 spacing: Kirigami.Units.largeSpacing
0184                 model: inactiveDevicesSwitch.checked || !inactiveDevicesSwitch.visible ? paSourceModel : paSourceFilterModel
0185                 delegate: DeviceListItem {
0186                     isPlayback: false
0187                     comboBoxLabelsVisible: column.comboBoxLabelsVisible
0188                 }
0189             }
0190 
0191             Kirigami.ListSectionHeader {
0192                 Layout.fillWidth: true
0193                 visible: inactiveCards.visible
0194                 text: i18nd("kcm_pulseaudio", "Inactive Cards")
0195             }
0196 
0197             ListView {
0198                 id: inactiveCards
0199                 visible: count > 0
0200                 Layout.fillWidth: true
0201                 Layout.leftMargin: Kirigami.Units.largeSpacing
0202                 Layout.rightMargin: Kirigami.Units.largeSpacing
0203                 Layout.topMargin: Kirigami.Units.smallSpacing
0204                 Layout.bottomMargin: Kirigami.Units.smallSpacing
0205                 Layout.preferredHeight: contentHeight
0206                 interactive: false
0207                 spacing: Kirigami.Units.largeSpacing
0208                 model: KItemModels.KSortFilterProxyModel {
0209                     sourceModel: paCardModel
0210                     filterRowCallback: function(source_row, source_parent) {
0211                         let idx = sourceModel.index(source_row, 0);
0212                         let profiles = sourceModel.data(idx, sourceModel.KItemModels.KRoleNames.role("Profiles"))
0213                         let activeProfileIndex = sourceModel.data(idx, sourceModel.KItemModels.KRoleNames.role("ActiveProfileIndex"))
0214                         return profiles[activeProfileIndex].name == "off";
0215                     }
0216                 }
0217                 delegate: CardListItem {
0218                     comboBoxLabelsVisible: column.comboBoxLabelsVisible
0219                 }
0220             }
0221 
0222             Kirigami.ListSectionHeader {
0223                 Layout.fillWidth: true
0224                 visible: eventStreamView.visible || sinkInputView.visible
0225                 text: i18nd("kcm_pulseaudio", "Playback Streams")
0226             }
0227 
0228             ListView {
0229                 id: eventStreamView
0230                 visible: count > 0
0231                 Layout.fillWidth: true
0232                 Layout.leftMargin: Kirigami.Units.largeSpacing
0233                 Layout.rightMargin: Kirigami.Units.largeSpacing
0234                 Layout.topMargin: Kirigami.Units.smallSpacing
0235                 // Only have a bottom margin if the ListView beneath is empty and not visible
0236                 Layout.bottomMargin: sinkInputView.visible ? 0 : Kirigami.Units.smallSpacing
0237                 Layout.preferredHeight: contentHeight
0238                 interactive: false
0239                 spacing: Kirigami.Units.largeSpacing
0240                 model: PulseObjectFilterModel {
0241                     filters: [ { role: "Name", value: "sink-input-by-media-role:event" } ]
0242                     sourceModel: StreamRestoreModel {}
0243                 }
0244                 delegate: StreamListItem {
0245                     deviceModel: paSinkModel
0246                     isPlayback: true
0247                 }
0248             }
0249 
0250             ListView {
0251                 id: sinkInputView
0252                 visible: count > 0
0253                 Layout.fillWidth: true
0254                 Layout.leftMargin: Kirigami.Units.largeSpacing
0255                 Layout.rightMargin: Kirigami.Units.largeSpacing
0256                 Layout.topMargin: Kirigami.Units.smallSpacing
0257                 Layout.bottomMargin: Kirigami.Units.smallSpacing
0258                 Layout.preferredHeight: contentHeight
0259                 interactive: false
0260                 spacing: Kirigami.Units.largeSpacing
0261                 model: PulseObjectFilterModel {
0262                     filters: [ { role: "VirtualStream", value: false } ]
0263                     sourceModel: SinkInputModel {}
0264                 }
0265                 delegate: StreamListItem {
0266                     deviceModel: paSinkModel
0267                     isPlayback: true
0268                 }
0269             }
0270 
0271             Kirigami.ListSectionHeader {
0272                 Layout.fillWidth: true
0273                 visible: sourceOutputView.visible
0274                 text: i18nd("kcm_pulseaudio", "Recording Streams")
0275             }
0276 
0277             ListView {
0278                 id: sourceOutputView
0279                 visible: count > 0
0280                 Layout.fillWidth: true
0281                 Layout.leftMargin: Kirigami.Units.largeSpacing
0282                 Layout.rightMargin: Kirigami.Units.largeSpacing
0283                 Layout.topMargin: Kirigami.Units.smallSpacing
0284                 Layout.bottomMargin: Kirigami.Units.smallSpacing
0285                 Layout.preferredHeight: contentHeight
0286                 interactive: false
0287                 spacing: Kirigami.Units.largeSpacing
0288                 model: PulseObjectFilterModel {
0289                     filters: [ { role: "VirtualStream", value: false } ]
0290                     sourceModel: SourceOutputModel {}
0291                 }
0292 
0293                 delegate: StreamListItem {
0294                     deviceModel: sourceModel
0295                     isPlayback: false
0296                 }
0297             }
0298         }
0299     }
0300 
0301     SpeakerTest {
0302         id: tester
0303         sink: testOverlay.sinkObject
0304 
0305         onShowErrorMessage: message => {
0306             testError.text = i18ndc("kcm_pulseaudio",
0307                                     "%1 is an error string produced by an external component, and probably untranslated",
0308                                     "Error trying to play a test sound. \nThe system said: \"%1\"", message);
0309             testError.visible = true;
0310         }
0311     }
0312 
0313     Kirigami.OverlaySheet {
0314         id: testOverlay
0315 
0316         property var sinkObject: null
0317         property string description: ""
0318         property string iconName: "audio-card"
0319         property string profile: ""
0320         property string port: ""
0321 
0322         function testSink(index) {
0323             let modelIndex = sinks.model.index(Math.max(index, 0), 0);
0324             sinkObject = sinks.model.data(modelIndex, sinks.model.KItemModels.KRoleNames.role("PulseObject"));
0325             description = sinks.model.data(modelIndex, sinks.model.KItemModels.KRoleNames.role("Description"));
0326             iconName = sinks.model.data(modelIndex, sinks.model.KItemModels.KRoleNames.role("IconName")) || "audio-card";
0327 
0328             let ports = sinks.model.data(modelIndex, sinks.model.KItemModels.KRoleNames.role("Ports"));
0329             port = ports.length > 1 ? ports[sinks.model.data(modelIndex, sinks.model.KItemModels.KRoleNames.role("ActivePortIndex"))].description : "";
0330 
0331             profile = "";
0332             let cardIndex = paCardModel.indexOfCardNumber(sinks.model.data(modelIndex, sinks.model.KItemModels.KRoleNames.role("CardIndex")));
0333             if (cardIndex.valid) {
0334                 let profiles = paCardModel.data(cardIndex, paCardModel.KItemModels.KRoleNames.role("Profiles")) || [];
0335                 profile = profiles.length > 1 ? profiles[paCardModel.data(cardIndex, paCardModel.KItemModels.KRoleNames.role("ActiveProfileIndex"))].description : "";
0336             }
0337 
0338             testOverlay.open();
0339         }
0340 
0341         header: GridLayout {
0342             columns: 2
0343             rowSpacing: Kirigami.Units.smallSpacing
0344 
0345             Kirigami.Icon {
0346                 source: testOverlay.iconName || "audio-card"
0347                 Layout.rowSpan: 3
0348                 Layout.alignment: Qt.AlignCenter
0349             }
0350             Label {
0351                 text: testOverlay.description
0352                 font.bold: true
0353                 Layout.fillWidth: true
0354                 wrapMode: Text.WordWrap
0355             }
0356             Label {
0357                 text: {
0358                     if (testOverlay.port.length === 0) { return testOverlay.profile }
0359                     if (testOverlay.profile.length === 0) { return testOverlay.port }
0360                     return testOverlay.profile + " / " + testOverlay.port
0361                 }
0362                 visible: text.length > 0
0363                 Layout.fillWidth: true
0364                 elide: Text.ElideRight
0365             }
0366         }
0367 
0368         ColumnLayout {
0369             Kirigami.InlineMessage {
0370                 id: testError
0371                 type: Kirigami.MessageType.Error
0372                 showCloseButton: true
0373                 Layout.fillWidth: true
0374             }
0375 
0376             GridLayout {
0377                 columns: 3
0378                 rowSpacing: Kirigami.Units.gridUnit
0379 
0380                 LayoutMirroring.enabled: false  // To preserve spacial layout on RTL
0381 
0382                 KirigamiComponents.Avatar {
0383                     KCoreAddons.KUser {
0384                         id: kuser
0385                     }
0386                     source: kuser.faceIconUrl
0387                     name: kuser.fullName
0388                     implicitWidth: Kirigami.Units.gridUnit * 4
0389                     implicitHeight: implicitWidth
0390 
0391                     Layout.row: 1
0392                     Layout.column: 1
0393                     Layout.alignment: Qt.AlignCenter
0394                 }
0395 
0396                 Repeater {
0397                     id: channelRepeater
0398 
0399                     model: testOverlay.sinkObject && testOverlay.sinkObject.rawChannels
0400 
0401                     delegate: ToolButton {
0402                         readonly property bool isPlaying: tester.playingChannels.includes(modelData)
0403                         readonly property var channelData: {
0404                             switch (modelData) {
0405                                 case "front-left": return {text: i18nd("kcm_pulseaudio", "Front Left"), row: 0, column: 0, angle: 45};
0406                                 case "front-center": return {text: i18nd("kcm_pulseaudio", "Front Center"), row: 0, column: 1, angle: 90};
0407                                 case "front-right": return {text: i18nd("kcm_pulseaudio", "Front Right"), row: 0, column: 2, angle: 135};
0408                                 case "side-left": return {text: i18nd("kcm_pulseaudio", "Side Left"), row: 1, column: 0, angle: 0};
0409                                 case "side-right": return {text: i18nd("kcm_pulseaudio", "Side Right"), row: 1, column: 2, angle: 180};
0410                                 case "rear-left": return {text: i18nd("kcm_pulseaudio", "Rear Left"), row: 2, column: 0, angle: -45};
0411                                 case "lfe": return {text: i18nd("kcm_pulseaudio", "Subwoofer"), row: 2, column: 1, angle: -90};
0412                                 case "rear-right": return {text: i18nd("kcm_pulseaudio", "Rear Right"), row: 2, column: 2, angle: -135};
0413                                 case "mono" : return {text: i18nd("kcm_pulseaudio", "Mono"), row: 0, column: 1, angle: 90};
0414                             }
0415                             // We have a non-standard channel name
0416                             return {
0417                                 text: modelData,
0418                                 row: 3 + Math.floor(index / 3),
0419                                 // To keep the avatar centered in case of just 1 or 2 non-standard channels
0420                                 column: (channelRepeater.count < 3 && index === channelRepeater.count - 1) ? index + 1 : index % 3,
0421                                 angle: 0
0422                             }
0423                         }
0424 
0425                         Layout.row: channelData.row
0426                         Layout.column: channelData.column
0427                         Layout.fillWidth: true
0428                         Layout.fillHeight: true
0429                         Layout.preferredWidth: Kirigami.Units.gridUnit * 8
0430                         Layout.minimumHeight: Kirigami.Units.gridUnit * 4
0431 
0432                         contentItem: ColumnLayout {
0433                             spacing: 0
0434 
0435                             Kirigami.Icon {
0436                                 source: ":/kcm/kcm_pulseaudio/icons/audio-speakers-symbolic.svg"
0437                                 isMask: true
0438                                 color: isPlaying ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
0439                                 implicitWidth: Kirigami.Units.iconSizes.medium
0440                                 implicitHeight: Kirigami.Units.iconSizes.medium
0441                                 Layout.fillWidth: true
0442                                 Layout.margins: Kirigami.Units.smallSpacing
0443                                 rotation: channelData.angle
0444                             }
0445 
0446                             Label {
0447                                 text: channelData.text
0448                                 color: isPlaying ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
0449                                 Layout.fillWidth: true
0450                                 Layout.fillHeight: true
0451                                 Layout.margins: Kirigami.Units.smallSpacing
0452                                 horizontalAlignment: Text.AlignHCenter
0453                                 verticalAlignment: Text.AlignTop
0454                                 wrapMode: Text.WordWrap
0455                             }
0456                         }
0457 
0458                         onClicked: {
0459                             testError.visible = false;
0460                             tester.testChannel(modelData);
0461                         }
0462                     }
0463                 }
0464             }
0465 
0466             Label {
0467                 text: i18nd("kcm_pulseaudio", "Click on any speaker to test sound")
0468                 font: Kirigami.Theme.smallFont
0469                 Layout.alignment: Qt.AlignCenter
0470                 Layout.topMargin: Kirigami.Units.gridUnit
0471             }
0472         }
0473     }
0474 }