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