mirror of
https://github.com/KDE/latte-dock.git
synced 2025-03-21 22:50:14 +03:00
fix #142,support audio indicators for tasks
--the user is able to toggle also mute/unmute for the audiostream
This commit is contained in:
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>
|
||||
|
||||
|
77
plasmoid/contents/ui/AudioStream.qml
Normal file
77
plasmoid/contents/ui/AudioStream.qml
Normal file
@ -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
|
||||
|
97
plasmoid/contents/ui/PulseAudio.qml
Normal file
97
plasmoid/contents/ui/PulseAudio.qml
Normal file
@ -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…
x
Reference in New Issue
Block a user