mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-28 06:33:09 +00:00
Add advanced VolumeOSD to top panel
This commit is contained in:
parent
b0b0e6f237
commit
40419e48bc
10 changed files with 864 additions and 8 deletions
|
|
@ -12,6 +12,8 @@ import org.kde.plasma.core 2.0 as PlasmaCore
|
|||
import org.kde.plasma.components 2.0 as PlasmaComponents
|
||||
import org.kde.plasma.private.volume 0.1
|
||||
|
||||
import "../../volumeosd"
|
||||
|
||||
QtObject {
|
||||
property bool isVisible: paSinkModel.preferredSink && paSinkModel.preferredSink.muted
|
||||
property string icon: paSinkModel.preferredSink && !isDummyOutput(paSinkModel.preferredSink)
|
||||
|
|
@ -23,6 +25,10 @@ QtObject {
|
|||
property int volumeStep: Math.round(5 * PulseAudio.NormalVolume / 100.0)
|
||||
readonly property string dummyOutputName: "auto_null"
|
||||
|
||||
function showVolumeOverlay() {
|
||||
osd.showOverlay();
|
||||
}
|
||||
|
||||
function iconName(volume, muted, prefix) {
|
||||
if (!prefix) {
|
||||
prefix = "audio-volume";
|
||||
|
|
@ -57,10 +63,10 @@ QtObject {
|
|||
}
|
||||
|
||||
function playFeedback(sinkIndex) {
|
||||
if(!volumeFeedback){
|
||||
if (!volumeFeedback){
|
||||
return;
|
||||
}
|
||||
if(sinkIndex == undefined) {
|
||||
if (sinkIndex == undefined) {
|
||||
sinkIndex = paSinkModel.preferredSink.index;
|
||||
}
|
||||
feedback.play(sinkIndex)
|
||||
|
|
@ -75,7 +81,8 @@ QtObject {
|
|||
var percent = volumePercent(volume, maxVolumeValue);
|
||||
paSinkModel.preferredSink.muted = percent == 0;
|
||||
paSinkModel.preferredSink.volume = volume;
|
||||
osd.show(percent);
|
||||
osd.volume = percent;
|
||||
osd.showOverlay();
|
||||
playFeedback();
|
||||
|
||||
}
|
||||
|
|
@ -89,7 +96,8 @@ QtObject {
|
|||
var percent = volumePercent(volume, maxVolumeValue);
|
||||
paSinkModel.preferredSink.muted = percent == 0;
|
||||
paSinkModel.preferredSink.volume = volume;
|
||||
osd.show(percent);
|
||||
osd.volume = percent;
|
||||
osd.showOverlay();
|
||||
playFeedback();
|
||||
}
|
||||
|
||||
|
|
@ -102,15 +110,37 @@ QtObject {
|
|||
|
||||
var toMute = !paSinkModel.preferredSink.muted;
|
||||
paSinkModel.preferredSink.muted = toMute;
|
||||
osd.show(toMute ? 0 : volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue));
|
||||
|
||||
osd.volume = toMute ? 0 : volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue);
|
||||
osd.showOverlay();
|
||||
|
||||
if (!toMute) {
|
||||
playFeedback();
|
||||
}
|
||||
}
|
||||
|
||||
property var updateVolume: Connections {
|
||||
target: paSinkModel.preferredSink
|
||||
|
||||
function onVolumeChanged() {
|
||||
var percent = volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue);
|
||||
osd.volume = percent;
|
||||
}
|
||||
}
|
||||
property var updateVolumeOnSinkChange: Connections {
|
||||
target: paSinkModel
|
||||
|
||||
function onPreferredSinkChanged() {
|
||||
if (paSinkModel.preferredSink) {
|
||||
var percent = volumePercent(paSinkModel.preferredSink.volume, maxVolumeValue);
|
||||
osd.volume = percent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property SinkModel paSinkModel: SinkModel {}
|
||||
|
||||
property VolumeOSD osd: VolumeOSD {}
|
||||
property VolumeOsd osd: VolumeOsd {}
|
||||
|
||||
property VolumeFeedback feedback: VolumeFeedback {}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ Item {
|
|||
fullContainer.applet = applet;
|
||||
fullContainer.contentItem = applet.fullRepresentationItem;
|
||||
//applet.fullRepresentationItem.anchors.fill = fullContainer;
|
||||
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
|
|
@ -233,7 +234,7 @@ Item {
|
|||
onCloseRequested: slidingPanel.close()
|
||||
}
|
||||
|
||||
// notifications
|
||||
// notifications and media player
|
||||
ListView {
|
||||
id: fullRepresentationView
|
||||
implicitHeight: units.gridUnit * 20
|
||||
|
|
|
|||
|
|
@ -106,6 +106,10 @@ Item {
|
|||
root.closeRequested();
|
||||
}
|
||||
|
||||
function openVolumeOsd() {
|
||||
volumeProvider.showVolumeOverlay();
|
||||
}
|
||||
|
||||
// initialize quick settings
|
||||
Component.onCompleted: {
|
||||
//NOTE: add all in javascript as the static decl of listelements can't have scripts
|
||||
|
|
@ -150,7 +154,7 @@ Item {
|
|||
"icon": "audio-speakers-symbolic",
|
||||
"enabled": false,
|
||||
"settingsCommand": "plasma-settings -m kcm_pulseaudio",
|
||||
"toggleFunction": ""
|
||||
"toggleFunction": "openVolumeOsd"
|
||||
});
|
||||
settingsModel.append({
|
||||
"text": i18n("Flashlight"),
|
||||
|
|
|
|||
203
containments/panel/package/contents/ui/volumeosd/AudioApplet.qml
Normal file
203
containments/panel/package/contents/ui/volumeosd/AudioApplet.qml
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
|
||||
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.2
|
||||
import QtGraphicalEffects 1.12
|
||||
|
||||
import org.kde.plasma.core 2.1 as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtra
|
||||
import org.kde.plasma.plasmoid 2.0
|
||||
import org.kde.kquickcontrolsaddons 2.0 as KQCAddons
|
||||
|
||||
import org.kde.plasma.private.volume 0.1
|
||||
|
||||
// adapted version of https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/main.qml
|
||||
|
||||
// most audio functions are in VolumeProvider.qml (which will be a parent)
|
||||
// capture presses on the audio applet so it doesn't close the overlay
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
// pulseaudio models
|
||||
|
||||
function isDummyOutput(output) {
|
||||
return output && output.name === dummyOutputName;
|
||||
}
|
||||
|
||||
SinkModel {
|
||||
id: paSinkModel
|
||||
}
|
||||
|
||||
PulseObjectFilterModel {
|
||||
id: paSinkFilterModel
|
||||
sortRole: "SortByDefault"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
filterOutInactiveDevices: true
|
||||
sourceModel: paSinkModel
|
||||
}
|
||||
|
||||
SourceModel {
|
||||
id: paSourceModel
|
||||
}
|
||||
|
||||
PulseObjectFilterModel {
|
||||
id: paSourceFilterModel
|
||||
sortRole: "SortByDefault"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
filterOutInactiveDevices: true
|
||||
sourceModel: paSourceModel
|
||||
}
|
||||
|
||||
CardModel {
|
||||
id: paCardModel
|
||||
}
|
||||
|
||||
// ui elements
|
||||
|
||||
PopupCard {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: PlasmaCore.Units.largeSpacing
|
||||
contentItem: ColumnLayout {
|
||||
anchors.rightMargin: PlasmaCore.Units.smallSpacing
|
||||
anchors.leftMargin: PlasmaCore.Units.smallSpacing
|
||||
|
||||
PlasmaExtra.Heading {
|
||||
level: 2
|
||||
text: i18n("Outputs")
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: PlasmaCore.Units.smallSpacing
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sinkView
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: paSinkFilterModel
|
||||
delegate: DeviceListItem {
|
||||
Layout.fillWidth: true
|
||||
type: "sink"
|
||||
onlyone: sinkView.count === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopupCard {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: PlasmaCore.Units.largeSpacing
|
||||
contentItem: ColumnLayout {
|
||||
anchors.rightMargin: PlasmaCore.Units.smallSpacing
|
||||
anchors.leftMargin: PlasmaCore.Units.smallSpacing
|
||||
|
||||
PlasmaExtra.Heading {
|
||||
level: 2
|
||||
text: i18n("Inputs")
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: PlasmaCore.Units.smallSpacing
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sourceView
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: paSourceFilterModel
|
||||
delegate: DeviceListItem {
|
||||
Layout.fillWidth: true
|
||||
type: "source"
|
||||
onlyone: sinkView.count === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopupCard {
|
||||
visible: sourceInputView.model.count + sourceMediaInputView.model.count !== 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: PlasmaCore.Units.largeSpacing
|
||||
contentItem: ColumnLayout {
|
||||
anchors.rightMargin: PlasmaCore.Units.smallSpacing
|
||||
anchors.leftMargin: PlasmaCore.Units.smallSpacing
|
||||
|
||||
PlasmaExtra.Heading {
|
||||
level: 2
|
||||
text: i18n("Playback Streams")
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: PlasmaCore.Units.smallSpacing
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sourceMediaInputView
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: PulseObjectFilterModel {
|
||||
filters: [ { role: "Name", value: "sink-input-by-media-role:event" } ]
|
||||
sourceModel: StreamRestoreModel {}
|
||||
}
|
||||
delegate: StreamListItem {
|
||||
Layout.fillWidth: true
|
||||
width: sourceOutputView.width
|
||||
type: "sink-input"
|
||||
devicesModel: sourceView.model
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sourceInputView
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: PulseObjectFilterModel {
|
||||
filters: [ { role: "VirtualStream", value: false } ]
|
||||
sourceModel: SinkInputModel {}
|
||||
}
|
||||
|
||||
delegate: StreamListItem {
|
||||
Layout.fillWidth: true
|
||||
width: sourceOutputView.width
|
||||
type: "sink-input"
|
||||
devicesModel: sourceView.model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopupCard {
|
||||
visible: sourceOutputView.model.count !== 0
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: PlasmaCore.Units.largeSpacing
|
||||
contentItem: ColumnLayout {
|
||||
anchors.rightMargin: PlasmaCore.Units.smallSpacing
|
||||
anchors.leftMargin: PlasmaCore.Units.smallSpacing
|
||||
|
||||
PlasmaExtra.Heading {
|
||||
level: 2
|
||||
text: i18n("Recording Streams")
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: PlasmaCore.Units.smallSpacing
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sourceOutputView
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: PulseObjectFilterModel {
|
||||
filters: [ { role: "VirtualStream", value: false } ]
|
||||
sourceModel: SourceOutputModel {}
|
||||
}
|
||||
delegate: StreamListItem {
|
||||
Layout.fillWidth: true
|
||||
width: sourceOutputView.width
|
||||
type: "source-output"
|
||||
devicesModel: sourceView.model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
|
||||
SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.0
|
||||
|
||||
import org.kde.plasma.private.volume 0.1
|
||||
|
||||
// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/DeviceListItem.qml
|
||||
ListItemBase {
|
||||
readonly property var currentPort: Ports[ActivePortIndex]
|
||||
readonly property var currentActivePortIndex: ActivePortIndex
|
||||
readonly property var currentMuted: Muted
|
||||
readonly property var activePortIndex: ActivePortIndex
|
||||
property bool onlyone: false
|
||||
|
||||
label: {
|
||||
if (currentPort && currentPort.description) {
|
||||
if (onlyone || !Description) {
|
||||
return currentPort.description;
|
||||
} else {
|
||||
return i18nc("label of device items", "%1 (%2)", currentPort.description, Description);
|
||||
}
|
||||
}
|
||||
if (Description) {
|
||||
return Description;
|
||||
}
|
||||
if (Name) {
|
||||
return Name;
|
||||
}
|
||||
return i18n("Device name not found");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
|
||||
* SPDX-FileCopyrightText: 2019 Sefa Eyeoglu <contact@scrumplex.net>
|
||||
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.2
|
||||
import QtGraphicalEffects 1.12
|
||||
|
||||
import org.kde.kquickcontrolsaddons 2.0
|
||||
import org.kde.plasma.core 2.1 as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtra
|
||||
import org.kde.plasma.private.volume 0.1
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
import "icon.js" as Icon
|
||||
|
||||
// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/ListItemBase.qml
|
||||
Kirigami.SwipeListItem {
|
||||
id: baseItem
|
||||
|
||||
property string label
|
||||
property alias listIcon: clientIcon.source
|
||||
property alias iconUsesPlasmaTheme: clientIcon.usesPlasmaTheme
|
||||
property string type // sink, source, source-output
|
||||
|
||||
topPadding: PlasmaCore.Units.smallSpacing
|
||||
bottomPadding: PlasmaCore.Units.smallSpacing
|
||||
leftPadding: PlasmaCore.Units.smallSpacing
|
||||
rightPadding: PlasmaCore.Units.smallSpacing
|
||||
|
||||
alwaysVisibleActions: true
|
||||
|
||||
backgroundColor: "transparent" // we use panel background, no need for the same colour to be on top
|
||||
activeBackgroundColor: selectButton.visible ? PlasmaCore.Theme.highlightColor : "transparent"
|
||||
|
||||
onClicked: {
|
||||
if (selectButton.visible) {
|
||||
model.PulseObject.default = true;
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: row
|
||||
spacing: PlasmaCore.Units.smallSpacing
|
||||
|
||||
PlasmaComponents.RadioButton {
|
||||
id: selectButton
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: Math.round(row.height / 2 - implicitHeight - PlasmaCore.Units.smallSpacing / 2) // align with text
|
||||
checked: model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false
|
||||
visible: (baseItem.type == "sink" && sinkView.model.count > 1) || (baseItem.type == "source" && sourceView.model.count > 1)
|
||||
onClicked: model.PulseObject.default = true
|
||||
}
|
||||
|
||||
// application icon
|
||||
PlasmaCore.IconItem {
|
||||
id: clientIcon
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: PlasmaCore.Units.smallSpacing
|
||||
Layout.preferredWidth: PlasmaCore.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: PlasmaCore.Units.iconSizes.smallMedium
|
||||
visible: type === "sink-input" || type === "source-output"
|
||||
source: "unknown"
|
||||
onSourceChanged: {
|
||||
if (!valid && source != "unknown") {
|
||||
source = "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
spacing: PlasmaCore.Units.smallSpacing
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: PlasmaCore.Units.smallSpacing
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
|
||||
PlasmaComponents.Label {
|
||||
id: mainLabel
|
||||
text: baseItem.label
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
Layout.alignment: Qt.AlignBottom
|
||||
Layout.bottomMargin: -PlasmaCore.Units.smallSpacing
|
||||
icon.name: "application-menu"
|
||||
checkable: true
|
||||
checked: contextMenu.visible && contextMenu.visualParent === this
|
||||
visible: contextMenu.hasContent
|
||||
onClicked: {
|
||||
contextMenu.visualParent = this;
|
||||
contextMenu.openRelative();
|
||||
}
|
||||
PlasmaComponents.ToolTip {
|
||||
text: i18n("Show additional options for %1", baseItem.label)
|
||||
}
|
||||
|
||||
ListItemMenu {
|
||||
id: contextMenu
|
||||
pulseObject: model.PulseObject
|
||||
cardModel: paCardModel
|
||||
itemType: {
|
||||
switch (baseItem.type) {
|
||||
case "sink":
|
||||
return ListItemMenu.Sink;
|
||||
case "sink-input":
|
||||
return ListItemMenu.SinkInput;
|
||||
case "source":
|
||||
return ListItemMenu.Source;
|
||||
case "source-output":
|
||||
return ListItemMenu.SourceOutput;
|
||||
}
|
||||
}
|
||||
sourceModel: {
|
||||
if (baseItem.type.includes("sink")) {
|
||||
return sinkView.model;
|
||||
} else if (baseItem.type.includes("source")) {
|
||||
return sourceView.model;
|
||||
}
|
||||
}
|
||||
onVisibleChanged: window.suppressActiveClose = visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: PlasmaCore.Units.smallSpacing
|
||||
|
||||
// this slider was effectively copied from the source (linked at the top of the file)
|
||||
PlasmaComponents.Slider {
|
||||
id: slider
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
// Helper properties to allow async slider updates.
|
||||
// While we are sliding we must not react to value updates
|
||||
// as otherwise we can easily end up in a loop where value
|
||||
// changes trigger volume changes trigger value changes.
|
||||
property int volume: Volume
|
||||
property bool ignoreValueChange: true
|
||||
readonly property bool forceRaiseMaxVolume: volume >= PulseAudio.NormalVolume * 1.01
|
||||
|
||||
from: PulseAudio.MinimalVolume
|
||||
to: PulseAudio.NormalVolume
|
||||
stepSize: to / (to / PulseAudio.NormalVolume * 100.0)
|
||||
visible: HasVolume
|
||||
enabled: VolumeWritable
|
||||
opacity: Muted ? 0.5 : 1
|
||||
|
||||
Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", baseItem.label)
|
||||
|
||||
background: PlasmaCore.FrameSvgItem {
|
||||
imagePath: "widgets/slider"
|
||||
prefix: "groove"
|
||||
width: parent.availableWidth
|
||||
height: margins.top + margins.bottom
|
||||
anchors.centerIn: parent
|
||||
scale: parent.mirrored ? -1 : 1
|
||||
|
||||
PlasmaCore.FrameSvgItem {
|
||||
imagePath: "widgets/slider"
|
||||
prefix: "groove-highlight"
|
||||
anchors.left: parent.left
|
||||
y: (parent.height - height) / 2
|
||||
width: Math.max(margins.left + margins.right, slider.handle.x * meter.volume)
|
||||
height: Math.max(margins.top + margins.bottom, parent.height)
|
||||
opacity: meter.available && (meter.volume > 0 || animation.running)
|
||||
VolumeMonitor {
|
||||
id: meter
|
||||
target: parent.visible ? model.PulseObject : null
|
||||
}
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
id: animation
|
||||
duration: PlasmaCore.Units.shortDuration
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
ignoreValueChange = false;
|
||||
}
|
||||
|
||||
onVolumeChanged: {
|
||||
var oldIgnoreValueChange = ignoreValueChange;
|
||||
ignoreValueChange = true;
|
||||
value = Volume;
|
||||
ignoreValueChange = oldIgnoreValueChange;
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
if (!ignoreValueChange) {
|
||||
Volume = value;
|
||||
Muted = value == 0;
|
||||
|
||||
if (!pressed) {
|
||||
updateTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressedChanged: {
|
||||
if (!pressed) {
|
||||
// Make sure to sync the volume once the button was
|
||||
// released.
|
||||
// Otherwise it might be that the slider is at v10
|
||||
// whereas PA rejected the volume change and is
|
||||
// still at v15 (e.g.).
|
||||
updateTimer.restart();
|
||||
|
||||
if (baseItem.type == "sink") {
|
||||
playFeedback(Index); // goes to providers/VolumeProvider.qml
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 200
|
||||
onTriggered: slider.value = Volume
|
||||
}
|
||||
}
|
||||
PlasmaComponents.Label {
|
||||
id: percentText
|
||||
readonly property real value: model.PulseObject.volume > slider.to ? model.PulseObject.volume : slider.value
|
||||
readonly property real displayValue: Math.round(value / PulseAudio.NormalVolume * 100.0)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.minimumWidth: percentMetrics.advanceWidth
|
||||
horizontalAlignment: Qt.AlignRight
|
||||
text: i18nc("volume percentage", "%1%", displayValue)
|
||||
color: {
|
||||
if (displayValue <= 100) {
|
||||
return PlasmaCore.Theme.textColor
|
||||
} else if (displayValue > 100 && displayValue <= 125) {
|
||||
return PlasmaCore.Theme.neutralTextColor
|
||||
} else {
|
||||
return PlasmaCore.Theme.negativeTextColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: percentMetrics
|
||||
font: percentText.font
|
||||
text: i18nc("only used for sizing, should be widest possible string", "100%")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVolumeByPercent(targetPercent) {
|
||||
model.PulseObject.volume = Math.round(PulseAudio.NormalVolume * (targetPercent/100));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import org.kde.plasma.core 2.1 as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
|
||||
// capture presses on the audio applet so it doesn't close the overlay
|
||||
Controls.Control {
|
||||
id: content
|
||||
implicitWidth: Math.min(PlasmaCore.Units.gridUnit * 20, parent.width - PlasmaCore.Units.largeSpacing * 2)
|
||||
padding: PlasmaCore.Units.smallSpacing * 2
|
||||
background: PlasmaCore.FrameSvgItem {
|
||||
imagePath: "widgets/background"
|
||||
anchors.margins: -PlasmaCore.Units.smallSpacing * 2
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
|
||||
SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.0
|
||||
|
||||
import org.kde.plasma.private.volume 0.1
|
||||
|
||||
// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/StreamListItem.qml
|
||||
ListItemBase {
|
||||
id: item
|
||||
|
||||
property QtObject devicesModel
|
||||
readonly property bool isEventStream: Name == "sink-input-by-media-role:event"
|
||||
|
||||
label: {
|
||||
if (isEventStream) {
|
||||
return i18n("Notification Sounds");
|
||||
}
|
||||
if (Client && Client.name) {
|
||||
return i18nc("label of stream items", "%1: %2", Client.name, Name);
|
||||
}
|
||||
if (Name) {
|
||||
return Name;
|
||||
}
|
||||
return i18n("Stream name not found");
|
||||
}
|
||||
|
||||
listIcon: {
|
||||
if (IconName.length !== 0) {
|
||||
return IconName
|
||||
}
|
||||
|
||||
if (item.type === "source-output") {
|
||||
return "audio-input-microphone"
|
||||
}
|
||||
|
||||
return "audio-volume-high"
|
||||
}
|
||||
iconUsesPlasmaTheme: false
|
||||
}
|
||||
217
containments/panel/package/contents/ui/volumeosd/VolumeOsd.qml
Normal file
217
containments/panel/package/contents/ui/volumeosd/VolumeOsd.qml
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
|
||||
* SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@broulik.de>
|
||||
* SPDX-FileCopyrightText: 2021 Devin Lin <espidev@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.2
|
||||
import QtGraphicalEffects 1.12
|
||||
|
||||
import org.kde.plasma.core 2.0 as PlasmaCore
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtra
|
||||
import org.kde.plasma.components 3.0 as PlasmaComponents
|
||||
import org.kde.plasma.extras 2.0 as PlasmaExtra
|
||||
import org.kde.plasma.private.nanoshell 2.0 as NanoShell
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
|
||||
// this is loaded and managed by indicators/providers/VolumeProvider.qml
|
||||
NanoShell.FullScreenOverlay {
|
||||
id: window
|
||||
visible: false
|
||||
color: showFullApplet ? Qt.rgba(0, 0, 0, 0.6) : "transparent"
|
||||
|
||||
property bool suppressActiveClose: false // used by context menus opened in the applet to not autoclose the osd
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {}
|
||||
}
|
||||
|
||||
property int volume: 0
|
||||
property bool showFullApplet: false
|
||||
|
||||
function showOverlay() {
|
||||
if (!window.visible) {
|
||||
window.showFullApplet = false;
|
||||
window.showMaximized();
|
||||
hideTimer.restart();
|
||||
} else if (!window.showFullApplet) { // don't autohide applet when the full applet is showing
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (!active && !suppressActiveClose) {
|
||||
hideTimer.stop();
|
||||
hideTimer.triggered();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 3000
|
||||
running: false
|
||||
onTriggered: {
|
||||
window.close();
|
||||
window.showFullApplet = false;
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
contentHeight: cards.implicitHeight
|
||||
boundsBehavior: window.showFullApplet ? Flickable.DragAndOvershootBounds : Flickable.StopAtBounds
|
||||
|
||||
pressDelay: 50
|
||||
|
||||
MouseArea {
|
||||
// capture taps behind cards to close
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
width: parent.width
|
||||
height: Math.max(cards.implicitHeight, window.height)
|
||||
onReleased: {
|
||||
hideTimer.stop();
|
||||
hideTimer.triggered();
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: cards
|
||||
width: parent.width
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
// osd card
|
||||
PopupCard {
|
||||
id: osd
|
||||
Layout.topMargin: PlasmaCore.Units.largeSpacing
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: containerLayout
|
||||
spacing: PlasmaCore.Units.smallSpacing
|
||||
|
||||
anchors.leftMargin: PlasmaCore.Units.smallSpacing * 2
|
||||
anchors.rightMargin: PlasmaCore.Units.smallSpacing
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
icon.name: !paSinkModel.preferredSink || paSinkModel.preferredSink.muted ? "audio-volume-muted" : "audio-volume-high"
|
||||
text: !paSinkModel.preferredSink || paSinkModel.preferredSink.muted ? i18n("Unmute") : i18n("Mute")
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: PlasmaCore.Units.iconSizes.medium
|
||||
Layout.preferredHeight: PlasmaCore.Units.iconSizes.medium
|
||||
Layout.rightMargin: PlasmaCore.Units.smallSpacing
|
||||
onClicked: muteVolume()
|
||||
}
|
||||
|
||||
PlasmaComponents.ProgressBar {
|
||||
id: volumeSlider
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: PlasmaCore.Units.smallSpacing * 2
|
||||
value: window.volume
|
||||
from: 0
|
||||
to: 100
|
||||
Behavior on value { NumberAnimation { duration: PlasmaCore.Units.shortDuration } }
|
||||
}
|
||||
|
||||
// Get the width of a three-digit number so we can size the label
|
||||
// to the maximum width to avoid the progress bar resizing itself
|
||||
TextMetrics {
|
||||
id: widestLabelSize
|
||||
text: i18n("100%")
|
||||
font: percentageLabel.font
|
||||
}
|
||||
|
||||
PlasmaExtra.Heading {
|
||||
id: percentageLabel
|
||||
Layout.preferredWidth: widestLabelSize.width
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: PlasmaCore.Units.smallSpacing
|
||||
level: 3
|
||||
text: i18nc("Percentage value", "%1%", window.volume)
|
||||
|
||||
// Display a subtle visual indication that the volume might be
|
||||
// dangerously high
|
||||
// ------------------------------------------------
|
||||
// Keep this in sync with the copies in plasma-pa:ListItemBase.qml
|
||||
// and plasma-pa:VolumeSlider.qml
|
||||
color: {
|
||||
if (volumeSlider.value <= 100) {
|
||||
return PlasmaCore.Theme.textColor
|
||||
} else if (volumeSlider.value > 100 && volumeSlider.value <= 125) {
|
||||
return PlasmaCore.Theme.neutralTextColor
|
||||
} else {
|
||||
return PlasmaCore.Theme.negativeTextColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
icon.name: "configure"
|
||||
text: i18n("Open audio settings")
|
||||
visible: opacity !== 0
|
||||
opacity: showFullApplet ? 1 : 0
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: PlasmaCore.Units.iconSizes.medium
|
||||
Layout.preferredHeight: PlasmaCore.Units.iconSizes.medium
|
||||
Layout.rightMargin: PlasmaCore.Units.smallSpacing
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: PlasmaCore.Units.shortDuration } }
|
||||
|
||||
onClicked: {
|
||||
let coords = mapToItem(flickable, 0, 0);
|
||||
NanoShell.StartupFeedback.open("audio-volume-high", i18n("Audio Settings"), coords.x, coords.y, PlasmaCore.Units.iconSizes.medium);
|
||||
plasmoid.nativeInterface.executeCommand("plasma-settings -m kcm_pulseaudio");
|
||||
}
|
||||
}
|
||||
|
||||
PlasmaComponents.ToolButton {
|
||||
icon.name: window.showFullApplet ? "arrow-up" : "arrow-down"
|
||||
text: i18n("Toggle showing audio streams")
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: PlasmaCore.Units.iconSizes.medium
|
||||
Layout.preferredHeight: PlasmaCore.Units.iconSizes.medium
|
||||
onClicked: {
|
||||
window.showFullApplet = !window.showFullApplet
|
||||
// don't autohide applet when full applet is shown
|
||||
if (window.showFullApplet) {
|
||||
hideTimer.stop();
|
||||
} else {
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other applet cards
|
||||
AudioApplet {
|
||||
id: applet
|
||||
Layout.topMargin: PlasmaCore.Units.largeSpacing
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: cards.width
|
||||
|
||||
opacity: window.showFullApplet ? 1 : 0
|
||||
visible: opacity !== 0
|
||||
transform: Translate {
|
||||
y: window.showFullApplet ? 0 : -PlasmaCore.Units.gridUnit
|
||||
Behavior on y { NumberAnimation {} }
|
||||
}
|
||||
|
||||
Behavior on opacity { NumberAnimation {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
containments/panel/package/contents/ui/volumeosd/icon.js
Normal file
25
containments/panel/package/contents/ui/volumeosd/icon.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
// from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/code/icon.js
|
||||
function name(volume, muted, prefix) {
|
||||
if (!prefix) {
|
||||
prefix = "audio-volume";
|
||||
}
|
||||
var icon = null;
|
||||
var percent = volume / 100;
|
||||
if (percent <= 0.0 || muted) {
|
||||
icon = prefix + "-muted";
|
||||
} else if (percent <= 0.25) {
|
||||
icon = prefix + "-low";
|
||||
} else if (percent <= 0.75) {
|
||||
icon = prefix + "-medium";
|
||||
} else {
|
||||
icon = prefix + "-high";
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue