From 4441a8471e6ed558c2299aa7c93d028a833413c9 Mon Sep 17 00:00:00 2001 From: Michail Vourlakos Date: Sun, 30 Apr 2017 18:36:48 +0300 Subject: [PATCH] fix #142,support audio indicators for tasks --the user is able to toggle also mute/unmute for the audiostream --- plasmoid/contents/config/main.xml | 3 + plasmoid/contents/ui/AudioStream.qml | 77 +++++++++++++++++ plasmoid/contents/ui/ContextMenu.qml | 20 +++++ plasmoid/contents/ui/PulseAudio.qml | 97 +++++++++++++++++++++ plasmoid/contents/ui/TaskDelegate.qml | 58 +++++++++++++ plasmoid/contents/ui/TaskIconItem.qml | 117 ++++++++++++++++++++++++-- plasmoid/contents/ui/main.qml | 23 +++-- 7 files changed, 379 insertions(+), 16 deletions(-) create mode 100644 plasmoid/contents/ui/AudioStream.qml create mode 100644 plasmoid/contents/ui/PulseAudio.qml diff --git a/plasmoid/contents/config/main.xml b/plasmoid/contents/config/main.xml index a98aa3e95..8196ff6f6 100644 --- a/plasmoid/contents/config/main.xml +++ b/plasmoid/contents/config/main.xml @@ -107,6 +107,9 @@ false + + true + diff --git a/plasmoid/contents/ui/AudioStream.qml b/plasmoid/contents/ui/AudioStream.qml new file mode 100644 index 000000000..0e23042a8 --- /dev/null +++ b/plasmoid/contents/ui/AudioStream.qml @@ -0,0 +1,77 @@ +/*************************************************************************** + * Copyright (C) 2017 Kai Uwe Broulik * + * * + * 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) any later version. * + * * + * 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, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +import QtQuick 2.0 + +import org.kde.latte 0.1 as Latte + +Item { + id: background + + + Item { + id: subRectangle + width: parent.width/ 2 + height: width + + states: [ + State { + name: "default" + when: (root.position !== PlasmaCore.Types.RightPositioned) + + AnchorChanges { + target: subRectangle + anchors{ top:parent.top; bottom:undefined; left:parent.left; right:undefined;} + } + }, + State { + name: "right" + when: (root.position === PlasmaCore.Types.RightPositioned) + + AnchorChanges { + target: subRectangle + anchors{ top:parent.top; bottom:undefined; left:undefined; right:parent.right;} + } + } + ] + + Rectangle { + anchors.centerIn: parent + width: 0.8 * parent.width + height: width + radius: width/2 + + color: theme.textColor + border.width: 1 + border.color: "grey" + + + Latte.IconItem{ + id: audioStreamIcon + anchors.fill: parent + source: mainItemContainer.playingAudio && !mainItemContainer.muted ? "audio-volume-high" : "audio-volume-muted" + + MouseArea{ + anchors.fill: parent + onClicked: mainItemContainer.toggleMuted(); + } + } + } + } +} diff --git a/plasmoid/contents/ui/ContextMenu.qml b/plasmoid/contents/ui/ContextMenu.qml index ebf063830..d0987f759 100644 --- a/plasmoid/contents/ui/ContextMenu.qml +++ b/plasmoid/contents/ui/ContextMenu.qml @@ -204,6 +204,26 @@ PlasmaComponents.ContextMenu { } } } + + // We allow mute/unmute whenever an application has a stream, regardless of whether it + // is actually playing sound. + // This way you can unmute, e.g. a telephony app, even after the conversation has ended, + // so you still have it ringing later on. + if (menu.visualParent.hasAudioStream) { + var muteItem = menu.newMenuItem(menu); + muteItem.checkable = true; + muteItem.checked = Qt.binding(function() { + return menu.visualParent && menu.visualParent.muted; + }); + muteItem.clicked.connect(function() { + menu.visualParent.toggleMuted(); + }); + muteItem.text = i18n("Mute"); + muteItem.icon = "audio-volume-muted"; + menu.addMenuItem(muteItem, virtualDesktopsMenuItem); + + menu.addMenuItem(newSeparator(menu), virtualDesktopsMenuItem); + } } ///REMOVE diff --git a/plasmoid/contents/ui/PulseAudio.qml b/plasmoid/contents/ui/PulseAudio.qml new file mode 100644 index 000000000..0e780c400 --- /dev/null +++ b/plasmoid/contents/ui/PulseAudio.qml @@ -0,0 +1,97 @@ +/*************************************************************************** + * Copyright (C) 2017 by Kai Uwe Broulik * + * * + * 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) any later version. * + * * + * 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, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +import QtQuick 2.2 + +import org.kde.plasma.private.volume 0.1 + +QtObject { + id: pulseAudio + + signal streamsChanged + + // It's a JS object so we can do key lookup and don't need to take care of filtering duplicates. + property var pidMatches: ({}) + + // TODO Evict cache at some point, preferably if all instances of an application closed. + function registerPidMatch(appName) { + if (!hasPidMatch(appName)) { + pidMatches[appName] = true; + + // In case this match is new, notify that streams might have changed. + // This way we also catch the case when the non-playing instance + // shows up first. + // Only notify if we changed to avoid infinite recursion. + streamsChanged(); + } + } + + function hasPidMatch(appName) { + return pidMatches[appName] === true; + } + + function findStreams(key, value) { + var streams = [] + for (var i = 0, length = instantiator.count; i < length; ++i) { + var stream = instantiator.objectAt(i); + if (stream[key] == value) { + streams.push(stream); + } + } + return streams + } + + function streamsForAppName(appName) { + return findStreams("appName", appName); + } + + function streamsForPid(pid) { + return findStreams("pid", pid); + } + + // QtObject has no default property, hence adding the Instantiator to one explicitly. + property var instantiator: Instantiator { + model: PulseObjectFilterModel { + filters: [ { role: "VirtualStream", value: false } ] + sourceModel: SinkInputModel {} + } + + delegate: QtObject { + readonly property int pid: Client ? Client.properties["application.process.id"] : 0 + readonly property string appName: Client ? Client.properties["application.name"] : "" + readonly property bool muted: Muted + // whether there is nothing actually going on on that stream + readonly property bool corked: Corked + + function mute() { + Muted = true + } + function unmute() { + Muted = false + } + } + + onObjectAdded: pulseAudio.streamsChanged() + onObjectRemoved: pulseAudio.streamsChanged() + } + + Component.onCompleted: { + console.log("PulseAudio Latte interface was loaded..."); + } +} diff --git a/plasmoid/contents/ui/TaskDelegate.qml b/plasmoid/contents/ui/TaskDelegate.qml index 2113793a2..9c88eba6e 100644 --- a/plasmoid/contents/ui/TaskDelegate.qml +++ b/plasmoid/contents/ui/TaskDelegate.qml @@ -109,6 +109,9 @@ MouseArea{ property string activity: tasksModel.activity readonly property var m: model + readonly property int pid: model.AppPid + readonly property string appName: model.AppName + property string modelLauncherUrl: (LauncherUrlWithoutIcon !== null) ? LauncherUrlWithoutIcon : "" property string modelLauncherUrlWithIcon: (LauncherUrl !== null) ? LauncherUrl : "" property string launcherUrl: "" @@ -132,6 +135,18 @@ MouseArea{ } } + ////// Audio streams ////// + property Item audioStreamOverlay + property var audioStreams: [] + readonly property bool hasAudioStream: plasmoid.configuration.indicateAudioStreams && audioStreams.length > 0 + readonly property bool playingAudio: hasAudioStream && audioStreams.some(function (item) { + return !item.corked + }) + readonly property bool muted: hasAudioStream && audioStreams.every(function (item) { + return item.muted + }) + ////// + property QtObject contextMenu: null property QtObject draggingResistaner: null property QtObject hoveredTimerObj: null @@ -628,6 +643,9 @@ MouseArea{ // onItemIndexChanged: { // } + onAppNameChanged: updateAudioStreams() + onPidChanged: updateAudioStreams() + onHoveredIndexChanged: { var distanceFromHovered = Math.abs(index - icList.hoveredIndex); @@ -1131,6 +1149,45 @@ MouseArea{ } } + + function updateAudioStreams() { + var pa = pulseAudio.item; + if (!pa) { + task.audioStreams = []; + return; + } + + var streams = pa.streamsForPid(mainItemContainer.pid); + if (streams.length) { + pa.registerPidMatch(mainItemContainer.appName); + } else { + // We only want to fall back to appName matching if we never managed to map + // a PID to an audio stream window. Otherwise if you have two instances of + // an application, one playing and the other not, it will look up appName + // for the non-playing instance and erroneously show an indicator on both. + if (!pa.hasPidMatch(mainItemContainer.appName)) { + streams = pa.streamsForAppName(mainItemContainer.appName); + } + } + + mainItemContainer.audioStreams = streams; + } + + function toggleMuted() { + if (muted) { + mainItemContainer.audioStreams.forEach(function (item) { item.unmute(); }); + } else { + mainItemContainer.audioStreams.forEach(function (item) { item.mute(); }); + } + } + + Connections { + target: pulseAudio.item + ignoreUnknownSignals: true // Plasma-PA might not be available + onStreamsChanged: mainItemContainer.updateAudioStreams() + } + + ///REMOVE //fix wrong positioning of launchers.... onActivityChanged:{ @@ -1167,6 +1224,7 @@ MouseArea{ }*/ showWindowAnimation.showWindow(); + updateAudioStreams(); } Component.onDestruction: { diff --git a/plasmoid/contents/ui/TaskIconItem.qml b/plasmoid/contents/ui/TaskIconItem.qml index 99e1a0819..c41329de2 100644 --- a/plasmoid/contents/ui/TaskIconItem.qml +++ b/plasmoid/contents/ui/TaskIconItem.qml @@ -353,14 +353,115 @@ Item{ } } - // Loader { - // anchors.fill: parent - //// asynchronous: true - // source: "TaskProgressOverlay.qml" - // active: true - //active: (centralItem.smartLauncherEnabled && centralItem.smartLauncherItem - // && centralItem.smartLauncherItem.progressVisible) - //} + /// Audio Loader + + /*Loader { + id: audioStreamIconLoader + + readonly property bool shown: item && item.visible + + source: "AudioStream.qml" + width: units.roundToIconSize(Math.min(Math.min(iconImageBuffer.width, iconImageBuffer.height), units.iconSizes.smallMedium)) + height: width + active: mainItemContainer.hasAudioStream + }*/ + + + Loader{ + id: audioStreamIconLoader + anchors.fill: parent + active: mainItemContainer.hasAudioStream + asynchronous: true + + readonly property bool shown: item && item.visible + + sourceComponent: Item{ + ShaderEffect { + id: iconOverlay2 + enabled: false + anchors.fill: parent + property var source: ShaderEffectSource { + sourceItem: iconImageBuffer + hideSource: true + } + property var mask: ShaderEffectSource { + sourceItem: Item{ + width: iconImageBuffer.width + height: iconImageBuffer.height + Rectangle{ + id: maskRect2 + width: parent.width/2 + height: width + radius: width + + Rectangle{ + id: maskCorner2 + width:parent.width/2 + height:parent.height/2 + } + + states: [ + State { + name: "default" + when: (plasmoid.location !== PlasmaCore.Types.RightEdge) + + AnchorChanges { + target: maskRect2 + anchors{ top:parent.top; bottom:undefined; left:parent.left; right:undefined;} + } + AnchorChanges { + target: maskCorner2 + anchors{ top:parent.top; bottom:undefined; left:parent.left; right:undefined;} + } + }, + State { + name: "right" + when: (plasmoid.location === PlasmaCore.Types.RightEdge) + + AnchorChanges { + target: maskRect2 + anchors{ top:parent.top; bottom:undefined; left:undefined; right:parent.right;} + } + AnchorChanges { + target: maskCorner2 + anchors{ top:parent.top; bottom:undefined; left:undefined; right:parent.right;} + } + } + ] + + Connections{ + target: plasmoid + onLocationChanged: iconOverlay2.mask.scheduleUpdate(); + } + } + //badgeMask + } + hideSource: true + // live: mainItemContainer.badgeIndicator > 0 ? true : false + } + + supportsAtlasTextures: true + + fragmentShader: " + varying highp vec2 qt_TexCoord0; + uniform highp float qt_Opacity; + uniform lowp sampler2D source; + uniform lowp sampler2D mask; + void main() { + gl_FragColor = texture2D(source, qt_TexCoord0.st) * (1.0 - (texture2D(mask, qt_TexCoord0.st).a)) * qt_Opacity; + } + " + } + + AudioStream{ + anchors.fill:parent + } + } + } + + + + /// END of Audio Loader } ///Shadow in tasks diff --git a/plasmoid/contents/ui/main.qml b/plasmoid/contents/ui/main.qml index 9bd34cb17..9c841e25e 100644 --- a/plasmoid/contents/ui/main.qml +++ b/plasmoid/contents/ui/main.qml @@ -614,9 +614,8 @@ Item { id: mpris2Source engine: "mpris2" connectedSources: sources - - function sourceNameForLauncherUrl(launcherUrl) { - if (!launcherUrl) { + function sourceNameForLauncherUrl(launcherUrl, pid) { + if (!launcherUrl || launcherUrl == "") { return ""; } @@ -624,12 +623,15 @@ Item { // Moreover, remove URL parameters, like wmClass (part after the question mark) var desktopFileName = launcherUrl.toString().split('/').pop().split('?')[0].replace(".desktop", "") - for (var i = 0, length = sources.length; i < length; ++i) { - var source = sources[i]; + for (var i = 0, length = connectedSources.length; i < length; ++i) { + var source = connectedSources[i]; + // we intend to connect directly, otherwise the multiplexer steals the connection away + if (source === "@multiplex") { + continue; + } var sourceData = data[source]; - - if (sourceData && sourceData.DesktopEntry === desktopFileName) { - return source + if (sourceData && sourceData.DesktopEntry === desktopFileName && (pid === undefined || sourceData.InstancePid === pid)) { + return source; } } @@ -662,6 +664,11 @@ Item { } } + Loader { + id: pulseAudio + source: "PulseAudio.qml" + active: plasmoid.configuration.indicateAudioStreams + } /* IconsModel{ id: iconsmdl