diff --git a/containments/panel/package/contents/ui/indicators/providers/VolumeProvider.qml b/containments/panel/package/contents/ui/indicators/providers/VolumeProvider.qml index 47c7871c..cecac648 100644 --- a/containments/panel/package/contents/ui/indicators/providers/VolumeProvider.qml +++ b/containments/panel/package/contents/ui/indicators/providers/VolumeProvider.qml @@ -12,6 +12,8 @@ import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.private.volume 0.1 +import "../../volumeosd" + QtObject { property bool isVisible: paSinkModel.preferredSink && paSinkModel.preferredSink.muted property string icon: paSinkModel.preferredSink && !isDummyOutput(paSinkModel.preferredSink) @@ -23,6 +25,10 @@ QtObject { property int volumeStep: Math.round(5 * PulseAudio.NormalVolume / 100.0) readonly property string dummyOutputName: "auto_null" + function showVolumeOverlay() { + osd.showOverlay(); + } + function iconName(volume, muted, prefix) { if (!prefix) { prefix = "audio-volume"; @@ -57,10 +63,10 @@ QtObject { } function playFeedback(sinkIndex) { - if(!volumeFeedback){ + if (!volumeFeedback){ return; } - if(sinkIndex == undefined) { + if (sinkIndex == undefined) { sinkIndex = paSinkModel.preferredSink.index; } feedback.play(sinkIndex) @@ -75,7 +81,8 @@ QtObject { var percent = volumePercent(volume, maxVolumeValue); paSinkModel.preferredSink.muted = percent == 0; paSinkModel.preferredSink.volume = volume; - osd.show(percent); + osd.volume = percent; + osd.showOverlay(); playFeedback(); } @@ -89,7 +96,8 @@ QtObject { var percent = volumePercent(volume, maxVolumeValue); paSinkModel.preferredSink.muted = percent == 0; paSinkModel.preferredSink.volume = volume; - osd.show(percent); + osd.volume = percent; + osd.showOverlay(); playFeedback(); } @@ -102,15 +110,37 @@ QtObject { var toMute = !paSinkModel.preferredSink.muted; paSinkModel.preferredSink.muted = toMute; - osd.show(toMute ? 0 : volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue)); + + osd.volume = toMute ? 0 : volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue); + osd.showOverlay(); + if (!toMute) { playFeedback(); } } + + property var updateVolume: Connections { + target: paSinkModel.preferredSink + + function onVolumeChanged() { + var percent = volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue); + osd.volume = percent; + } + } + property var updateVolumeOnSinkChange: Connections { + target: paSinkModel + + function onPreferredSinkChanged() { + if (paSinkModel.preferredSink) { + var percent = volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue); + osd.volume = percent; + } + } + } property SinkModel paSinkModel: SinkModel {} - property VolumeOSD osd: VolumeOSD {} + property VolumeOsd osd: VolumeOsd {} property VolumeFeedback feedback: VolumeFeedback {} diff --git a/containments/panel/package/contents/ui/main.qml b/containments/panel/package/contents/ui/main.qml index c2c45df6..56025db8 100644 --- a/containments/panel/package/contents/ui/main.qml +++ b/containments/panel/package/contents/ui/main.qml @@ -74,6 +74,7 @@ Item { fullContainer.applet = applet; fullContainer.contentItem = applet.fullRepresentationItem; //applet.fullRepresentationItem.anchors.fill = fullContainer; + } Component.onCompleted: { @@ -233,7 +234,7 @@ Item { onCloseRequested: slidingPanel.close() } - // notifications + // notifications and media player ListView { id: fullRepresentationView implicitHeight: units.gridUnit * 20 diff --git a/containments/panel/package/contents/ui/quicksettings/QuickSettingsModel.qml b/containments/panel/package/contents/ui/quicksettings/QuickSettingsModel.qml index b31c8c3b..1942feff 100644 --- a/containments/panel/package/contents/ui/quicksettings/QuickSettingsModel.qml +++ b/containments/panel/package/contents/ui/quicksettings/QuickSettingsModel.qml @@ -106,6 +106,10 @@ Item { root.closeRequested(); } + function openVolumeOsd() { + volumeProvider.showVolumeOverlay(); + } + // initialize quick settings Component.onCompleted: { //NOTE: add all in javascript as the static decl of listelements can't have scripts @@ -150,7 +154,7 @@ Item { "icon": "audio-speakers-symbolic", "enabled": false, "settingsCommand": "plasma-settings -m kcm_pulseaudio", - "toggleFunction": "" + "toggleFunction": "openVolumeOsd" }); settingsModel.append({ "text": i18n("Flashlight"), diff --git a/containments/panel/package/contents/ui/volumeosd/AudioApplet.qml b/containments/panel/package/contents/ui/volumeosd/AudioApplet.qml new file mode 100644 index 00000000..21da0158 --- /dev/null +++ b/containments/panel/package/contents/ui/volumeosd/AudioApplet.qml @@ -0,0 +1,203 @@ +/* + * SPDX-FileCopyrightText: 2014-2015 Harald Sitter + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.12 + +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtra +import org.kde.plasma.plasmoid 2.0 +import org.kde.kquickcontrolsaddons 2.0 as KQCAddons + +import org.kde.plasma.private.volume 0.1 + +// adapted version of https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/main.qml + +// most audio functions are in VolumeProvider.qml (which will be a parent) +// capture presses on the audio applet so it doesn't close the overlay +ColumnLayout { + spacing: 0 + + // pulseaudio models + + function isDummyOutput(output) { + return output && output.name === dummyOutputName; + } + + SinkModel { + id: paSinkModel + } + + PulseObjectFilterModel { + id: paSinkFilterModel + sortRole: "SortByDefault" + sortOrder: Qt.DescendingOrder + filterOutInactiveDevices: true + sourceModel: paSinkModel + } + + SourceModel { + id: paSourceModel + } + + PulseObjectFilterModel { + id: paSourceFilterModel + sortRole: "SortByDefault" + sortOrder: Qt.DescendingOrder + filterOutInactiveDevices: true + sourceModel: paSourceModel + } + + CardModel { + id: paCardModel + } + + // ui elements + + PopupCard { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: PlasmaCore.Units.largeSpacing + contentItem: ColumnLayout { + anchors.rightMargin: PlasmaCore.Units.smallSpacing + anchors.leftMargin: PlasmaCore.Units.smallSpacing + + PlasmaExtra.Heading { + level: 2 + text: i18n("Outputs") + Layout.fillWidth: true + Layout.leftMargin: PlasmaCore.Units.smallSpacing + } + + Repeater { + id: sinkView + Layout.fillWidth: true + + model: paSinkFilterModel + delegate: DeviceListItem { + Layout.fillWidth: true + type: "sink" + onlyone: sinkView.count === 1 + } + } + } + } + + PopupCard { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: PlasmaCore.Units.largeSpacing + contentItem: ColumnLayout { + anchors.rightMargin: PlasmaCore.Units.smallSpacing + anchors.leftMargin: PlasmaCore.Units.smallSpacing + + PlasmaExtra.Heading { + level: 2 + text: i18n("Inputs") + Layout.fillWidth: true + Layout.leftMargin: PlasmaCore.Units.smallSpacing + } + + Repeater { + id: sourceView + Layout.fillWidth: true + + model: paSourceFilterModel + delegate: DeviceListItem { + Layout.fillWidth: true + type: "source" + onlyone: sinkView.count === 1 + } + } + } + } + + PopupCard { + visible: sourceInputView.model.count + sourceMediaInputView.model.count !== 0 + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: PlasmaCore.Units.largeSpacing + contentItem: ColumnLayout { + anchors.rightMargin: PlasmaCore.Units.smallSpacing + anchors.leftMargin: PlasmaCore.Units.smallSpacing + + PlasmaExtra.Heading { + level: 2 + text: i18n("Playback Streams") + Layout.fillWidth: true + Layout.leftMargin: PlasmaCore.Units.smallSpacing + } + + Repeater { + id: sourceMediaInputView + Layout.fillWidth: true + + model: PulseObjectFilterModel { + filters: [ { role: "Name", value: "sink-input-by-media-role:event" } ] + sourceModel: StreamRestoreModel {} + } + delegate: StreamListItem { + Layout.fillWidth: true + width: sourceOutputView.width + type: "sink-input" + devicesModel: sourceView.model + } + } + + Repeater { + id: sourceInputView + Layout.fillWidth: true + + model: PulseObjectFilterModel { + filters: [ { role: "VirtualStream", value: false } ] + sourceModel: SinkInputModel {} + } + + delegate: StreamListItem { + Layout.fillWidth: true + width: sourceOutputView.width + type: "sink-input" + devicesModel: sourceView.model + } + } + } + } + + PopupCard { + visible: sourceOutputView.model.count !== 0 + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: PlasmaCore.Units.largeSpacing + contentItem: ColumnLayout { + anchors.rightMargin: PlasmaCore.Units.smallSpacing + anchors.leftMargin: PlasmaCore.Units.smallSpacing + + PlasmaExtra.Heading { + level: 2 + text: i18n("Recording Streams") + Layout.fillWidth: true + Layout.leftMargin: PlasmaCore.Units.smallSpacing + } + + Repeater { + id: sourceOutputView + Layout.fillWidth: true + + model: PulseObjectFilterModel { + filters: [ { role: "VirtualStream", value: false } ] + sourceModel: SourceOutputModel {} + } + delegate: StreamListItem { + Layout.fillWidth: true + width: sourceOutputView.width + type: "source-output" + devicesModel: sourceView.model + } + } + } + } +} diff --git a/containments/panel/package/contents/ui/volumeosd/DeviceListItem.qml b/containments/panel/package/contents/ui/volumeosd/DeviceListItem.qml new file mode 100644 index 00000000..3a51f2f2 --- /dev/null +++ b/containments/panel/package/contents/ui/volumeosd/DeviceListItem.qml @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Harald Sitter + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.0 + +import org.kde.plasma.private.volume 0.1 + +// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/DeviceListItem.qml +ListItemBase { + readonly property var currentPort: Ports[ActivePortIndex] + readonly property var currentActivePortIndex: ActivePortIndex + readonly property var currentMuted: Muted + readonly property var activePortIndex: ActivePortIndex + property bool onlyone: false + + label: { + if (currentPort && currentPort.description) { + if (onlyone || !Description) { + return currentPort.description; + } else { + return i18nc("label of device items", "%1 (%2)", currentPort.description, Description); + } + } + if (Description) { + return Description; + } + if (Name) { + return Name; + } + return i18n("Device name not found"); + } +} diff --git a/containments/panel/package/contents/ui/volumeosd/ListItemBase.qml b/containments/panel/package/contents/ui/volumeosd/ListItemBase.qml new file mode 100644 index 00000000..d39ce38b --- /dev/null +++ b/containments/panel/package/contents/ui/volumeosd/ListItemBase.qml @@ -0,0 +1,271 @@ +/* + * SPDX-FileCopyrightText: 2014-2015 Harald Sitter + * SPDX-FileCopyrightText: 2019 Sefa Eyeoglu + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.12 + +import org.kde.kquickcontrolsaddons 2.0 +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtra +import org.kde.plasma.private.volume 0.1 + +import org.kde.kirigami 2.12 as Kirigami + +import "icon.js" as Icon + +// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/ListItemBase.qml +Kirigami.SwipeListItem { + id: baseItem + + property string label + property alias listIcon: clientIcon.source + property alias iconUsesPlasmaTheme: clientIcon.usesPlasmaTheme + property string type // sink, source, source-output + + topPadding: PlasmaCore.Units.smallSpacing + bottomPadding: PlasmaCore.Units.smallSpacing + leftPadding: PlasmaCore.Units.smallSpacing + rightPadding: PlasmaCore.Units.smallSpacing + + alwaysVisibleActions: true + + backgroundColor: "transparent" // we use panel background, no need for the same colour to be on top + activeBackgroundColor: selectButton.visible ? PlasmaCore.Theme.highlightColor : "transparent" + + onClicked: { + if (selectButton.visible) { + model.PulseObject.default = true; + } + } + + contentItem: RowLayout { + id: row + spacing: PlasmaCore.Units.smallSpacing + + PlasmaComponents.RadioButton { + id: selectButton + Layout.alignment: Qt.AlignTop + Layout.topMargin: Math.round(row.height / 2 - implicitHeight - PlasmaCore.Units.smallSpacing / 2) // align with text + checked: model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false + visible: (baseItem.type == "sink" && sinkView.model.count > 1) || (baseItem.type == "source" && sourceView.model.count > 1) + onClicked: model.PulseObject.default = true + } + + // application icon + PlasmaCore.IconItem { + id: clientIcon + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: PlasmaCore.Units.smallSpacing + Layout.preferredWidth: PlasmaCore.Units.iconSizes.smallMedium + Layout.preferredHeight: PlasmaCore.Units.iconSizes.smallMedium + visible: type === "sink-input" || type === "source-output" + source: "unknown" + onSourceChanged: { + if (!valid && source != "unknown") { + source = "unknown"; + } + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + spacing: PlasmaCore.Units.smallSpacing + + RowLayout { + Layout.fillWidth: true + spacing: PlasmaCore.Units.smallSpacing + Layout.alignment: Qt.AlignBottom + + PlasmaComponents.Label { + id: mainLabel + text: baseItem.label + Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + elide: Text.ElideRight + } + + PlasmaComponents.ToolButton { + Layout.alignment: Qt.AlignBottom + Layout.bottomMargin: -PlasmaCore.Units.smallSpacing + icon.name: "application-menu" + checkable: true + checked: contextMenu.visible && contextMenu.visualParent === this + visible: contextMenu.hasContent + onClicked: { + contextMenu.visualParent = this; + contextMenu.openRelative(); + } + PlasmaComponents.ToolTip { + text: i18n("Show additional options for %1", baseItem.label) + } + + ListItemMenu { + id: contextMenu + pulseObject: model.PulseObject + cardModel: paCardModel + itemType: { + switch (baseItem.type) { + case "sink": + return ListItemMenu.Sink; + case "sink-input": + return ListItemMenu.SinkInput; + case "source": + return ListItemMenu.Source; + case "source-output": + return ListItemMenu.SourceOutput; + } + } + sourceModel: { + if (baseItem.type.includes("sink")) { + return sinkView.model; + } else if (baseItem.type.includes("source")) { + return sourceView.model; + } + } + onVisibleChanged: window.suppressActiveClose = visible + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: PlasmaCore.Units.smallSpacing + + // this slider was effectively copied from the source (linked at the top of the file) + PlasmaComponents.Slider { + id: slider + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + + // Helper properties to allow async slider updates. + // While we are sliding we must not react to value updates + // as otherwise we can easily end up in a loop where value + // changes trigger volume changes trigger value changes. + property int volume: Volume + property bool ignoreValueChange: true + readonly property bool forceRaiseMaxVolume: volume >= PulseAudio.NormalVolume * 1.01 + + from: PulseAudio.MinimalVolume + to: PulseAudio.NormalVolume + stepSize: to / (to / PulseAudio.NormalVolume * 100.0) + visible: HasVolume + enabled: VolumeWritable + opacity: Muted ? 0.5 : 1 + + Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", baseItem.label) + + background: PlasmaCore.FrameSvgItem { + imagePath: "widgets/slider" + prefix: "groove" + width: parent.availableWidth + height: margins.top + margins.bottom + anchors.centerIn: parent + scale: parent.mirrored ? -1 : 1 + + PlasmaCore.FrameSvgItem { + imagePath: "widgets/slider" + prefix: "groove-highlight" + anchors.left: parent.left + y: (parent.height - height) / 2 + width: Math.max(margins.left + margins.right, slider.handle.x * meter.volume) + height: Math.max(margins.top + margins.bottom, parent.height) + opacity: meter.available && (meter.volume > 0 || animation.running) + VolumeMonitor { + id: meter + target: parent.visible ? model.PulseObject : null + } + Behavior on width { + NumberAnimation { + id: animation + duration: PlasmaCore.Units.shortDuration + easing.type: Easing.OutQuad + } + } + } + } + + Component.onCompleted: { + ignoreValueChange = false; + } + + onVolumeChanged: { + var oldIgnoreValueChange = ignoreValueChange; + ignoreValueChange = true; + value = Volume; + ignoreValueChange = oldIgnoreValueChange; + } + + onValueChanged: { + if (!ignoreValueChange) { + Volume = value; + Muted = value == 0; + + if (!pressed) { + updateTimer.restart(); + } + } + } + + onPressedChanged: { + if (!pressed) { + // Make sure to sync the volume once the button was + // released. + // Otherwise it might be that the slider is at v10 + // whereas PA rejected the volume change and is + // still at v15 (e.g.). + updateTimer.restart(); + + if (baseItem.type == "sink") { + playFeedback(Index); // goes to providers/VolumeProvider.qml + } + } + } + + Timer { + id: updateTimer + interval: 200 + onTriggered: slider.value = Volume + } + } + PlasmaComponents.Label { + id: percentText + readonly property real value: model.PulseObject.volume > slider.to ? model.PulseObject.volume : slider.value + readonly property real displayValue: Math.round(value / PulseAudio.NormalVolume * 100.0) + Layout.alignment: Qt.AlignHCenter + Layout.minimumWidth: percentMetrics.advanceWidth + horizontalAlignment: Qt.AlignRight + text: i18nc("volume percentage", "%1%", displayValue) + color: { + if (displayValue <= 100) { + return PlasmaCore.Theme.textColor + } else if (displayValue > 100 && displayValue <= 125) { + return PlasmaCore.Theme.neutralTextColor + } else { + return PlasmaCore.Theme.negativeTextColor + } + } + } + + TextMetrics { + id: percentMetrics + font: percentText.font + text: i18nc("only used for sizing, should be widest possible string", "100%") + } + } + } + } + + function setVolumeByPercent(targetPercent) { + model.PulseObject.volume = Math.round(PulseAudio.NormalVolume * (targetPercent/100)); + } +} diff --git a/containments/panel/package/contents/ui/volumeosd/PopupCard.qml b/containments/panel/package/contents/ui/volumeosd/PopupCard.qml new file mode 100644 index 00000000..551cba5f --- /dev/null +++ b/containments/panel/package/contents/ui/volumeosd/PopupCard.qml @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.1 as PlasmaCore +import org.kde.plasma.components 3.0 as PlasmaComponents + +// capture presses on the audio applet so it doesn't close the overlay +Controls.Control { + id: content + implicitWidth: Math.min(PlasmaCore.Units.gridUnit * 20, parent.width - PlasmaCore.Units.largeSpacing * 2) + padding: PlasmaCore.Units.smallSpacing * 2 + background: PlasmaCore.FrameSvgItem { + imagePath: "widgets/background" + anchors.margins: -PlasmaCore.Units.smallSpacing * 2 + anchors.fill: parent + } +} diff --git a/containments/panel/package/contents/ui/volumeosd/StreamListItem.qml b/containments/panel/package/contents/ui/volumeosd/StreamListItem.qml new file mode 100644 index 00000000..1df9585b --- /dev/null +++ b/containments/panel/package/contents/ui/volumeosd/StreamListItem.qml @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Harald Sitter + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick 2.0 + +import org.kde.plasma.private.volume 0.1 + +// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/StreamListItem.qml +ListItemBase { + id: item + + property QtObject devicesModel + readonly property bool isEventStream: Name == "sink-input-by-media-role:event" + + label: { + if (isEventStream) { + return i18n("Notification Sounds"); + } + if (Client && Client.name) { + return i18nc("label of stream items", "%1: %2", Client.name, Name); + } + if (Name) { + return Name; + } + return i18n("Stream name not found"); + } + + listIcon: { + if (IconName.length !== 0) { + return IconName + } + + if (item.type === "source-output") { + return "audio-input-microphone" + } + + return "audio-volume-high" + } + iconUsesPlasmaTheme: false +} diff --git a/containments/panel/package/contents/ui/volumeosd/VolumeOsd.qml b/containments/panel/package/contents/ui/volumeosd/VolumeOsd.qml new file mode 100644 index 00000000..35008194 --- /dev/null +++ b/containments/panel/package/contents/ui/volumeosd/VolumeOsd.qml @@ -0,0 +1,217 @@ +/* + * SPDX-FileCopyrightText: 2014 Martin Klapetek + * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik + * SPDX-FileCopyrightText: 2021 Devin Lin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 +import QtGraphicalEffects 1.12 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtra +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtra +import org.kde.plasma.private.nanoshell 2.0 as NanoShell + +import org.kde.kirigami 2.12 as Kirigami + +// this is loaded and managed by indicators/providers/VolumeProvider.qml +NanoShell.FullScreenOverlay { + id: window + visible: false + color: showFullApplet ? Qt.rgba(0, 0, 0, 0.6) : "transparent" + + property bool suppressActiveClose: false // used by context menus opened in the applet to not autoclose the osd + + Behavior on color { + ColorAnimation {} + } + + property int volume: 0 + property bool showFullApplet: false + + function showOverlay() { + if (!window.visible) { + window.showFullApplet = false; + window.showMaximized(); + hideTimer.restart(); + } else if (!window.showFullApplet) { // don't autohide applet when the full applet is showing + hideTimer.restart(); + } + } + + onActiveChanged: { + if (!active && !suppressActiveClose) { + hideTimer.stop(); + hideTimer.triggered(); + } + } + + Timer { + id: hideTimer + interval: 3000 + running: false + onTriggered: { + window.close(); + window.showFullApplet = false; + } + } + + Flickable { + id: flickable + anchors.fill: parent + contentHeight: cards.implicitHeight + boundsBehavior: window.showFullApplet ? Flickable.DragAndOvershootBounds : Flickable.StopAtBounds + + pressDelay: 50 + + MouseArea { + // capture taps behind cards to close + anchors.left: parent.left + anchors.right: parent.right + width: parent.width + height: Math.max(cards.implicitHeight, window.height) + onReleased: { + hideTimer.stop(); + hideTimer.triggered(); + } + + ColumnLayout { + id: cards + width: parent.width + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + // osd card + PopupCard { + id: osd + Layout.topMargin: PlasmaCore.Units.largeSpacing + Layout.alignment: Qt.AlignHCenter + + contentItem: RowLayout { + id: containerLayout + spacing: PlasmaCore.Units.smallSpacing + + anchors.leftMargin: PlasmaCore.Units.smallSpacing * 2 + anchors.rightMargin: PlasmaCore.Units.smallSpacing + + PlasmaComponents.ToolButton { + icon.name: !paSinkModel.preferredSink || paSinkModel.preferredSink.muted ? "audio-volume-muted" : "audio-volume-high" + text: !paSinkModel.preferredSink || paSinkModel.preferredSink.muted ? i18n("Unmute") : i18n("Mute") + display: Controls.AbstractButton.IconOnly + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: PlasmaCore.Units.iconSizes.medium + Layout.preferredHeight: PlasmaCore.Units.iconSizes.medium + Layout.rightMargin: PlasmaCore.Units.smallSpacing + onClicked: muteVolume() + } + + PlasmaComponents.ProgressBar { + id: volumeSlider + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: PlasmaCore.Units.smallSpacing * 2 + value: window.volume + from: 0 + to: 100 + Behavior on value { NumberAnimation { duration: PlasmaCore.Units.shortDuration } } + } + + // Get the width of a three-digit number so we can size the label + // to the maximum width to avoid the progress bar resizing itself + TextMetrics { + id: widestLabelSize + text: i18n("100%") + font: percentageLabel.font + } + + PlasmaExtra.Heading { + id: percentageLabel + Layout.preferredWidth: widestLabelSize.width + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: PlasmaCore.Units.smallSpacing + level: 3 + text: i18nc("Percentage value", "%1%", window.volume) + + // Display a subtle visual indication that the volume might be + // dangerously high + // ------------------------------------------------ + // Keep this in sync with the copies in plasma-pa:ListItemBase.qml + // and plasma-pa:VolumeSlider.qml + color: { + if (volumeSlider.value <= 100) { + return PlasmaCore.Theme.textColor + } else if (volumeSlider.value > 100 && volumeSlider.value <= 125) { + return PlasmaCore.Theme.neutralTextColor + } else { + return PlasmaCore.Theme.negativeTextColor + } + } + } + + PlasmaComponents.ToolButton { + icon.name: "configure" + text: i18n("Open audio settings") + visible: opacity !== 0 + opacity: showFullApplet ? 1 : 0 + display: Controls.AbstractButton.IconOnly + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: PlasmaCore.Units.iconSizes.medium + Layout.preferredHeight: PlasmaCore.Units.iconSizes.medium + Layout.rightMargin: PlasmaCore.Units.smallSpacing + + Behavior on opacity { NumberAnimation { duration: PlasmaCore.Units.shortDuration } } + + onClicked: { + let coords = mapToItem(flickable, 0, 0); + NanoShell.StartupFeedback.open("audio-volume-high", i18n("Audio Settings"), coords.x, coords.y, PlasmaCore.Units.iconSizes.medium); + plasmoid.nativeInterface.executeCommand("plasma-settings -m kcm_pulseaudio"); + } + } + + PlasmaComponents.ToolButton { + icon.name: window.showFullApplet ? "arrow-up" : "arrow-down" + text: i18n("Toggle showing audio streams") + display: Controls.AbstractButton.IconOnly + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: PlasmaCore.Units.iconSizes.medium + Layout.preferredHeight: PlasmaCore.Units.iconSizes.medium + onClicked: { + window.showFullApplet = !window.showFullApplet + // don't autohide applet when full applet is shown + if (window.showFullApplet) { + hideTimer.stop(); + } else { + hideTimer.restart(); + } + } + } + } + } + + // other applet cards + AudioApplet { + id: applet + Layout.topMargin: PlasmaCore.Units.largeSpacing + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: cards.width + + opacity: window.showFullApplet ? 1 : 0 + visible: opacity !== 0 + transform: Translate { + y: window.showFullApplet ? 0 : -PlasmaCore.Units.gridUnit + Behavior on y { NumberAnimation {} } + } + + Behavior on opacity { NumberAnimation {} } + } + } + } + } +} diff --git a/containments/panel/package/contents/ui/volumeosd/icon.js b/containments/panel/package/contents/ui/volumeosd/icon.js new file mode 100644 index 00000000..25e55879 --- /dev/null +++ b/containments/panel/package/contents/ui/volumeosd/icon.js @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2014-2015 Harald Sitter + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +// from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/code/icon.js +function name(volume, muted, prefix) { + if (!prefix) { + prefix = "audio-volume"; + } + var icon = null; + var percent = volume / 100; + if (percent <= 0.0 || muted) { + icon = prefix + "-muted"; + } else if (percent <= 0.25) { + icon = prefix + "-low"; + } else if (percent <= 0.75) { + icon = prefix + "-medium"; + } else { + icon = prefix + "-high"; + } + return icon; +} +