mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
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:
parent
2871d486ae
commit
67ca0fd6fc
8 changed files with 336 additions and 305 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
131
components/mobileshell/qml/popups/volumeosd/VolumeSlider.qml
Normal file
131
components/mobileshell/qml/popups/volumeosd/VolumeSlider.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue