Warning, /plasma/plasma-pa/applet/contents/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 0004 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 import QtQuick 0008 import QtQuick.Layouts 0009 0010 import org.kde.plasma.core as PlasmaCore 0011 import org.kde.ksvg as KSvg 0012 import org.kde.kirigami as Kirigami 0013 import org.kde.kitemmodels as KItemModels 0014 import org.kde.plasma.components as PC3 0015 import org.kde.plasma.extras as PlasmaExtras 0016 import org.kde.plasma.plasmoid 0017 0018 import org.kde.kcmutils as KCMUtils 0019 import org.kde.config as KConfig 0020 0021 import org.kde.plasma.private.volume 0022 0023 import "../code/icon.js" as Icon 0024 0025 PlasmoidItem { 0026 id: main 0027 0028 GlobalConfig { 0029 id: config 0030 } 0031 0032 property bool volumeFeedback: config.audioFeedback 0033 property bool globalMute: config.globalMute 0034 property int currentMaxVolumePercent: config.raiseMaximumVolume ? 150 : 100 0035 property int currentMaxVolumeValue: currentMaxVolumePercent * PulseAudio.NormalVolume / 100.00 0036 property int volumePercentStep: config.volumeStep 0037 property string displayName: i18n("Audio Volume") 0038 property QtObject draggedStream: null 0039 0040 Connections { 0041 target: paSinkModel.preferredSink 0042 function onVolumeChanged() { 0043 osd.showVolume(volumePercent(paSinkModel.preferredSink.volume)) 0044 } 0045 } 0046 0047 property bool showVirtualDevices: Plasmoid.configuration.showVirtualDevices 0048 0049 // DEFAULT_SINK_NAME in module-always-sink.c 0050 readonly property string dummyOutputName: "auto_null" 0051 readonly property string noDevicePlaceholderMessage: i18n("No output or input devices found") 0052 0053 switchHeight: Layout.minimumHeight 0054 switchWidth: Layout.minimumWidth 0055 0056 Plasmoid.icon: paSinkModel.preferredSink && !isDummyOutput(paSinkModel.preferredSink) ? Icon.name(paSinkModel.preferredSink.volume, paSinkModel.preferredSink.muted) 0057 : Icon.name(0, true) 0058 toolTipMainText: { 0059 var sink = paSinkModel.preferredSink; 0060 if (!sink || isDummyOutput(sink)) { 0061 return displayName; 0062 } 0063 0064 if (sink.muted) { 0065 return i18n("Audio Muted"); 0066 } else { 0067 return i18n("Volume at %1%", volumePercent(sink.volume)); 0068 } 0069 } 0070 toolTipSubText: { 0071 let lines = []; 0072 0073 if (paSinkModel.preferredSink && paSinkFilterModel.count > 1 && !isDummyOutput(paSinkModel.preferredSink)) { 0074 lines.push(nodeName(paSinkModel.preferredSink)) 0075 } 0076 0077 if (paSinkFilterModel.count > 0) { 0078 lines.push(main.globalMute ? i18n("Middle-click to unmute") 0079 : i18n("Middle-click to mute all audio")); 0080 lines.push(i18n("Scroll to adjust volume")); 0081 } else { 0082 lines.push(main.noDevicePlaceholderMessage); 0083 } 0084 return lines.join("\n"); 0085 } 0086 0087 function nodeName(pulseObject) { 0088 const nodeNick = pulseObject.pulseProperties["node.nick"] 0089 if (nodeNick) { 0090 return nodeNick 0091 } 0092 0093 if (pulseObject.description) { 0094 return pulseObject.description 0095 } 0096 0097 if (pulseObject.name) { 0098 return pulseObject.name 0099 } 0100 0101 return i18n("Device name not found") 0102 } 0103 0104 function isDummyOutput(output) { 0105 return output && output.name === dummyOutputName; 0106 } 0107 0108 function boundVolume(volume) { 0109 return Math.max(PulseAudio.MinimalVolume, Math.min(volume, currentMaxVolumeValue)); 0110 } 0111 0112 function volumePercent(volume) { 0113 return Math.round(volume / PulseAudio.NormalVolume * 100.0); 0114 } 0115 0116 // Increment a VolumeObject (Sink or Source) by %volume. 0117 function changeVolumeByPercent(volumeObject, deltaPercent) { 0118 const oldVolume = volumeObject.volume; 0119 const oldPercent = volumePercent(oldVolume); 0120 const targetPercent = oldPercent + deltaPercent; 0121 const newVolume = boundVolume(Math.round(PulseAudio.NormalVolume * (targetPercent/100))); 0122 const newPercent = volumePercent(newVolume); 0123 volumeObject.muted = newPercent == 0; 0124 volumeObject.volume = newVolume; 0125 return newPercent; 0126 } 0127 0128 // Increment the preferredSink by %volume. 0129 function changeSpeakerVolume(deltaPercent) { 0130 if (!paSinkModel.preferredSink || isDummyOutput(paSinkModel.preferredSink)) { 0131 return; 0132 } 0133 const newPercent = changeVolumeByPercent(paSinkModel.preferredSink, deltaPercent); 0134 osd.showVolume(newPercent); 0135 playFeedback(); 0136 } 0137 0138 function increaseVolume(modifiers) { 0139 if (globalMute) { 0140 disableGlobalMute(); 0141 } 0142 changeSpeakerVolume((modifiers & Qt.ShiftModifier) ? 1 : volumePercentStep); 0143 } 0144 0145 function decreaseVolume(modifiers) { 0146 if (globalMute) { 0147 disableGlobalMute(); 0148 } 0149 changeSpeakerVolume((modifiers & Qt.ShiftModifier) ? -1 : -volumePercentStep); 0150 } 0151 0152 function muteVolume() { 0153 if (!paSinkModel.preferredSink || isDummyOutput(paSinkModel.preferredSink)) { 0154 return; 0155 } 0156 var toMute = !paSinkModel.preferredSink.muted; 0157 if (toMute) { 0158 enableGlobalMute(); 0159 osd.showMute(0); 0160 } else { 0161 if (globalMute) { 0162 disableGlobalMute(); 0163 } 0164 paSinkModel.preferredSink.muted = toMute; 0165 osd.showMute(volumePercent(paSinkModel.preferredSink.volume)); 0166 playFeedback(); 0167 } 0168 } 0169 0170 // Increment the defaultSource by %volume. 0171 function changeMicrophoneVolume(deltaPercent) { 0172 if (!paSourceModel.defaultSource) { 0173 return; 0174 } 0175 const newPercent = changeVolumeByPercent(paSourceModel.defaultSource, deltaPercent); 0176 osd.showMic(newPercent); 0177 } 0178 0179 function increaseMicrophoneVolume() { 0180 changeMicrophoneVolume(volumePercentStep); 0181 } 0182 0183 function decreaseMicrophoneVolume() { 0184 changeMicrophoneVolume(-volumePercentStep); 0185 } 0186 0187 function muteMicrophone() { 0188 if (!paSourceModel.defaultSource) { 0189 return; 0190 } 0191 var toMute = !paSourceModel.defaultSource.muted; 0192 paSourceModel.defaultSource.muted = toMute; 0193 osd.showMicMute(toMute? 0 : volumePercent(paSourceModel.defaultSource.volume)); 0194 } 0195 0196 function playFeedback(sinkIndex) { 0197 if (!volumeFeedback) { 0198 return; 0199 } 0200 if (sinkIndex == undefined) { 0201 sinkIndex = paSinkModel.preferredSink.index; 0202 } 0203 feedback.play(sinkIndex); 0204 } 0205 0206 0207 function enableGlobalMute() { 0208 var role = paSinkModel.KItemModels.KRoleNames.role("Muted"); 0209 var rowCount = paSinkModel.rowCount(); 0210 // List for devices that are already muted. Will use to keep muted after disable GlobalMute. 0211 var globalMuteDevices = []; 0212 0213 for (var i = 0; i < rowCount; i++) { 0214 var idx = paSinkModel.index(i, 0); 0215 var name = paSinkModel.data(idx, paSinkModel.KItemModels.KRoleNames.role("Name")); 0216 if (paSinkModel.data(idx, role) === false) { 0217 paSinkModel.setData(idx, true, role); 0218 } else { 0219 globalMuteDevices.push(name + "." + paSinkModel.data(idx, paSinkModel.KItemModels.KRoleNames.role("ActivePortIndex"))); 0220 } 0221 } 0222 // If all the devices were muted, will unmute them all with disable GlobalMute. 0223 config.globalMuteDevices = globalMuteDevices.length < rowCount ? globalMuteDevices : []; 0224 config.globalMute = true; 0225 config.save(); 0226 } 0227 0228 function disableGlobalMute() { 0229 var role = paSinkModel.KItemModels.KRoleNames.role("Muted"); 0230 for (var i = 0; i < paSinkModel.rowCount(); i++) { 0231 var idx = paSinkModel.index(i, 0); 0232 var name = paSinkModel.data(idx, paSinkModel.KItemModels.KRoleNames.role("Name")) + "." + paSinkModel.data(idx, paSinkModel.KItemModels.KRoleNames.role("ActivePortIndex")); 0233 if (config.globalMuteDevices.indexOf(name) === -1) { 0234 paSinkModel.setData(idx, false, role); 0235 } 0236 } 0237 config.globalMuteDevices = []; 0238 config.globalMute = false; 0239 config.save(); 0240 } 0241 0242 // Output devices 0243 readonly property SinkModel paSinkModel: SinkModel { 0244 id: paSinkModel 0245 0246 property bool initalDefaultSinkIsSet: false 0247 0248 onDefaultSinkChanged: { 0249 if (!defaultSink || !config.defaultOutputDeviceOsd) { 0250 return; 0251 } 0252 0253 // avoid showing a OSD on startup 0254 if (!initalDefaultSinkIsSet) { 0255 initalDefaultSinkIsSet = true; 0256 return; 0257 } 0258 0259 var description = nodeName(defaultSink); 0260 if (isDummyOutput(defaultSink)) { 0261 description = i18n("No output device"); 0262 } else { 0263 const cardModelIdx = paCardModel.indexOfCardNumber(defaultSink.cardIndex); 0264 if (cardModelIdx.valid) { 0265 const cardProperties = paCardModel.data(cardModelIdx, paCardModel.KItemModels.KRoleNames.role("Properties")); 0266 const cardBluetoothBattery = cardProperties["bluetooth.battery"]; 0267 // This property is returned as a string with percent sign, 0268 // parse it into an int in case they change it to a number later. 0269 const batteryInt = parseInt(cardBluetoothBattery, 10); 0270 0271 if (!isNaN(batteryInt)) { 0272 description = i18nc("Device name (Battery percent)", "%1 (%2% Battery)", description, batteryInt); 0273 } 0274 } 0275 } 0276 0277 var icon = Icon.formFactorIcon(defaultSink.formFactor); 0278 if (!icon) { 0279 // Show "muted" icon for Dummy output 0280 if (isDummyOutput(defaultSink)) { 0281 icon = "audio-volume-muted"; 0282 } 0283 } 0284 0285 if (!icon) { 0286 icon = Icon.name(defaultSink.volume, defaultSink.muted); 0287 } 0288 osd.showText(icon, description); 0289 } 0290 0291 onRowsInserted: { 0292 if (globalMute) { 0293 var role = paSinkModel.KItemModels.KRoleNames.role("Muted"); 0294 for (var i = 0; i < paSinkModel.rowCount(); i++) { 0295 var idx = paSinkModel.index(i, 0); 0296 if (paSinkModel.data(idx, role) === false) { 0297 paSinkModel.setData(idx, true, role); 0298 } 0299 } 0300 } 0301 } 0302 } 0303 0304 // Input devices 0305 readonly property SourceModel paSourceModel: SourceModel { id: paSourceModel } 0306 0307 // Confusingly, Sink Input is what PulseAudio calls streams that send audio to an output device 0308 readonly property SinkInputModel paSinkInputModel: SinkInputModel { id: paSinkInputModel } 0309 0310 // Confusingly, Source Output is what PulseAudio calls streams that take audio from an input device 0311 readonly property SourceOutputModel paSourceOutputModel: SourceOutputModel { id: paSourceOutputModel } 0312 0313 // active output devices 0314 readonly property PulseObjectFilterModel paSinkFilterModel: PulseObjectFilterModel { 0315 id: paSinkFilterModel 0316 sortRoleName: "SortByDefault" 0317 sortOrder: Qt.DescendingOrder 0318 filterOutInactiveDevices: true 0319 filterVirtualDevices: !main.showVirtualDevices 0320 sourceModel: paSinkModel 0321 } 0322 0323 // active input devices 0324 readonly property PulseObjectFilterModel paSourceFilterModel: PulseObjectFilterModel { 0325 id: paSourceFilterModel 0326 sortRoleName: "SortByDefault" 0327 sortOrder: Qt.DescendingOrder 0328 filterOutInactiveDevices: true 0329 filterVirtualDevices: !main.showVirtualDevices 0330 sourceModel: paSourceModel 0331 } 0332 0333 // non-virtual streams going to output devices 0334 readonly property PulseObjectFilterModel paSinkInputFilterModel: PulseObjectFilterModel { 0335 id: paSinkInputFilterModel 0336 filters: [ { role: "VirtualStream", value: false } ] 0337 sourceModel: paSinkInputModel 0338 } 0339 0340 // non-virtual streams coming from input devices 0341 readonly property PulseObjectFilterModel paSourceOutputFilterModel: PulseObjectFilterModel { 0342 id: paSourceOutputFilterModel 0343 filters: [ { role: "VirtualStream", value: false } ] 0344 sourceModel: paSourceOutputModel 0345 } 0346 0347 readonly property CardModel paCardModel: CardModel { 0348 id: paCardModel 0349 0350 function indexOfCardNumber(cardNumber) { 0351 const indexRole = KItemModels.KRoleNames.role("Index"); 0352 for (let idx = 0; idx < count; ++idx) { 0353 if (data(index(idx, 0), indexRole) === cardNumber) { 0354 return index(idx, 0); 0355 } 0356 } 0357 return index(-1, 0); 0358 } 0359 } 0360 0361 // Only exists because the default CompactRepresentation doesn't expose: 0362 // - scroll actions 0363 // - a middle-click action 0364 // TODO remove once it gains those features. 0365 compactRepresentation: MouseArea { 0366 property int wheelDelta: 0 0367 property bool wasExpanded: false 0368 0369 anchors.fill: parent 0370 hoverEnabled: true 0371 acceptedButtons: Qt.LeftButton | Qt.MiddleButton 0372 onPressed: mouse => { 0373 if (mouse.button == Qt.LeftButton) { 0374 wasExpanded = main.expanded; 0375 } else if (mouse.button == Qt.MiddleButton) { 0376 muteVolume(); 0377 } 0378 } 0379 onClicked: mouse => { 0380 if (mouse.button == Qt.LeftButton) { 0381 main.expanded = !wasExpanded; 0382 } 0383 } 0384 onWheel: wheel => { 0385 const delta = (wheel.inverted ? -1 : 1) * (wheel.angleDelta.y ? wheel.angleDelta.y : -wheel.angleDelta.x); 0386 wheelDelta += delta; 0387 // Magic number 120 for common "one click" 0388 // See: https://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop 0389 while (wheelDelta >= 120) { 0390 wheelDelta -= 120; 0391 increaseVolume(wheel.modifiers); 0392 } 0393 while (wheelDelta <= -120) { 0394 wheelDelta += 120; 0395 decreaseVolume(wheel.modifiers); 0396 } 0397 } 0398 Kirigami.Icon { 0399 anchors.fill: parent 0400 source: plasmoid.icon 0401 active: parent.containsMouse 0402 } 0403 } 0404 0405 GlobalActionCollection { 0406 // KGlobalAccel cannot transition from kmix to something else, so if 0407 // the user had a custom shortcut set for kmix those would get lost. 0408 // To avoid this we hijack kmix name and actions. Entirely mental but 0409 // best we can do to not cause annoyance for the user. 0410 // The display name actually is updated to whatever registered last 0411 // though, so as far as user visible strings go we should be fine. 0412 // As of 2015-07-21: 0413 // componentName: kmix 0414 // actions: increase_volume, decrease_volume, mute 0415 name: "kmix" 0416 displayName: main.displayName 0417 GlobalAction { 0418 objectName: "increase_volume" 0419 text: i18n("Increase Volume") 0420 shortcut: Qt.Key_VolumeUp 0421 onTriggered: increaseVolume(Qt.NoModifier) 0422 } 0423 GlobalAction { 0424 objectName: "increase_volume_small" 0425 text: i18nc("@action shortcut", "Increase Volume by 1%") 0426 shortcut: Qt.ShiftModifier | Qt.Key_VolumeUp 0427 onTriggered: increaseVolume(Qt.ShiftModifier) 0428 } 0429 GlobalAction { 0430 objectName: "decrease_volume" 0431 text: i18n("Decrease Volume") 0432 shortcut: Qt.Key_VolumeDown 0433 onTriggered: decreaseVolume(Qt.NoModifier) 0434 } 0435 GlobalAction { 0436 objectName: "decrease_volume_small" 0437 text: i18nc("@action shortcut", "Decrease Volume by 1%") 0438 shortcut: Qt.ShiftModifier | Qt.Key_VolumeDown 0439 onTriggered: decreaseVolume(Qt.ShiftModifier) 0440 } 0441 GlobalAction { 0442 objectName: "mute" 0443 text: i18n("Mute") 0444 shortcut: Qt.Key_VolumeMute 0445 onTriggered: muteVolume() 0446 } 0447 GlobalAction { 0448 objectName: "increase_microphone_volume" 0449 text: i18n("Increase Microphone Volume") 0450 shortcut: Qt.Key_MicVolumeUp 0451 onTriggered: increaseMicrophoneVolume() 0452 } 0453 GlobalAction { 0454 objectName: "decrease_microphone_volume" 0455 text: i18n("Decrease Microphone Volume") 0456 shortcut: Qt.Key_MicVolumeDown 0457 onTriggered: decreaseMicrophoneVolume() 0458 } 0459 GlobalAction { 0460 objectName: "mic_mute" 0461 text: i18n("Mute Microphone") 0462 shortcuts: [Qt.Key_MicMute, Qt.MetaModifier | Qt.Key_VolumeMute] 0463 onTriggered: muteMicrophone() 0464 } 0465 } 0466 0467 VolumeOSD { 0468 id: osd 0469 0470 function showVolume(text) { 0471 if (!config.volumeOsd) { 0472 return 0473 } 0474 show(text, currentMaxVolumePercent) 0475 } 0476 0477 function showMute(text) { 0478 if (!config.muteOsd) { 0479 return 0480 } 0481 show(text, currentMaxVolumePercent) 0482 } 0483 0484 function showMic(text) { 0485 if (!config.microphoneSensitivityOsd) { 0486 return 0487 } 0488 showMicrophone(text) 0489 } 0490 0491 function showMicMute(text) { 0492 if (!config.muteOsd) { 0493 return 0494 } 0495 showMicrophone(text) 0496 } 0497 } 0498 0499 VolumeFeedback { 0500 id: feedback 0501 } 0502 0503 fullRepresentation: PlasmaExtras.Representation { 0504 id: fullRep 0505 0506 Layout.minimumHeight: Kirigami.Units.gridUnit * 8 0507 Layout.minimumWidth: Kirigami.Units.gridUnit * 14 0508 Layout.preferredHeight: Kirigami.Units.gridUnit * 21 0509 Layout.preferredWidth: Kirigami.Units.gridUnit * 24 0510 0511 collapseMarginsHint: true 0512 0513 KeyNavigation.down: tabBar.currentItem 0514 0515 function beginMoveStream(type, stream) { 0516 if (type === "sink") { 0517 contentView.hiddenTypes = "source" 0518 } else if (type === "source") { 0519 contentView.hiddenTypes = "sink" 0520 } 0521 tabBar.setCurrentIndex(devicesTab.PC3.TabBar.index) 0522 } 0523 0524 function endMoveStream() { 0525 contentView.hiddenTypes = [] 0526 tabBar.setCurrentIndex(streamsTab.PC3.TabBar.index) 0527 } 0528 0529 header: PlasmaExtras.PlasmoidHeading { 0530 // Make this toolbar's buttons align vertically with the ones above 0531 rightPadding: -1 0532 // Allow tabbar to touch the header's bottom border 0533 bottomPadding: -bottomInset 0534 0535 RowLayout { 0536 anchors.fill: parent 0537 spacing: Kirigami.Units.smallSpacing 0538 0539 PC3.TabBar { 0540 id: tabBar 0541 Layout.fillWidth: true 0542 Layout.fillHeight: true 0543 0544 currentIndex: { 0545 switch (plasmoid.configuration.currentTab) { 0546 case "devices": 0547 return devicesTab.PC3.TabBar.index; 0548 case "streams": 0549 return streamsTab.PC3.TabBar.index; 0550 } 0551 } 0552 0553 KeyNavigation.down: contentView.currentItem.contentItem.upperListView.itemAtIndex(0) 0554 0555 onCurrentIndexChanged: { 0556 switch (currentIndex) { 0557 case devicesTab.PC3.TabBar.index: 0558 plasmoid.configuration.currentTab = "devices"; 0559 break; 0560 case streamsTab.PC3.TabBar.index: 0561 plasmoid.configuration.currentTab = "streams"; 0562 break; 0563 } 0564 } 0565 0566 PC3.TabButton { 0567 id: devicesTab 0568 text: i18n("Devices") 0569 0570 KeyNavigation.up: fullRep.KeyNavigation.up 0571 } 0572 0573 PC3.TabButton { 0574 id: streamsTab 0575 text: i18n("Applications") 0576 0577 KeyNavigation.up: fullRep.KeyNavigation.up 0578 } 0579 } 0580 0581 PC3.ToolButton { 0582 id: globalMuteCheckbox 0583 0584 visible: !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading) 0585 0586 icon.name: "audio-volume-muted" 0587 onClicked: { 0588 if (!globalMute) { 0589 enableGlobalMute(); 0590 } else { 0591 disableGlobalMute(); 0592 } 0593 } 0594 checked: globalMute 0595 0596 Accessible.name: i18n("Force mute all playback devices") 0597 PC3.ToolTip { 0598 text: i18n("Force mute all playback devices") 0599 } 0600 } 0601 0602 PC3.ToolButton { 0603 visible: !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading) 0604 0605 icon.name: "configure" 0606 onClicked: plasmoid.internalAction("configure").trigger() 0607 0608 Accessible.name: plasmoid.internalAction("configure").text 0609 PC3.ToolTip { 0610 text: plasmoid.internalAction("configure").text 0611 } 0612 } 0613 } 0614 } 0615 0616 // NOTE: using a StackView instead of a SwipeView partly because 0617 // SwipeView would never start with the correct initial view when the 0618 // last saved view was the streams view. 0619 // We also don't need to be able to swipe between views. 0620 contentItem: HorizontalStackView { 0621 id: contentView 0622 property var hiddenTypes: [] 0623 initialItem: plasmoid.configuration.currentTab === "streams" ? streamsView : devicesView 0624 movementTransitionsEnabled: currentItem !== null 0625 TwoPartView { 0626 id: devicesView 0627 upperModel: paSinkFilterModel 0628 upperType: "sink" 0629 lowerModel: paSourceFilterModel 0630 lowerType: "source" 0631 iconName: "audio-volume-muted" 0632 placeholderText: main.noDevicePlaceholderMessage 0633 upperDelegate: DeviceListItem { 0634 width: ListView.view.width 0635 type: devicesView.upperType 0636 } 0637 lowerDelegate: DeviceListItem { 0638 width: ListView.view.width 0639 type: devicesView.lowerType 0640 } 0641 } 0642 // NOTE: Don't unload this while dragging and dropping a stream 0643 // to a device or else the D&D operation will be cancelled. 0644 TwoPartView { 0645 id: streamsView 0646 upperModel: paSinkInputFilterModel 0647 upperType: "sink-input" 0648 lowerModel: paSourceOutputFilterModel 0649 lowerType: "source-output" 0650 iconName: "edit-none" 0651 placeholderText: i18n("No applications playing or recording audio") 0652 upperDelegate: StreamListItem { 0653 width: ListView.view.width 0654 type: streamsView.upperType 0655 devicesModel: paSinkFilterModel 0656 } 0657 lowerDelegate: StreamListItem { 0658 width: ListView.view.width 0659 type: streamsView.lowerType 0660 devicesModel: paSourceFilterModel 0661 } 0662 } 0663 Connections { 0664 target: tabBar 0665 function onCurrentIndexChanged() { 0666 if (tabBar.currentItem === devicesTab) { 0667 contentView.reverseTransitions = false 0668 contentView.replace(devicesView) 0669 } else if (tabBar.currentItem === streamsTab) { 0670 contentView.reverseTransitions = true 0671 contentView.replace(streamsView) 0672 } 0673 } 0674 } 0675 } 0676 0677 component TwoPartView : PC3.ScrollView { 0678 id: scrollView 0679 required property PulseObjectFilterModel upperModel 0680 required property string upperType 0681 required property Component upperDelegate 0682 required property PulseObjectFilterModel lowerModel 0683 required property string lowerType 0684 required property Component lowerDelegate 0685 property string iconName: "" 0686 property string placeholderText: "" 0687 0688 // HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890 0689 PC3.ScrollBar.horizontal.policy: PC3.ScrollBar.AlwaysOff 0690 0691 Loader { 0692 parent: scrollView 0693 anchors.centerIn: parent 0694 width: parent.width - Kirigami.Units.gridUnit * 4 0695 active: visible 0696 visible: scrollView.placeholderText.length > 0 && !upperSection.visible && !lowerSection.visible 0697 sourceComponent: PlasmaExtras.PlaceholderMessage { 0698 iconName: scrollView.iconName 0699 text: scrollView.placeholderText 0700 } 0701 } 0702 contentItem: Flickable { 0703 contentHeight: layout.implicitHeight 0704 clip: true 0705 0706 property ListView upperListView: upperSection.visible ? upperSection : lowerSection 0707 property ListView lowerListView: lowerSection.visible ? lowerSection : upperSection 0708 0709 ColumnLayout { 0710 id: layout 0711 width: parent.width 0712 spacing: 0 0713 ListView { 0714 id: upperSection 0715 visible: count && !contentView.hiddenTypes.includes(scrollView.upperType) 0716 interactive: false 0717 Layout.fillWidth: true 0718 implicitHeight: contentHeight 0719 model: scrollView.upperModel 0720 delegate: scrollView.upperDelegate 0721 focus: visible 0722 0723 Keys.onDownPressed: event => { 0724 if (currentIndex < count - 1) { 0725 incrementCurrentIndex(); 0726 currentItem.forceActiveFocus(); 0727 } else if (lowerSection.visible) { 0728 lowerSection.currentIndex = 0; 0729 lowerSection.currentItem.forceActiveFocus(); 0730 } else { 0731 raiseMaximumVolumeCheckbox.forceActiveFocus(Qt.TabFocusReason); 0732 } 0733 event.accepted = true; 0734 } 0735 Keys.onUpPressed: event => { 0736 if (currentIndex > 0) { 0737 decrementCurrentIndex(); 0738 currentItem.forceActiveFocus(); 0739 } else { 0740 tabBar.currentItem.forceActiveFocus(Qt.BacktabFocusReason); 0741 } 0742 event.accepted = true; 0743 } 0744 } 0745 KSvg.SvgItem { 0746 imagePath: "widgets/line" 0747 elementId: "horizontal-line" 0748 Layout.fillWidth: true 0749 Layout.leftMargin: Kirigami.Units.smallSpacing * 2 0750 Layout.rightMargin: Layout.leftMargin 0751 Layout.topMargin: Kirigami.Units.smallSpacing 0752 visible: upperSection.visible && lowerSection.visible 0753 } 0754 ListView { 0755 id: lowerSection 0756 visible: count && !contentView.hiddenTypes.includes(scrollView.lowerType) 0757 interactive: false 0758 Layout.fillWidth: true 0759 implicitHeight: contentHeight 0760 model: scrollView.lowerModel 0761 delegate: scrollView.lowerDelegate 0762 focus: visible && !upperSection.visible 0763 0764 Keys.onDownPressed: event => { 0765 if (currentIndex < count - 1) { 0766 incrementCurrentIndex(); 0767 currentItem.forceActiveFocus(); 0768 } else { 0769 raiseMaximumVolumeCheckbox.forceActiveFocus(Qt.TabFocusReason); 0770 } 0771 event.accepted = true; 0772 } 0773 Keys.onUpPressed: event => { 0774 if (currentIndex > 0) { 0775 decrementCurrentIndex(); 0776 currentItem.forceActiveFocus(); 0777 } else if (upperSection.visible) { 0778 upperSection.currentIndex = upperSection.count - 1; 0779 upperSection.currentItem.forceActiveFocus(); 0780 } else { 0781 tabBar.currentItem.forceActiveFocus(Qt.BacktabFocusReason); 0782 } 0783 event.accepted = true; 0784 } 0785 } 0786 } 0787 } 0788 } 0789 0790 footer: PlasmaExtras.PlasmoidHeading { 0791 height: fullRep.header.height 0792 PC3.Switch { 0793 id: raiseMaximumVolumeCheckbox 0794 anchors.left: parent.left 0795 anchors.leftMargin: Kirigami.Units.smallSpacing 0796 anchors.verticalCenter: parent.verticalCenter 0797 0798 checked: config.raiseMaximumVolume 0799 0800 Accessible.onPressAction: raiseMaximumVolumeCheckbox.toggle() 0801 KeyNavigation.backtab: contentView.currentItem.contentItem.lowerListView.itemAtIndex(contentView.currentItem.contentItem.lowerListView.count - 1) 0802 Keys.onUpPressed: event => { 0803 KeyNavigation.backtab.forceActiveFocus(Qt.BacktabFocusReason); 0804 } 0805 0806 text: i18n("Raise maximum volume") 0807 0808 onToggled: { 0809 config.raiseMaximumVolume = checked; 0810 config.save(); 0811 } 0812 } 0813 } 0814 } 0815 0816 Plasmoid.contextualActions: [ 0817 PlasmaCore.Action { 0818 text: i18n("Force mute all playback devices") 0819 icon.name: "audio-volume-muted" 0820 checkable: true 0821 checked: globalMute 0822 onTriggered: { 0823 if (!globalMute) { 0824 enableGlobalMute(); 0825 } else { 0826 disableGlobalMute(); 0827 } 0828 } 0829 }, 0830 PlasmaCore.Action { 0831 text: i18n("Show virtual devices") 0832 icon.name: "audio-card" 0833 checkable: true 0834 checked: plasmoid.configuration.showVirtualDevices 0835 onTriggered: Plasmoid.configuration.showVirtualDevices = !Plasmoid.configuration.showVirtualDevices 0836 } 0837 ] 0838 0839 PlasmaCore.Action { 0840 id: configureAction 0841 text: i18n("&Configure Audio Devices…") 0842 icon.name: "configure" 0843 shortcut: "alt+d, s" 0844 visible: KConfig.KAuthorized.authorizeControlModule("kcm_pulseaudio") 0845 onTriggered: KCMUtils.KCMLauncher.openSystemSettings("kcm_pulseaudio") 0846 } 0847 0848 Component.onCompleted: { 0849 MicrophoneIndicator.init(); 0850 Plasmoid.setInternalAction("configure", configureAction); 0851 0852 // migrate settings if they aren't default 0853 // this needs to be done per instance of the applet 0854 if (Plasmoid.configuration.migrated) { 0855 return; 0856 } 0857 if (Plasmoid.configuration.volumeFeedback === false && config.audioFeedback) { 0858 config.audioFeedback = false; 0859 config.save(); 0860 } 0861 if (Plasmoid.configuration.volumeStep && Plasmoid.configuration.volumeStep !== 5 && config.volumeStep === 5) { 0862 config.volumeStep = Plasmoid.configuration.volumeStep; 0863 config.save(); 0864 } 0865 if (Plasmoid.configuration.raiseMaximumVolume === true && !config.raiseMaximumVolume) { 0866 config.raiseMaximumVolume = true; 0867 config.save(); 0868 } 0869 if (Plasmoid.configuration.volumeOsd === false && config.volumeOsd) { 0870 config.volumeOsd = false; 0871 config.save(); 0872 } 0873 if (Plasmoid.configuration.muteOsd === false && config.muteOsd) { 0874 config.muteOsd = false; 0875 config.save(); 0876 } 0877 if (Plasmoid.configuration.micOsd === false && config.microphoneSensitivityOsd) { 0878 config.microphoneSensitivityOsd = false; 0879 config.save(); 0880 } 0881 if (Plasmoid.configuration.globalMute === true && !config.globalMute) { 0882 config.globalMute = true; 0883 config.save(); 0884 } 0885 if (Plasmoid.configuration.globalMuteDevices.length !== 0) { 0886 for (const device in Plasmoid.configuration.globalMuteDevices) { 0887 if (!config.globalMuteDevices.includes(device)) { 0888 config.globalMuteDevices.push(device); 0889 } 0890 } 0891 config.save(); 0892 } 0893 Plasmoid.configuration.migrated = true; 0894 } 0895 }