volumeosd: Refactor and simplify logic

This commit makes a much needed cleanup and overhaul of the volume OSD.

Changes:
- Fix context menus for app streams
- Remove dedicated mute button in cards (simplifies the UI for touch
  devices)
- Add selection background for card delegates
- Sync audio models with plasma-pa
- Simplify implementation of cards
- Sync volume slider implementation with plasma-pa
This commit is contained in:
Devin Lin 2025-09-13 18:59:09 -04:00
parent 2871d486ae
commit 67ca0fd6fc
8 changed files with 336 additions and 305 deletions

View file

@ -91,6 +91,7 @@ ecm_target_qml_sources(mobileshellplugin SOURCES
qml/popups/volumeosd/VolumeChangedPopup.qml
qml/popups/volumeosd/VolumeOSD.qml
qml/popups/volumeosd/VolumeOSDProvider.qml
qml/popups/volumeosd/VolumeSlider.qml
qml/popups/PopupProviderLoader.qml
qml/statusbar/indicators/BatteryIndicator.qml

View file

@ -27,53 +27,68 @@ ColumnLayout {
property real scale: 1.0
PulseObjectFilterModel {
id: paSinkFilterModel
sortRoleName: "SortByDefault"
sortOrder: Qt.DescendingOrder
filterOutInactiveDevices: true
sourceModel: MobileShell.AudioInfo.paSinkModel
}
// Input devices
readonly property SourceModel paSourceModel: SourceModel { id: paSourceModel }
SourceModel {
id: paSourceModel
}
// Output devices
readonly property SinkModel paSinkModel: SinkModel { id: paSinkModel }
PulseObjectFilterModel {
// Confusingly, Sink Input is what PulseAudio calls streams that send audio to an output device
readonly property SinkInputModel paSinkInputModel: SinkInputModel { id: paSinkInputModel }
// Confusingly, Source Output is what PulseAudio calls streams that take audio from an input device
readonly property SourceOutputModel paSourceOutputModel: SourceOutputModel { id: paSourceOutputModel }
// Active input devices
readonly property PulseObjectFilterModel paSourceFilterModel: PulseObjectFilterModel {
id: paSourceFilterModel
sortRoleName: "SortByDefault"
sortOrder: Qt.DescendingOrder
filterOutInactiveDevices: true
filterVirtualDevices: true
sourceModel: paSourceModel
}
CardModel {
id: paCardModel
// Active output devices
readonly property PulseObjectFilterModel paSinkFilterModel: PulseObjectFilterModel {
id: paSinkFilterModel
filterOutInactiveDevices: true
filterVirtualDevices: true
sourceModel: paSinkModel
}
// ui elements
// non-virtual streams going to output devices
readonly property PulseObjectFilterModel paSinkInputFilterModel: PulseObjectFilterModel {
id: paSinkInputFilterModel
filters: [
{ role: "VirtualStream", value: false },
{ role: "Client", value: (client) => client.name !== "libcanberra" },
]
sourceModel: paSinkInputModel
}
// non-virtual streams coming from input devices
readonly property PulseObjectFilterModel paSourceOutputFilterModel: PulseObjectFilterModel {
id: paSourceOutputFilterModel
filters: [ { role: "VirtualStream", value: false } ]
sourceModel: paSourceOutputModel
}
readonly property CardModel paCardModel: CardModel { id: paCardModel }
// UI elements
PopupCard {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: audioApplet.scale
yScale: audioApplet.scale
}
scaleFactor: audioApplet.scale
contentItem: ColumnLayout {
anchors.rightMargin: Kirigami.Units.smallSpacing
anchors.leftMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
level: 2
text: i18n("Outputs")
text: i18n("Output Devices")
wrapMode: Text.Wrap
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.smallSpacing
}
Repeater {
@ -83,9 +98,8 @@ ColumnLayout {
model: paSinkFilterModel
delegate: DeviceListItem {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.smallSpacing
type: "sink"
onlyone: sinkView.count === 1
onlyOne: sinkView.count === 1
}
}
}
@ -94,24 +108,16 @@ ColumnLayout {
PopupCard {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: audioApplet.scale
yScale: audioApplet.scale
}
scaleFactor: audioApplet.scale
contentItem: ColumnLayout {
anchors.rightMargin: Kirigami.Units.smallSpacing
anchors.leftMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
level: 2
text: i18n("Inputs")
text: i18n("Input Devices")
wrapMode: Text.Wrap
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.smallSpacing
}
Repeater {
@ -121,38 +127,30 @@ ColumnLayout {
model: paSourceFilterModel
delegate: DeviceListItem {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.smallSpacing
type: "source"
onlyone: sinkView.count === 1
onlyOne: sourceView.count === 1
}
}
}
}
PopupCard {
visible: sourceInputView.model.count + sourceMediaInputView.model.count !== 0
visible: (sourceMediaInputView.count + sourceInputView.count) > 0
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: audioApplet.scale
yScale: audioApplet.scale
}
scaleFactor: audioApplet.scale
contentItem: ColumnLayout {
anchors.rightMargin: Kirigami.Units.smallSpacing
anchors.leftMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
level: 2
text: i18n("Playback Streams")
wrapMode: Text.Wrap
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.smallSpacing
}
// "Grouped" media sources (ex. Notifications)
Repeater {
id: sourceMediaInputView
Layout.fillWidth: true
@ -166,25 +164,22 @@ ColumnLayout {
Layout.margins: Kirigami.Units.smallSpacing
width: sourceOutputView.width
type: "sink-input"
devicesModel: sourceView.model
devicesModel: paSinkFilterModel
}
}
// Regular playback streams
Repeater {
id: sourceInputView
Layout.fillWidth: true
model: PulseObjectFilterModel {
filters: [ { role: "VirtualStream", value: false } ]
sourceModel: SinkInputModel {}
}
model: paSinkInputFilterModel
delegate: StreamListItem {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.smallSpacing
width: sourceOutputView.width
type: "sink-input"
devicesModel: sourceView.model
devicesModel: paSinkFilterModel
}
}
}
@ -194,40 +189,29 @@ ColumnLayout {
visible: sourceOutputView.model.count !== 0
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: audioApplet.scale
yScale: audioApplet.scale
}
scaleFactor: audioApplet.scale
contentItem: ColumnLayout {
anchors.rightMargin: Kirigami.Units.smallSpacing
anchors.leftMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Kirigami.Heading {
level: 2
text: i18n("Recording Streams")
wrapMode: Text.Wrap
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.leftMargin: Kirigami.Units.smallSpacing
}
Repeater {
id: sourceOutputView
Layout.fillWidth: true
model: PulseObjectFilterModel {
filters: [ { role: "VirtualStream", value: false } ]
sourceModel: SourceOutputModel {}
}
model: paSourceOutputFilterModel
delegate: StreamListItem {
Layout.fillWidth: true
Layout.margins: Kirigami.Units.smallSpacing
width: sourceOutputView.width
type: "source-output"
devicesModel: sourceView.model
devicesModel: paSourceFilterModel
}
}
}

View file

@ -11,25 +11,24 @@ 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
readonly property var currentPort: model.Ports[model.ActivePortIndex]
readonly property var currentActivePortIndex: model.ActivePortIndex
readonly property var currentMuted: model.Muted
readonly property var activePortIndex: model.ActivePortIndex
label: {
if (currentPort && currentPort.description) {
if (onlyone || !Description) {
if (onlyOne || !model.Description) {
return currentPort.description;
} else {
return i18nc("label of device items", "%1 (%2)", currentPort.description, Description);
return i18nc("label of device items", "%1 (%2)", currentPort.description, model.Description);
}
}
if (Description) {
return Description;
if (model.Description) {
return model.Description;
}
if (Name) {
return Name;
if (model.Name) {
return model.Name;
}
return i18n("Device name not found");
}

View file

@ -6,52 +6,57 @@
* 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 QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami 2.20 as Kirigami
import org.kde.ksvg 1.0 as KSvg
import org.kde.kquickcontrolsaddons 2.0
import org.kde.kirigami as Kirigami
import org.kde.ksvg as KSvg
import org.kde.kquickcontrolsaddons
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.volume 0.1
import org.kde.plasma.components as PlasmaComponents
import org.kde.plasma.extras as PlasmaExtras
import org.kde.plasma.private.volume
import "icon.js" as Icon
// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/ListItemBase.qml
Controls.Control {
Controls.AbstractButton {
id: baseItem
property string label
property alias listIcon: clientIcon.source
property string type // sink, source, source-output
MouseArea {
id: clickArea
anchors.fill: parent
z: -1
onClicked: {
if (selectButton.visible) {
model.PulseObject.default = true;
}
}
property bool onlyOne: false
// Whether this item is selected
readonly property bool supportsSelection: (baseItem.type == "sink" || baseItem.type == "source")
readonly property bool selected: supportsSelection && (model.PulseObject.hasOwnProperty("default") ? model.PulseObject.default : false)
onClicked: {
// Set as the default audio device
model.PulseObject.default = true
}
topPadding: Kirigami.Units.mediumSpacing
bottomPadding: Kirigami.Units.mediumSpacing
leftPadding: Kirigami.Units.mediumSpacing
rightPadding: Kirigami.Units.mediumSpacing
background: Rectangle {
radius: Kirigami.Units.cornerRadius
// border.width: 1
// border.color: baseItem.selected ? Kirigami.Theme.highlightColor : 'transparent'
color: (baseItem.selected || baseItem.down)
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.3)
: 'transparent'
}
contentItem: RowLayout {
id: row
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.RadioButton {
id: selectButton
Layout.alignment: Qt.AlignTop
Layout.topMargin: Math.round(row.height / 2 - implicitHeight - Kirigami.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
Kirigami.Icon {
id: clientIcon
@ -68,6 +73,24 @@ Controls.Control {
}
}
RowLayout {
spacing: 0
Layout.maximumWidth: Infinity // Ignore maximum width of children
visible: (baseItem.type === "sink" || baseItem.type === "source") && !baseItem.onlyOne
PlasmaComponents.RadioButton {
id: defaultButton
Accessible.ignored: true // read out from delegate
activeFocusOnTab: false // toggle from delegate
checked: model.PulseObject?.default ?? false
onToggled: {
if (checked) {
baseItem.click();
}
}
}
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
@ -87,9 +110,10 @@ Controls.Control {
}
PlasmaComponents.ToolButton {
id: viewButton
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: -Kirigami.Units.smallSpacing
icon.name: "application-menu"
icon.name: "view-more-symbolic"
checkable: true
checked: contextMenu.visible && contextMenu.visualParent === this
visible: contextMenu.hasContent
@ -132,94 +156,26 @@ Controls.Control {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
PlasmaComponents.ToolButton {
icon.name: Icon.name(Volume / PulseAudio.NormalVolume * 100.0, Muted)
text: Muted ? i18n("Unmute") : i18n("Mute")
display: Controls.AbstractButton.IconOnly
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
onClicked: {
Muted = !Muted
}
}
// this slider was effectively copied from the source (linked at the top of the file)
PlasmaComponents.Slider {
VolumeSlider {
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: model.HasVolume === true // (may be undefined)
to: model.Volume >= PulseAudio.NormalVolume * 1.01 ? PulseAudio.MaximalVolume : PulseAudio.NormalVolume
stepSize: PulseAudio.NormalVolume / 100.0
property real myStepSize: PulseAudio.NormalVolume / 100.0
visible: model.HasVolume !== false // Devices always have volume but Streams don't necessarily
enabled: model.VolumeWritable
opacity: Muted ? 0.5 : 1
muted: model.Muted
volumeObject: model.PulseObject
activeFocusOnTab: false // access from delegate
Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", baseItem.label)
background: KSvg.FrameSvgItem {
imagePath: "widgets/slider"
prefix: "groove"
width: parent.availableWidth
height: margins.top + margins.bottom
anchors.centerIn: parent
scale: parent.mirrored ? -1 : 1
KSvg.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: Kirigami.Units.shortDuration
easing.type: Easing.OutQuad
}
}
}
value: to, model.Volume
onMoved: {
model.Volume = value;
model.Muted = value === 0;
}
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
@ -227,14 +183,28 @@ Controls.Control {
// 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();
value = Qt.binding(() => model.Volume);
}
}
Timer {
id: updateTimer
interval: 200
onTriggered: slider.value = Volume
function updateVolume() {
if (model.Volume > PulseAudio.NormalVolume) {
model.Volume = PulseAudio.NormalVolume;
}
}
SequentialAnimation {
id: seqAnimation
NumberAnimation {
id: toAnimation
target: slider
property: "to"
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutQuad
}
ScriptAction {
script: slider.updateVolume()
}
}
}
PlasmaComponents.Label {
@ -264,8 +234,4 @@ Controls.Control {
}
}
}
function setVolumeByPercent(targetPercent) {
model.PulseObject.volume = Math.round(PulseAudio.NormalVolume * (targetPercent/100));
}
}

View file

@ -18,13 +18,21 @@ import org.kde.ksvg 1.0 as KSvg
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
// capture presses on the audio applet so it doesn't close the overlay
Controls.Control {
id: content
implicitWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2)
padding: Kirigami.Units.smallSpacing * 2
property bool popupBackground: false
property real scaleFactor: 1.0
implicitWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2)
padding: Kirigami.Units.largeSpacing
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: scaleFactor
yScale: scaleFactor
}
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false

View file

@ -44,7 +44,7 @@ Window {
if (cards.state == "closed") {
hideTimer.stop();
window.open();
} else if (!volumeSlider.isPressed) {
} else if (!volumeSlider.pressed) {
hideTimer.restart();
}
}
@ -154,81 +154,41 @@ Window {
property int volumePercent: PreferredDevice.sink.volume / PulseAudio.NormalVolume * 100.0
PlasmaComponents.ToolButton {
icon.name: !PreferredDevice.sink || PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon
text: !PreferredDevice.sink || PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute")
icon.name: !PreferredDevice.sink || (PreferredDevice.sink.muted ? "audio-volume-muted" : MobileShell.AudioInfo.icon)
text: !PreferredDevice.sink || (PreferredDevice.sink.muted ? i18n("Unmute") : i18n("Mute"))
display: Controls.AbstractButton.IconOnly
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
Layout.rightMargin: Kirigami.Units.smallSpacing
onClicked: {
hideTimer.restart();
PreferredDevice.sink.muted = !PreferredDevice.sink.muted;
}
}
PlasmaComponents.Slider {
VolumeSlider {
id: volumeSlider
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: Kirigami.Units.smallSpacing * 2
property real volume: PreferredDevice.sink.volume
property bool muted: PreferredDevice.sink.muted
property bool ignoreValueChange: false
property bool isPressed: false
from: PulseAudio.MinimalVolume
to: PulseAudio.NormalVolume
stepSize: to / (to / PulseAudio.NormalVolume * 100.0)
opacity: muted ? 0.5 : 1.0
Component.onCompleted: {
ignoreValueChange = false;
volumeObject: PreferredDevice.sink
muted: PreferredDevice.sink.muted
value: PreferredDevice.sink.volume
onMoved: {
PreferredDevice.sink.volume = value;
PreferredDevice.sink.muted = value === 0;
}
onVolumeChanged: {
if (!window.visible) {
return;
}
var oldIgnoreValueChange = ignoreValueChange;
ignoreValueChange = true;
value = muted ? 0 : PreferredDevice.sink.volume;
ignoreValueChange = oldIgnoreValueChange;
if (volumeSlider.isPressed) {
return;
}
window.open();
hideTimer.restart();
}
onMutedChanged: {
var oldIgnoreValueChange = ignoreValueChange;
ignoreValueChange = true;
value = muted ? 0 : PreferredDevice.sink.volume;
ignoreValueChange = oldIgnoreValueChange;
if (!window.visible || volumeSlider.isPressed) {
return;
}
window.open();
hideTimer.restart();
}
onValueChanged: {
if (!ignoreValueChange) {
PreferredDevice.sink.muted = false;
PreferredDevice.sink.volume = value;
if (!volumeSlider.isPressed) {
updateTimer.restart();
}
}
}
onPressedChanged: {
volumeSlider.isPressed = pressed;
if (pressed) {
window.open();
hideTimer.stop();
} else {
// Make sure to sync the volume once the button was
@ -236,16 +196,10 @@ Window {
// Otherwise it might be that the slider is at v10
// whereas PA rejected the volume change and is
// still at v15 (e.g.).
value = Qt.binding(() => PreferredDevice.sink.volume);
hideTimer.restart();
updateTimer.restart();
}
}
Timer {
id: updateTimer
interval: 200
onTriggered: volumeSlider.value = PreferredDevice.sink.volume
}
}
PlasmaComponents.ToolButton {

View file

@ -177,44 +177,32 @@ Window {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: Kirigami.Units.gridUnit
transform: Scale {
origin.x: Math.round(implicitWidth / 2)
origin.y: Math.round(height / 2)
xScale: flickable.scale
yScale: flickable.scale
}
scaleFactor: flickable.scale
contentItem: RowLayout {
contentItem: PlasmaComponents.ToolButton {
id: audioSettingsButton
PlasmaComponents.ToolButton {
property int addedPadding: Kirigami.Units.smallSpacing * 2
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.preferredWidth: parent.width - addedPadding * 2
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
Layout.margins: addedPadding
contentItem: Item {
anchors.fill: parent
RowLayout {
spacing: Kirigami.Units.largeSpacing
anchors.centerIn: parent
Kirigami.Icon {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
source: "settings-configure"
}
PlasmaComponents.Label {
text: i18n("Open audio settings")
anchors.verticalCenter: parent.verticalCenter
}
contentItem: Item {
anchors.fill: parent
RowLayout {
spacing: Kirigami.Units.largeSpacing
anchors.centerIn: parent
Kirigami.Icon {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
source: "settings-configure"
}
PlasmaComponents.Label {
text: i18n("Open audio settings")
anchors.verticalCenter: parent.verticalCenter
}
}
}
onClicked: {
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_pulseaudio");
window.close();
}
onClicked: {
MobileShell.ShellUtil.executeCommand("plasma-open-settings kcm_pulseaudio");
window.close();
}
}
}

View file

@ -0,0 +1,131 @@
/*
SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
SPDX-FileCopyrightText: 2019 Sefa Eyeoglu <contact@scrumplex.net>
SPDX-FileCopyrightText: 2022 ivan (@ratijas) tkachenko <me@ratijas.tk>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick
import QtQuick.Layouts
import org.kde.kquickcontrolsaddons
import org.kde.plasma.components as PC3
import org.kde.ksvg as KSvg
import org.kde.kirigami as Kirigami
import org.kde.plasma.private.volume
// Audio volume slider. Value represents desired volume level in
// device-specific units, while volume property reports current volume level
// normalized to 0..1 range.
PC3.Slider {
id: control
property VolumeObject volumeObject
// When muted, the whole slider will appear slightly faded, but remain
// functional and interactive.
property bool muted: false
// Current (monitored) volume. To be animated. Do not update too fast
// (i.e. faster or close to screen refresh rate), otherwise it won't
// animate smoothly.
property real volume: meter.volume
VolumeMonitor {
id: meter
target: control.visible ? control.volumeObject : null
}
Behavior on volume {
NumberAnimation {
id: animate
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutQuad
}
}
// When a maximum volume limit is raised/lower, animate the change.
Behavior on to {
NumberAnimation {
duration: Kirigami.Units.shortDuration
easing.type: Easing.InOutQuad
}
}
opacity: muted ? 0.5 : 1
// Prevents the groove from showing through the handle
layer.enabled: opacity < 1
wheelEnabled: false
// `wheelEnabled: true` doesn't work we can't both respect stepsize
// on scroll and allow fine-tuning on drag.
// So we have to implement the scroll handling ourselves. See
// https://bugreports.qt.io/browse/QTBUG-93081
WheelHandler {
orientation: Qt.Vertical | Qt.Horizontal
property int wheelDelta: 0
acceptedButtons: Qt.NoButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: wheel => {
const lastValue = control.value
// We want a positive delta to increase the slider for up/right scrolling,
// independently of the scrolling inversion setting
// The x-axis is also inverted (scrolling right produce negative values)
const delta = (wheel.angleDelta.y || -wheel.angleDelta.x) * (wheel.inverted ? -1 : 1)
wheelDelta += delta;
// magic number 120 for common "one click"
// See: https://doc.qt.io/qt-6/qml-qtquick-wheelevent.html#angleDelta-prop
while (wheelDelta >= 120) {
wheelDelta -= 120;
control.increase();
}
while (wheelDelta <= -120) {
wheelDelta += 120;
control.decrease();
}
if (lastValue !== control.value) {
control.moved();
}
}
}
background: KSvg.FrameSvgItem {
imagePath: "widgets/slider"
prefix: "groove"
implicitWidth: control.horizontal ? Kirigami.Units.gridUnit * 12 : fixedMargins.left + fixedMargins.right
implicitHeight: control.vertical ? Kirigami.Units.gridUnit * 12 : fixedMargins.top + fixedMargins.bottom
width: control.horizontal ? Math.max(fixedMargins.left + fixedMargins.right, control.availableWidth) : implicitWidth
height: control.vertical ? Math.max(fixedMargins.top + fixedMargins.bottom, control.availableHeight) : implicitHeight
x: control.leftPadding + (control.horizontal ? 0 : Math.round((control.availableWidth - width) / 2))
y: control.topPadding + (control.vertical ? 0 : Math.round((control.availableHeight - height) / 2))
KSvg.FrameSvgItem {
imagePath: "widgets/slider"
prefix: "groove-highlight"
anchors.left: parent.left
anchors.bottom: parent.bottom
LayoutMirroring.enabled: control.mirrored
width: control.horizontal ? Math.max(fixedMargins.left + fixedMargins.right, Math.round(control.position * (control.availableWidth - control.handle.width / 2) + (control.handle.width / 2))) : parent.width
height: control.vertical ? Math.max(fixedMargins.top + fixedMargins.bottom, Math.round(control.position * (control.availableHeight - control.handle.height / 2) + (control.handle.height / 2))) : parent.height
}
KSvg.FrameSvgItem {
imagePath: "widgets/slider"
prefix: "groove-highlight"
status: KSvg.FrameSvgItem.Selected
visible: meter.available && control.volume > 0
anchors.left: parent.left
anchors.bottom: parent.bottom
LayoutMirroring.enabled: control.mirrored
width: control.horizontal ? Math.max(fixedMargins.left + fixedMargins.right, Math.round(control.volume * control.position * control.availableWidth)) : parent.width
height: control.vertical ? Math.max(fixedMargins.top + fixedMargins.bottom, Math.round(control.volume * control.position * control.availableHeight)) : parent.height
}
}
}