diff --git a/applets/taskmanager/package/contents/config/main.xml b/applets/taskmanager/package/contents/config/main.xml --- a/applets/taskmanager/package/contents/config/main.xml +++ b/applets/taskmanager/package/contents/config/main.xml @@ -89,6 +89,10 @@ true + + + true + diff --git a/applets/taskmanager/package/contents/ui/ConfigGeneral.qml b/applets/taskmanager/package/contents/ui/ConfigGeneral.qml --- a/applets/taskmanager/package/contents/ui/ConfigGeneral.qml +++ b/applets/taskmanager/package/contents/ui/ConfigGeneral.qml @@ -34,6 +34,7 @@ property alias cfg_wheelEnabled: wheelEnabled.checked property alias cfg_highlightWindows: highlightWindows.checked property alias cfg_smartLaunchersEnabled: smartLaunchers.checked + property alias cfg_indicateAudioStreams: indicateAudioStreams.checked property alias cfg_maxStripes: maxStripes.value property alias cfg_groupingStrategy: groupingStrategy.currentIndex property alias cfg_middleClickAction: middleClickAction.currentIndex @@ -107,6 +108,12 @@ text: i18n("Show progress and status information in task buttons") } + CheckBox { + id: indicateAudioStreams + Layout.fillWidth: true + text: i18n("Denote applications that play audio") + } + RowLayout { Label { text: i18n("On middle-click:") diff --git a/applets/taskmanager/package/contents/ui/ContextMenu.qml b/applets/taskmanager/package/contents/ui/ContextMenu.qml --- a/applets/taskmanager/package/contents/ui/ContextMenu.qml +++ b/applets/taskmanager/package/contents/ui/ContextMenu.qml @@ -188,6 +188,26 @@ } } } + + // 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); + } } PlasmaComponents.MenuItem { diff --git a/applets/taskmanager/package/contents/ui/Task.qml b/applets/taskmanager/package/contents/ui/Task.qml --- a/applets/taskmanager/package/contents/ui/Task.qml +++ b/applets/taskmanager/package/contents/ui/Task.qml @@ -41,6 +41,7 @@ readonly property var m: model + readonly property int pid: model.AppPid property int itemIndex: index property bool inPopup: false property bool isWindow: model.IsWindow === true @@ -55,14 +56,26 @@ readonly property bool smartLauncherEnabled: plasmoid.configuration.smartLaunchersEnabled && !inPopup && model.IsStartup !== true property QtObject smartLauncherItem: null + 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 + }) + readonly property bool highlighted: (inPopup && activeFocus) || (!inPopup && containsMouse) function hideToolTipTemporarily() { toolTipArea.hideToolTip(); } acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MidButton + onPidChanged: updateAudioStreams() + onIsWindowChanged: { if (isWindow) { taskInitComponent.createObject(task); @@ -167,14 +180,49 @@ } } + onHasAudioStreamChanged: { + if (hasAudioStream) { + audioStreamIconLoader.active = true + } + } + Keys.onReturnPressed: TaskTools.activateTask(modelIndex(), model, event.modifiers, task) Keys.onEnterPressed: Keys.onReturnPressed(event); function modelIndex() { return (inPopup ? tasksModel.makeModelIndex(groupDialog.visualParent.itemIndex, index) : tasksModel.makeModelIndex(index)); } + function updateAudioStreams() { + if (!pid) { + task.audioStreams = []; + return; + } + + var pa = pulseAudio.item; + if (!pa) { + task.audioStreams = []; + return; + } + + task.audioStreams = pa.streamsForPid(pid); + } + + function toggleMuted() { + if (muted) { + task.audioStreams.forEach(function (item) { item.unmute(); }); + } else { + task.audioStreams.forEach(function (item) { item.mute(); }); + } + } + + Connections { + target: pulseAudio.item + ignoreUnknownSignals: true // Plasma-PA might not be available + onStreamsChanged: task.updateAudioStreams() + } + Component { id: taskInitComponent @@ -300,9 +348,7 @@ topMargin: adjustMargin(false, parent.height, taskFrame.margins.top) } - width: (label.visible ? height - : parent.width - adjustMargin(true, parent.width, taskFrame.margins.left) - - adjustMargin(true, parent.width, taskFrame.margins.right)) + width: height height: (parent.height - adjustMargin(false, parent.height, taskFrame.margins.top) - adjustMargin(false, parent.height, taskFrame.margins.bottom)) @@ -351,7 +397,7 @@ // the text label margin, which derives from the icon width. State { name: "standalone" - when: !label.visible + when: !label.visible && !audioStreamIconLoader.shown AnchorChanges { target: iconBox @@ -362,6 +408,8 @@ PropertyChanges { target: iconBox anchors.leftMargin: 0 + width: parent.width - adjustMargin(true, task.width, taskFrame.margins.left) + - adjustMargin(true, task.width, taskFrame.margins.right) } } ] @@ -380,6 +428,23 @@ } } + Loader { + id: audioStreamIconLoader + + readonly property bool shown: item && item.visible + + source: "AudioStream.qml" + width: Math.min(units.iconSizes.medium, iconBox.width) + height: Math.min(units.iconSizes.medium, iconBox.height) + + anchors { + right: parent.right + rightMargin: iconBox.adjustMargin(true, parent.width, taskFrame.margins.right) + top: parent.top + topMargin: iconBox.adjustMargin(false, parent.height, taskFrame.margins.top) + } + } + PlasmaComponents.Label { id: label @@ -390,7 +455,7 @@ fill: parent leftMargin: taskFrame.margins.left + iconBox.width + units.smallSpacing topMargin: taskFrame.margins.top - rightMargin: taskFrame.margins.right + rightMargin: taskFrame.margins.right + (audioStreamIconLoader.shown ? (audioStreamIconLoader.width + units.smallSpacing) : 0) bottomMargin: taskFrame.margins.bottom } @@ -458,5 +523,7 @@ if (!inPopup && model.IsWindow !== true) { taskInitComponent.createObject(task); } + + updateAudioStreams() } } diff --git a/applets/taskmanager/package/contents/ui/ToolTipInstance.qml b/applets/taskmanager/package/contents/ui/ToolTipInstance.qml --- a/applets/taskmanager/package/contents/ui/ToolTipInstance.qml +++ b/applets/taskmanager/package/contents/ui/ToolTipInstance.qml @@ -460,6 +460,14 @@ } } + if (parentTask.playingAudio) { + if (parentTask.muted) { + subTextEntries.push(i18nc("The application this window belongs to is muted", "Currently Muted")); + } else { + subTextEntries.push(i18nc("The application this window belongs to is playing audio", "Currently Playing Audio")); + } + } + return subTextEntries.join("\n"); } } diff --git a/applets/taskmanager/package/contents/ui/main.qml b/applets/taskmanager/package/contents/ui/main.qml --- a/applets/taskmanager/package/contents/ui/main.qml +++ b/applets/taskmanager/package/contents/ui/main.qml @@ -233,6 +233,12 @@ } } + Loader { + id: pulseAudio + source: "PulseAudio.qml" + active: plasmoid.configuration.indicateAudioStreams + } + Timer { id: iconGeometryTimer