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 }