main.qml 25.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*
    Copyright 2014-2015 Harald Sitter <sitter@kde.org>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
    published by the Free Software Foundation; either version 2 of
    the License or (at your option) version 3 or any later version
    accepted by the membership of KDE e.V. (or its successor approved
    by the membership of KDE e.V.), which shall act as a proxy
    defined in Section 14 of version 3 of the license.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

David Edmundson's avatar
David Edmundson committed
21
import QtQuick 2.2
22 23
import QtQuick.Layouts 1.0

24
import org.kde.plasma.core 2.1 as PlasmaCore
25
import org.kde.plasma.components 2.0 as PlasmaComponents // PC3 TabBar/TabButton need work first
26
import org.kde.plasma.components 3.0 as PlasmaComponents3
27 28
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.plasmoid 2.0
Harald Sitter's avatar
Harald Sitter committed
29

Harald Sitter's avatar
Harald Sitter committed
30
import org.kde.plasma.private.volume 0.1
Harald Sitter's avatar
Harald Sitter committed
31

Harald Sitter's avatar
Harald Sitter committed
32 33
import "../code/icon.js" as Icon

Harald Sitter's avatar
Harald Sitter committed
34
Item {
35
    id: main
36

37
    property bool volumeFeedback: Plasmoid.configuration.volumeFeedback
38
    property bool globalMute: Plasmoid.configuration.globalMute
39 40
    property int currentMaxVolumePercent: plasmoid.configuration.raiseMaximumVolume ? 150 : 100
    property int currentMaxVolumeValue: currentMaxVolumePercent * PulseAudio.NormalVolume / 100.00
41
    property int volumeStep: Math.round(Plasmoid.configuration.volumeStep * PulseAudio.NormalVolume / 100.0)
42
    property string displayName: i18n("Audio Volume")
43
    property QtObject draggedStream: null
44

45 46 47
    // DEFAULT_SINK_NAME in module-always-sink.c
    readonly property string dummyOutputName: "auto_null"

48 49 50 51
    Layout.minimumHeight: PlasmaCore.Units.gridUnit * 8
    Layout.minimumWidth: PlasmaCore.Units.gridUnit * 14
    Layout.preferredHeight: PlasmaCore.Units.gridUnit * 21
    Layout.preferredWidth: PlasmaCore.Units.gridUnit * 24
52 53
    Plasmoid.switchHeight: Layout.minimumHeight
    Plasmoid.switchWidth: Layout.minimumWidth
Harald Sitter's avatar
Harald Sitter committed
54

55 56
    Plasmoid.icon: paSinkModel.preferredSink && !isDummyOutput(paSinkModel.preferredSink) ? Icon.name(paSinkModel.preferredSink.volume, paSinkModel.preferredSink.muted)
                                                                                          : Icon.name(0, true)
57
    Plasmoid.toolTipMainText: {
58
        var sink = paSinkModel.preferredSink;
59
        if (!sink || isDummyOutput(sink)) {
60 61 62 63 64 65 66 67 68
            return displayName;
        }

        if (sink.muted) {
            return i18n("Audio Muted");
        } else {
            return i18n("Volume at %1%", volumePercent(sink.volume));
        }
    }
69 70 71 72 73 74 75 76 77 78
    Plasmoid.toolTipSubText: {
        if (paSinkModel.preferredSink && !isDummyOutput(paSinkModel.preferredSink)) {
            var port = paSinkModel.preferredSink.ports[paSinkModel.preferredSink.activePortIndex];
            if (port) {
                return port.description
            }
            return paSinkModel.preferredSink.name
        }
        return ""
    }
79

80
    function isDummyOutput(output) {
81
        return output && output.name === dummyOutputName;
82 83
    }

David Rosca's avatar
David Rosca committed
84
    function boundVolume(volume) {
85
        return Math.max(PulseAudio.MinimalVolume, Math.min(volume, currentMaxVolumeValue));
86 87
    }

88 89
    function volumePercent(volume) {
        return Math.round(volume / PulseAudio.NormalVolume * 100.0);
90 91
    }

92
    function increaseVolume() {
93
        if (!paSinkModel.preferredSink || isDummyOutput(paSinkModel.preferredSink)) {
94
            return;
95
        }
96
        var volume = boundVolume(paSinkModel.preferredSink.volume + volumeStep);
97
        var percent = volumePercent(volume);
98 99
        paSinkModel.preferredSink.muted = percent == 0;
        paSinkModel.preferredSink.volume = volume;
Sebastian Goth's avatar
Sebastian Goth committed
100
        osd.showVolume(percent);
101
        playFeedback();
102 103
    }

104
    function decreaseVolume() {
105
        if (!paSinkModel.preferredSink || isDummyOutput(paSinkModel.preferredSink)) {
106 107
            return;
        }
108
        var volume = boundVolume(paSinkModel.preferredSink.volume - volumeStep);
109
        var percent = volumePercent(volume);
110 111
        paSinkModel.preferredSink.muted = percent == 0;
        paSinkModel.preferredSink.volume = volume;
Sebastian Goth's avatar
Sebastian Goth committed
112
        osd.showVolume(percent);
113
        playFeedback();
114
    }
115

116
    function muteVolume() {
117
        if (!paSinkModel.preferredSink || isDummyOutput(paSinkModel.preferredSink)) {
118 119
            return;
        }
120
        var toMute = !paSinkModel.preferredSink.muted;
121 122
        if (toMute) {
            enableGlobalMute();
Sebastian Goth's avatar
Sebastian Goth committed
123
            osd.showMute(0);
124 125 126 127 128
        } else {
            if (globalMute) {
                disableGlobalMute();
            }
            paSinkModel.preferredSink.muted = toMute;
129
            osd.showMute(volumePercent(paSinkModel.preferredSink.volume));
Nicolas Fella's avatar
Nicolas Fella committed
130 131
            playFeedback();
        }
132 133
    }

134
    function increaseMicrophoneVolume() {
135
        if (!paSourceModel.defaultSource) {
136 137
            return;
        }
138
        var volume = boundVolume(paSourceModel.defaultSource.volume + volumeStep);
139
        var percent = volumePercent(volume);
140 141
        paSourceModel.defaultSource.muted = percent == 0;
        paSourceModel.defaultSource.volume = volume;
Sebastian Goth's avatar
Sebastian Goth committed
142
        osd.showMic(percent);
143 144 145
    }

    function decreaseMicrophoneVolume() {
146
        if (!paSourceModel.defaultSource) {
147 148
            return;
        }
149
        var volume = boundVolume(paSourceModel.defaultSource.volume - volumeStep);
150
        var percent = volumePercent(volume);
151 152
        paSourceModel.defaultSource.muted = percent == 0;
        paSourceModel.defaultSource.volume = volume;
Sebastian Goth's avatar
Sebastian Goth committed
153
        osd.showMic(percent);
154 155 156
    }

    function muteMicrophone() {
157
        if (!paSourceModel.defaultSource) {
158 159
            return;
        }
160 161
        var toMute = !paSourceModel.defaultSource.muted;
        paSourceModel.defaultSource.muted = toMute;
162
        osd.showMicMute(toMute? 0 : volumePercent(paSourceModel.defaultSource.volume));
163 164
    }

165 166 167 168
    function playFeedback(sinkIndex) {
        if (!volumeFeedback) {
            return;
        }
169
        if (sinkIndex == undefined) {
170
            sinkIndex = paSinkModel.preferredSink.index;
171 172 173 174
        }
        feedback.play(sinkIndex);
    }

Sebastian Goth's avatar
Sebastian Goth committed
175

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
    function enableGlobalMute() {
        var role = paSinkModel.role("Muted");
        var rowCount = paSinkModel.rowCount();
        // List for devices that are already muted. Will use to keep muted after disable GlobalMute.
        var globalMuteDevices = [];

        for (var i = 0; i < rowCount; i++) {
            var idx = paSinkModel.index(i, 0);
            var name = paSinkModel.data(idx, paSinkModel.role("Name"));
            if (paSinkModel.data(idx, role) === false) {
                paSinkModel.setData(idx, true, role);
            } else {
                globalMuteDevices.push(name + "." + paSinkModel.data(idx, paSinkModel.role("ActivePortIndex")));
            }
        }
        // If all the devices were muted, will unmute them all with disable GlobalMute.
        plasmoid.configuration.globalMuteDevices = globalMuteDevices.length < rowCount ? globalMuteDevices : [];
        plasmoid.configuration.globalMute = true;
        globalMute = true;
    }

    function disableGlobalMute() {
        var role = paSinkModel.role("Muted");
        for (var i = 0; i < paSinkModel.rowCount(); i++) {
            var idx = paSinkModel.index(i, 0);
            var name = paSinkModel.data(idx, paSinkModel.role("Name")) + "." + paSinkModel.data(idx, paSinkModel.role("ActivePortIndex"));
            if (plasmoid.configuration.globalMuteDevices.indexOf(name) === -1) {
                paSinkModel.setData(idx, false, role);
            }
        }
        plasmoid.configuration.globalMuteDevices = [];
        plasmoid.configuration.globalMute = false;
        globalMute = false;
    }

211
    SinkModel {
212
        id: paSinkModel
213 214 215 216 217 218 219 220 221 222 223 224 225 226

        property bool initalDefaultSinkIsSet: false

        onDefaultSinkChanged: {
            if (!defaultSink || !plasmoid.configuration.outputChangeOsd) {
                return;
            }

            // avoid showing a OSD on startup
            if (!initalDefaultSinkIsSet) {
                initalDefaultSinkIsSet = true;
                return;
            }

227 228 229 230 231
            var description = defaultSink.description;
            if (isDummyOutput(defaultSink)) {
                description = i18n("No output device");
            }

232
            var icon = Icon.formFactorIcon(defaultSink.formFactor);
233 234
            if (!icon) {
                // Show "muted" icon for Dummy output
235
                if (isDummyOutput(defaultSink)) {
236 237 238 239
                    icon = "audio-volume-muted";
                }
            }

240 241 242
            if (!icon) {
                icon = Icon.name(defaultSink.volume, defaultSink.muted);
            }
243
            osd.showText(icon, description);
244
        }
245 246 247 248 249 250 251 252 253 254 255 256

        onRowsInserted: {
            if (globalMute) {
                var role = paSinkModel.role("Muted");
                for (var i = 0; i < paSinkModel.rowCount(); i++) {
                    var idx = paSinkModel.index(i, 0);
                    if (paSinkModel.data(idx, role) === false) {
                        paSinkModel.setData(idx, true, role);
                    }
                }
            }
        }
257 258
    }

259 260 261 262 263 264 265 266
    PulseObjectFilterModel {
        id: paSinkFilterModel
        sortRole: "SortByDefault"
        sortOrder: Qt.DescendingOrder
        filterOutInactiveDevices: true
        sourceModel: paSinkModel
    }

267 268
    SourceModel {
        id: paSourceModel
269 270
    }

271 272 273 274 275 276 277 278
    PulseObjectFilterModel {
        id: paSourceFilterModel
        sortRole: "SortByDefault"
        sortOrder: Qt.DescendingOrder
        filterOutInactiveDevices: true
        sourceModel: paSourceModel
    }

279
    Plasmoid.compactRepresentation: PlasmaCore.IconItem {
Harald Sitter's avatar
Harald Sitter committed
280
        source: plasmoid.icon
281
        active: mouseArea.containsMouse
Marco Martin's avatar
Marco Martin committed
282
        colorGroup: PlasmaCore.ColorScope.colorGroup
283 284 285 286

        MouseArea {
            id: mouseArea

287
            property int wheelDelta: 0
288 289 290 291
            property bool wasExpanded: false

            anchors.fill: parent
            hoverEnabled: true
Harald Sitter's avatar
Harald Sitter committed
292 293
            acceptedButtons: Qt.LeftButton | Qt.MiddleButton
            onPressed: {
Harald Sitter's avatar
Harald Sitter committed
294 295 296
                if (mouse.button == Qt.LeftButton) {
                    wasExpanded = plasmoid.expanded;
                } else if (mouse.button == Qt.MiddleButton) {
297
                    muteVolume();
Harald Sitter's avatar
Harald Sitter committed
298 299 300 301 302
                }
            }
            onClicked: {
                if (mouse.button == Qt.LeftButton) {
                    plasmoid.expanded = !wasExpanded;
Harald Sitter's avatar
Harald Sitter committed
303 304
                }
            }
305
            onWheel: {
306 307 308
                var delta = wheel.angleDelta.y || wheel.angleDelta.x;
                wheelDelta += delta;
                // Magic number 120 for common "one click"
309
                // See: https://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop
310 311
                while (wheelDelta >= 120) {
                    wheelDelta -= 120;
Harald Sitter's avatar
Harald Sitter committed
312
                    increaseVolume();
313 314 315
                }
                while (wheelDelta <= -120) {
                    wheelDelta += 120;
Harald Sitter's avatar
Harald Sitter committed
316
                    decreaseVolume();
317 318 319 320 321
                }
            }
        }
    }

322
    GlobalActionCollection {
323 324 325 326 327 328 329 330
        // KGlobalAccel cannot transition from kmix to something else, so if
        // the user had a custom shortcut set for kmix those would get lost.
        // To avoid this we hijack kmix name and actions. Entirely mental but
        // best we can do to not cause annoyance for the user.
        // The display name actually is updated to whatever registered last
        // though, so as far as user visible strings go we should be fine.
        // As of 2015-07-21:
        //   componentName: kmix
Harald Sitter's avatar
Harald Sitter committed
331
        //   actions: increase_volume, decrease_volume, mute
332
        name: "kmix"
333
        displayName: main.displayName
334 335 336 337
        GlobalAction {
            objectName: "increase_volume"
            text: i18n("Increase Volume")
            shortcut: Qt.Key_VolumeUp
338
            onTriggered: increaseVolume()
339 340 341 342 343
        }
        GlobalAction {
            objectName: "decrease_volume"
            text: i18n("Decrease Volume")
            shortcut: Qt.Key_VolumeDown
344
            onTriggered: decreaseVolume()
345 346
        }
        GlobalAction {
Rajeesh K V's avatar
Rajeesh K V committed
347
            objectName: "mute"
348 349
            text: i18n("Mute")
            shortcut: Qt.Key_VolumeMute
350
            onTriggered: muteVolume()
351
        }
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
        GlobalAction {
            objectName: "increase_microphone_volume"
            text: i18n("Increase Microphone Volume")
            shortcut: Qt.Key_MicVolumeUp
            onTriggered: increaseMicrophoneVolume()
        }
        GlobalAction {
            objectName: "decrease_microphone_volume"
            text: i18n("Decrease Microphone Volume")
            shortcut: Qt.Key_MicVolumeDown
            onTriggered: decreaseMicrophoneVolume()
        }
        GlobalAction {
            objectName: "mic_mute"
            text: i18n("Mute Microphone")
            shortcut: Qt.Key_MicMute
            onTriggered: muteMicrophone()
        }
370 371
    }

372 373
    VolumeOSD {
        id: osd
Sebastian Goth's avatar
Sebastian Goth committed
374 375 376 377

        function showVolume(text) {
            if (!main.Plasmoid.configuration.volumeOsd)
                return
378
            show(text, currentMaxVolumePercent)
Sebastian Goth's avatar
Sebastian Goth committed
379 380 381 382 383
        }

        function showMute(text) {
            if (!main.Plasmoid.configuration.muteOsd)
                return
384
            show(text, currentMaxVolumePercent)
Sebastian Goth's avatar
Sebastian Goth committed
385 386 387 388 389 390 391 392 393 394 395 396 397
        }

        function showMic(text) {
            if (!main.Plasmoid.configuration.micOsd)
                return
            showMicrophone(text)
        }

        function showMicMute(text) {
            if (!main.Plasmoid.configuration.muteOsd)
                return
            showMicrophone(text)
        }
398 399
    }

400 401 402 403
    VolumeFeedback {
        id: feedback
    }

404 405 406 407 408
    PlasmaCore.Svg {
        id: lineSvg
        imagePath: "widgets/line"
    }

409
    Plasmoid.fullRepresentation: PlasmaComponents3.Page {
410 411
        Layout.preferredHeight: main.Layout.preferredHeight
        Layout.preferredWidth: main.Layout.preferredWidth
David Rosca's avatar
David Rosca committed
412

413 414 415 416 417 418 419
        function beginMoveStream(type, stream) {
            if (type == "sink") {
                sourceView.visible = false;
            } else if (type == "source") {
                sinkView.visible = false;
            }

420
            devicesLine.visible = false;
421 422 423 424 425 426 427
            tabBar.currentTab = devicesTab;
        }

        function endMoveStream() {
            tabBar.currentTab = streamsTab;

            sourceView.visible = true;
428
            devicesLine.visible = true;
429 430 431
            sinkView.visible = true;
        }

432
        header: PlasmaExtras.PlasmoidHeading {
Nate Graham's avatar
Nate Graham committed
433 434 435
            // Make this toolbar's buttons align vertically with the ones above
            rightPadding: -PlasmaCore.Units.devicePixelRatio

436 437
            RowLayout {
                anchors.fill: parent
438

Nate Graham's avatar
Nate Graham committed
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
                PlasmaComponents3.CheckBox {
                    id: raiseMaximumVolumeCheckbox
                    checked: plasmoid.configuration.raiseMaximumVolume
                    onToggled: {
                        plasmoid.configuration.raiseMaximumVolume = checked
                        if (!checked) {
                            for (var i = 0; i < paSinkModel.rowCount(); i++) {
                                if (paSinkModel.data(paSinkModel.index(i, 0), paSinkModel.role("Volume")) > PulseAudio.NormalVolume) {
                                    paSinkModel.setData(paSinkModel.index(i, 0), PulseAudio.NormalVolume, paSinkModel.role("Volume"));
                                }
                            }
                            for (var i = 0; i < paSourceModel.rowCount(); i++) {
                                if (paSourceModel.data(paSourceModel.index(i, 0), paSourceModel.role("Volume")) > PulseAudio.NormalVolume) {
                                    paSourceModel.setData(paSourceModel.index(i, 0), PulseAudio.NormalVolume, paSourceModel.role("Volume"));
                                }
                            }
                        }
                    }
                    text: i18n("Raise maximum volume")
                }

                Item {
461
                    Layout.fillWidth: true
Nate Graham's avatar
Nate Graham committed
462
                }
463

Nate Graham's avatar
Nate Graham committed
464 465 466 467 468 469 470 471 472 473 474 475
                PlasmaComponents3.ToolButton {
                    id: showHiddenDevices
                    icon.name: "view-visible"

                    // Only show if there actually are any inactive devices
                    visible: (paSourceModel.count != paSourceFilterModel.count) || (paSinkModel.count != paSinkFilterModel.count)

                    checkable: true

                    Accessible.name: i18n("show hidden devices")
                    PlasmaComponents3.ToolTip {
                        text: i18n("Show hidden devices")
476
                    }
Nate Graham's avatar
Nate Graham committed
477
                }
478

Nate Graham's avatar
Nate Graham committed
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
                PlasmaComponents3.ToolButton {
                    id: globalMuteCheckbox
                    icon.name: "audio-volume-muted"
                    onClicked: {
                        if (!globalMute) {
                            enableGlobalMute();
                        } else {
                            disableGlobalMute();
                        }
                    }
                    checked: globalMute

                    Accessible.name: i18n("Force mute all playback devices")
                    PlasmaComponents3.ToolTip {
                        text: i18n("Force mute all playback devices")
                    }
                }

                PlasmaComponents3.ToolButton {
                    visible: !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading)

                    icon.name: "configure"
                    onClicked: plasmoid.action("configure").trigger()

                    Accessible.name: plasmoid.action("configure").text
                    PlasmaComponents3.ToolTip {
                        text: plasmoid.action("configure").text
506
                    }
507
                }
508
            }
David Rosca's avatar
David Rosca committed
509 510
        }

511 512
        ColumnLayout {
            anchors.fill: parent
Harald Sitter's avatar
Harald Sitter committed
513

514 515
            PlasmaExtras.ScrollArea {
                id: scrollView
516

517 518
                Layout.fillWidth: true
                Layout.fillHeight: true
519

520 521
                horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff
                flickableItem.boundsBehavior: Flickable.StopAtBounds;
522

523 524
                //our scroll isn't a list of delegates, all internal items are tab focussable, making this redundant
                activeFocusOnTab: false
525

526 527 528
                Item {
                    width: streamsView.visible ? streamsView.width : devicesView.width
                    height: streamsView.visible ? streamsView.height : devicesView.height
529

530 531 532 533 534 535 536
                    ColumnLayout {
                        id: streamsView
                        spacing: 0
                        visible: tabBar.currentTab == streamsTab
                        property int maximumWidth: scrollView.viewport.width
                        width: maximumWidth
                        Layout.maximumWidth: maximumWidth
David Rosca's avatar
David Rosca committed
537

538 539
                        ListView {
                            id: sinkInputView
540

541 542 543 544 545 546 547 548 549 550 551 552 553
                            Layout.fillWidth: true
                            Layout.minimumHeight: contentHeight
                            Layout.maximumHeight: contentHeight

                            model: PulseObjectFilterModel {
                                filters: [ { role: "VirtualStream", value: false } ]
                                sourceModel: SinkInputModel {}
                            }
                            boundsBehavior: Flickable.StopAtBounds;
                            delegate: StreamListItem {
                                type: "sink-input"
                                draggable: sinkView.count > 1
                            }
554
                        }
Harald Sitter's avatar
Harald Sitter committed
555

556 557
                        PlasmaCore.SvgItem {
                            elementId: "horizontal-line"
558
                            Layout.preferredWidth: scrollView.viewport.width - PlasmaCore.Units.smallSpacing * 4
559
                            Layout.preferredHeight: naturalSize.height
560 561 562
                            Layout.leftMargin: PlasmaCore.Units.smallSpacing * 2
                            Layout.rightMargin: PlasmaCore.Units.smallSpacing * 2
                            Layout.topMargin: PlasmaCore.Units.smallSpacing
563 564 565
                            svg: lineSvg
                            visible: sinkInputView.model.count > 0 && sourceOutputView.model.count > 0
                        }
566

567 568
                        ListView {
                            id: sourceOutputView
David Rosca's avatar
David Rosca committed
569

570 571 572
                            Layout.fillWidth: true
                            Layout.minimumHeight: contentHeight
                            Layout.maximumHeight: contentHeight
David Rosca's avatar
David Rosca committed
573

574 575 576 577 578 579 580 581 582
                            model: PulseObjectFilterModel {
                                filters: [ { role: "VirtualStream", value: false } ]
                                sourceModel: SourceOutputModel {}
                            }
                            boundsBehavior: Flickable.StopAtBounds;
                            delegate: StreamListItem {
                                type: "source-input"
                                draggable: sourceView.count > 1
                            }
583
                        }
584
                    }
Harald Sitter's avatar
Harald Sitter committed
585

586 587 588 589 590 591 592
                    ColumnLayout {
                        id: devicesView
                        visible: tabBar.currentTab == devicesTab
                        property int maximumWidth: scrollView.viewport.width
                        width: maximumWidth
                        Layout.maximumWidth: maximumWidth
                        spacing: 0
593

594 595
                        ListView {
                            id: sinkView
David Rosca's avatar
David Rosca committed
596

597 598 599 600
                            Layout.fillWidth: true
                            Layout.minimumHeight: contentHeight
                            Layout.maximumHeight: contentHeight
                            spacing: 0
601

602 603
                            model: showHiddenDevices.checked || !showHiddenDevices.visible ? paSinkModel : paSinkFilterModel

604 605 606 607
                            boundsBehavior: Flickable.StopAtBounds;
                            delegate: DeviceListItem {
                                type: "sink"
                                onlyone: sinkView.count === 1
608
                            }
609 610
                        }

611 612 613
                        PlasmaCore.SvgItem {
                            id: devicesLine
                            elementId: "horizontal-line"
614 615
                            Layout.preferredWidth: scrollView.viewport.width - PlasmaCore.Units.smallSpacing * 4
                            Layout.leftMargin: PlasmaCore.Units.smallSpacing * 2
616
                            Layout.rightMargin: Layout.leftMargin
617
                            Layout.topMargin: PlasmaCore.Units.smallSpacing
618 619 620
                            svg: lineSvg
                            visible: sinkView.model.count > 0 && sourceView.model.count > 0 && (sinkView.model.count > 1 || sourceView.model.count > 1)
                        }
621

622 623
                        ListView {
                            id: sourceView
624

625 626 627
                            Layout.fillWidth: true
                            Layout.minimumHeight: contentHeight
                            Layout.maximumHeight: contentHeight
628

629 630
                            model: showHiddenDevices.checked || !showHiddenDevices.visible ? paSourceModel : paSourceFilterModel

631 632 633 634 635
                            boundsBehavior: Flickable.StopAtBounds;
                            delegate: DeviceListItem {
                                type: "source"
                                onlyone: sourceView.count === 1
                            }
636
                        }
637
                    }
638

639 640 641 642 643 644 645 646 647 648 649
                    PlasmaExtras.Heading {
                        level: 4
                        enabled: false
                        width: parent.width
                        height: scrollView.height
                        visible: streamsView.visible && !sinkInputView.count && !sourceOutputView.count
                        text: i18n("No applications playing or recording audio")
                        wrapMode: Text.WordWrap
                        verticalAlignment: Text.AlignVCenter
                        horizontalAlignment: Text.AlignHCenter
                    }
650

651 652 653 654 655 656 657 658 659 660 661
                    PlasmaExtras.Heading {
                        level: 4
                        enabled: false
                        width: parent.width
                        height: scrollView.height
                        visible: devicesView.visible && !sinkView.count && !sourceView.count
                        text: i18n("No output or input devices found")
                        wrapMode: Text.WordWrap
                        verticalAlignment: Text.AlignVCenter
                        horizontalAlignment: Text.AlignHCenter
                    }
662
                }
663
            }
Harald Sitter's avatar
Harald Sitter committed
664
        }
665

666 667
        footer: PlasmaExtras.PlasmoidHeading {
            location: PlasmaExtras.PlasmoidHeading.Location.Footer
Nate Graham's avatar
Nate Graham committed
668 669 670
            // Allow tabbar to touch the footer's top border
            topPadding: -topInset

671 672 673
            RowLayout {
                anchors.fill: parent

Nate Graham's avatar
Nate Graham committed
674 675
                PlasmaComponents.TabBar {
                    id: tabBar
676
                    Layout.fillWidth: true
Nate Graham's avatar
Nate Graham committed
677 678
                    activeFocusOnTab: true
                    tabPosition: Qt.BottomEdge
679

Nate Graham's avatar
Nate Graham committed
680 681 682
                    PlasmaComponents.TabButton {
                        id: devicesTab
                        text: i18n("Devices")
683
                    }
684

Nate Graham's avatar
Nate Graham committed
685 686 687
                    PlasmaComponents.TabButton {
                        id: streamsTab
                        text: i18n("Applications")
688
                    }
689
                }
690 691
            }
        }
Harald Sitter's avatar
Harald Sitter committed
692
    }
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
693 694 695 696

    Component.onCompleted: {
        MicrophoneIndicator.init();
    }
Harald Sitter's avatar
Harald Sitter committed
697
}