fix #142,support audio indicators for tasks

--the user is able to toggle also mute/unmute
for the audiostream
pull/1/head
Michail Vourlakos 8 years ago
parent ec68562174
commit 4441a8471e

@ -107,6 +107,9 @@
<entry name="showWindowActions" type="Bool">
<default>false</default>
</entry>
<entry name="indicateAudioStreams" type="Bool">
<default>true</default>
</entry>
</group>
</kcfg>

@ -0,0 +1,77 @@
/***************************************************************************
* Copyright (C) 2017 Kai Uwe Broulik <kde@privat.broulik.de> *
* *
* 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();
}
}
}
}
}

@ -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

@ -0,0 +1,97 @@
/***************************************************************************
* Copyright (C) 2017 by Kai Uwe Broulik <kde@privat.broulik.de> *
* *
* 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...");
}
}

@ -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: {

@ -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

@ -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

Loading…
Cancel
Save