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 }