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