mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-07-26 14:24:45 +00:00
The eventual goal is to have as few singletons with state as possible in the mobileshell component when it is imported into components such as the lockscreen. This doesn't fully accomplish it, but moves the audio provider singleton to MobileShellState, which will eventually need to be prevented from importing into non plasmashell processes. This also disables the sound feedback when changing volume, since it can be a source of lag when showing the applet.
255 lines
11 KiB
QML
255 lines
11 KiB
QML
/*
|
|
* 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 Qt5Compat.GraphicalEffects
|
|
|
|
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 "icon.js" as Icon
|
|
|
|
// adapted from https://invent.kde.org/plasma/plasma-pa/-/blob/master/applet/contents/ui/ListItemBase.qml
|
|
Controls.ItemDelegate {
|
|
id: baseItem
|
|
|
|
property string label
|
|
property alias listIcon: clientIcon.source
|
|
property alias iconUsesPlasmaTheme: clientIcon.usesPlasmaTheme
|
|
property string type // sink, source, source-output
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|