mirror of
https://gitgud.io/wackyideas/aerothemeplasma.git
synced 2024-08-15 00:43:43 +00:00
607 lines
23 KiB
QML
607 lines
23 KiB
QML
/*
|
|
SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
|
|
|
|
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.plasma.core as PlasmaCore
|
|
import org.kde.ksvg as KSvg
|
|
import org.kde.kirigami as Kirigami
|
|
import org.kde.kitemmodels as KItemModels
|
|
import org.kde.plasma.components as PC3
|
|
import org.kde.plasma.extras as PlasmaExtras
|
|
import org.kde.plasma.plasmoid
|
|
|
|
import org.kde.kcmutils as KCMUtils
|
|
import org.kde.config as KConfig
|
|
|
|
import org.kde.plasma.private.volume
|
|
|
|
PlasmoidItem {
|
|
id: main
|
|
|
|
GlobalConfig {
|
|
id: config
|
|
}
|
|
|
|
property bool volumeFeedback: config.audioFeedback
|
|
property bool globalMute: config.globalMute
|
|
property string displayName: i18n("Audio Volume")
|
|
property QtObject draggedStream: null
|
|
|
|
property bool showVirtualDevices: Plasmoid.configuration.showVirtualDevices
|
|
|
|
// DEFAULT_SINK_NAME in module-always-sink.c
|
|
readonly property string dummyOutputName: "auto_null"
|
|
readonly property string noDevicePlaceholderMessage: i18n("No output or input devices found")
|
|
|
|
switchHeight: Layout.minimumHeight
|
|
switchWidth: Layout.minimumWidth
|
|
|
|
Plasmoid.icon: PreferredDevice.sink && !isDummyOutput(PreferredDevice.sink) ? AudioIcon.forVolume(volumePercent(PreferredDevice.sink.volume), PreferredDevice.sink.muted, "")
|
|
: AudioIcon.forVolume(0, true, "")
|
|
toolTipMainText: {
|
|
var sink = PreferredDevice.sink
|
|
if (!sink || isDummyOutput(sink)) {
|
|
return displayName;
|
|
}
|
|
|
|
if (sink.muted) {
|
|
return i18n("Audio Muted");
|
|
} else {
|
|
return i18n("Volume at %1%", volumePercent(sink.volume));
|
|
}
|
|
}
|
|
toolTipSubText: {
|
|
let lines = [];
|
|
|
|
if (PreferredDevice.sink && paSinkFilterModel.count > 1 && !isDummyOutput(PreferredDevice.sink)) {
|
|
lines.push(nodeName(PreferredDevice.sink))
|
|
}
|
|
|
|
if (paSinkFilterModel.count > 0) {
|
|
lines.push(main.globalMute ? i18n("Middle-click to unmute")
|
|
: i18n("Middle-click to mute all audio"));
|
|
lines.push(i18n("Scroll to adjust volume"));
|
|
} else {
|
|
lines.push(main.noDevicePlaceholderMessage);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function nodeName(pulseObject) {
|
|
const nodeNick = pulseObject.pulseProperties["node.nick"]
|
|
if (nodeNick) {
|
|
return nodeNick
|
|
}
|
|
|
|
if (pulseObject.description) {
|
|
return pulseObject.description
|
|
}
|
|
|
|
if (pulseObject.name) {
|
|
return pulseObject.name
|
|
}
|
|
|
|
return i18n("Device name not found")
|
|
}
|
|
|
|
function isDummyOutput(output) {
|
|
return output && output.name === dummyOutputName;
|
|
}
|
|
|
|
function volumePercent(volume) {
|
|
return Math.round(volume / PulseAudio.NormalVolume * 100.0);
|
|
}
|
|
|
|
function playFeedback(sinkIndex) {
|
|
if (!volumeFeedback) {
|
|
return;
|
|
}
|
|
if (sinkIndex == undefined) {
|
|
sinkIndex = PreferredDevice.sink.index;
|
|
}
|
|
feedback.play(sinkIndex);
|
|
}
|
|
|
|
// Output devices
|
|
readonly property SinkModel paSinkModel: SinkModel { id: paSinkModel }
|
|
|
|
// Input devices
|
|
readonly property SourceModel paSourceModel: SourceModel { id: paSourceModel }
|
|
|
|
// 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 output devices
|
|
readonly property PulseObjectFilterModel paSinkFilterModel: PulseObjectFilterModel {
|
|
id: paSinkFilterModel
|
|
filterOutInactiveDevices: true
|
|
filterVirtualDevices: !main.showVirtualDevices
|
|
sourceModel: paSinkModel
|
|
}
|
|
|
|
// active input devices
|
|
readonly property PulseObjectFilterModel paSourceFilterModel: PulseObjectFilterModel {
|
|
id: paSourceFilterModel
|
|
filterOutInactiveDevices: true
|
|
filterVirtualDevices: !main.showVirtualDevices
|
|
sourceModel: paSourceModel
|
|
}
|
|
|
|
// non-virtual streams going to output devices
|
|
readonly property PulseObjectFilterModel paSinkInputFilterModel: PulseObjectFilterModel {
|
|
id: paSinkInputFilterModel
|
|
filters: [ { role: "VirtualStream", value: false } ]
|
|
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
|
|
|
|
function indexOfCardNumber(cardNumber) {
|
|
const indexRole = KItemModels.KRoleNames.role("Index");
|
|
for (let idx = 0; idx < count; ++idx) {
|
|
if (data(index(idx, 0), indexRole) === cardNumber) {
|
|
return index(idx, 0);
|
|
}
|
|
}
|
|
return index(-1, 0);
|
|
}
|
|
}
|
|
|
|
// Only exists because the default CompactRepresentation doesn't expose:
|
|
// - scroll actions
|
|
// - a middle-click action
|
|
// TODO remove once it gains those features.
|
|
compactRepresentation: MouseArea {
|
|
property int wheelDelta: 0
|
|
property bool wasExpanded: false
|
|
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
|
onPressed: mouse => {
|
|
if (mouse.button == Qt.LeftButton) {
|
|
wasExpanded = main.expanded;
|
|
} else if (mouse.button == Qt.MiddleButton) {
|
|
GlobalService.globalMute();
|
|
}
|
|
}
|
|
onClicked: mouse => {
|
|
if (mouse.button == Qt.LeftButton) {
|
|
main.expanded = !wasExpanded;
|
|
}
|
|
}
|
|
onWheel: wheel => {
|
|
const delta = (wheel.inverted ? -1 : 1) * (wheel.angleDelta.y ? wheel.angleDelta.y : -wheel.angleDelta.x);
|
|
wheelDelta += delta;
|
|
// Magic number 120 for common "one click"
|
|
// See: https://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop
|
|
while (wheelDelta >= 120) {
|
|
wheelDelta -= 120;
|
|
if (wheel.modifiers & Qt.ShiftModifier) {
|
|
GlobalService.volumeUpSmall();
|
|
} else {
|
|
GlobalService.volumeUp();
|
|
}
|
|
}
|
|
while (wheelDelta <= -120) {
|
|
wheelDelta += 120;
|
|
if (wheel.modifiers & Qt.ShiftModifier) {
|
|
GlobalService.volumeDownSmall();
|
|
} else {
|
|
GlobalService.volumeDown();
|
|
}
|
|
}
|
|
}
|
|
Kirigami.Icon {
|
|
anchors.fill: parent
|
|
source: plasmoid.icon
|
|
active: parent.containsMouse
|
|
}
|
|
}
|
|
|
|
VolumeFeedback {
|
|
id: feedback
|
|
}
|
|
|
|
fullRepresentation: PlasmaExtras.Representation {
|
|
id: fullRep
|
|
|
|
Layout.minimumHeight: Kirigami.Units.gridUnit * 8
|
|
Layout.minimumWidth: Kirigami.Units.gridUnit * 14
|
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 21
|
|
Layout.preferredWidth: Kirigami.Units.gridUnit * 24
|
|
|
|
collapseMarginsHint: true
|
|
|
|
KeyNavigation.down: tabBar.currentItem
|
|
|
|
property list<string> hiddenTypes: []
|
|
|
|
function beginMoveStream(type: /* "sink" | "source" */ string) {
|
|
if (type === "sink") {
|
|
hiddenTypes = ["source"]
|
|
} else if (type === "source") {
|
|
hiddenTypes = ["sink"]
|
|
}
|
|
tabBar.setCurrentIndex(devicesTab.TabBar.index)
|
|
}
|
|
|
|
function endMoveStream() {
|
|
hiddenTypes = []
|
|
tabBar.setCurrentIndex(streamsTab.TabBar.index)
|
|
}
|
|
|
|
header: PlasmaExtras.PlasmoidHeading {
|
|
// Make this toolbar's buttons align vertically with the ones above
|
|
rightPadding: -1
|
|
// Allow tabbar to touch the header's bottom border
|
|
bottomPadding: -bottomInset
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
TabBar {
|
|
id: tabBar
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
currentIndex: {
|
|
switch (plasmoid.configuration.currentTab) {
|
|
case "devices":
|
|
return devicesTab.TabBar.index;
|
|
case "streams":
|
|
return streamsTab.TabBar.index;
|
|
}
|
|
}
|
|
|
|
KeyNavigation.down: contentView.currentItem.contentItem.upperListView.itemAtIndex(0)
|
|
|
|
onCurrentIndexChanged: {
|
|
switch (currentIndex) {
|
|
case devicesTab.TabBar.index:
|
|
plasmoid.configuration.currentTab = "devices";
|
|
break;
|
|
case streamsTab.TabBar.index:
|
|
plasmoid.configuration.currentTab = "streams";
|
|
break;
|
|
}
|
|
}
|
|
|
|
PC3.TabButton {
|
|
id: devicesTab
|
|
text: i18n("Devices")
|
|
|
|
KeyNavigation.up: fullRep.KeyNavigation.up
|
|
}
|
|
|
|
PC3.TabButton {
|
|
id: streamsTab
|
|
text: i18n("Applications")
|
|
|
|
KeyNavigation.up: fullRep.KeyNavigation.up
|
|
}
|
|
}
|
|
|
|
PC3.ToolButton {
|
|
id: globalMuteCheckbox
|
|
|
|
visible: !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading)
|
|
|
|
icon.name: "audio-volume-muted"
|
|
onClicked: {
|
|
GlobalService.globalMute()
|
|
}
|
|
checked: globalMute
|
|
|
|
Accessible.name: i18n("Force mute all playback devices")
|
|
PC3.ToolTip {
|
|
text: i18n("Force mute all playback devices")
|
|
}
|
|
}
|
|
|
|
PC3.ToolButton {
|
|
visible: !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading)
|
|
|
|
icon.name: "configure"
|
|
onClicked: plasmoid.internalAction("configure").trigger()
|
|
|
|
Accessible.name: plasmoid.internalAction("configure").text
|
|
PC3.ToolTip {
|
|
text: plasmoid.internalAction("configure").text
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: using a StackView instead of a SwipeView partly because
|
|
// SwipeView would never start with the correct initial view when the
|
|
// last saved view was the streams view.
|
|
// We also don't need to be able to swipe between views.
|
|
contentItem: HorizontalStackView {
|
|
id: contentView
|
|
initialItem: plasmoid.configuration.currentTab === "streams" ? streamsView : devicesView
|
|
movementTransitionsEnabled: currentItem !== null
|
|
TwoPartView {
|
|
id: devicesView
|
|
upperModel: paSinkFilterModel
|
|
upperType: "sink"
|
|
lowerModel: paSourceFilterModel
|
|
lowerType: "source"
|
|
iconName: "audio-volume-muted"
|
|
placeholderText: main.noDevicePlaceholderMessage
|
|
upperDelegate: DeviceListItem {
|
|
width: ListView.view.width
|
|
type: devicesView.upperType
|
|
}
|
|
lowerDelegate: DeviceListItem {
|
|
width: ListView.view.width
|
|
type: devicesView.lowerType
|
|
}
|
|
}
|
|
// NOTE: Don't unload this while dragging and dropping a stream
|
|
// to a device or else the D&D operation will be cancelled.
|
|
TwoPartView {
|
|
id: streamsView
|
|
upperModel: paSinkInputFilterModel
|
|
upperType: "sink-input"
|
|
lowerModel: paSourceOutputFilterModel
|
|
lowerType: "source-output"
|
|
iconName: "edit-none"
|
|
placeholderText: i18n("No applications playing or recording audio")
|
|
upperDelegate: StreamListItem {
|
|
width: ListView.view.width
|
|
type: streamsView.upperType
|
|
devicesModel: paSinkFilterModel
|
|
}
|
|
lowerDelegate: StreamListItem {
|
|
width: ListView.view.width
|
|
type: streamsView.lowerType
|
|
devicesModel: paSourceFilterModel
|
|
}
|
|
}
|
|
Connections {
|
|
target: tabBar
|
|
function onCurrentIndexChanged() {
|
|
if (tabBar.currentItem === devicesTab) {
|
|
contentView.reverseTransitions = false
|
|
contentView.replace(devicesView)
|
|
} else if (tabBar.currentItem === streamsTab) {
|
|
contentView.reverseTransitions = true
|
|
contentView.replace(streamsView)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component TwoPartView : PC3.ScrollView {
|
|
id: scrollView
|
|
required property PulseObjectFilterModel upperModel
|
|
required property string upperType
|
|
required property Component upperDelegate
|
|
required property PulseObjectFilterModel lowerModel
|
|
required property string lowerType
|
|
required property Component lowerDelegate
|
|
property string iconName: ""
|
|
property string placeholderText: ""
|
|
|
|
// HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
|
|
PC3.ScrollBar.horizontal.policy: PC3.ScrollBar.AlwaysOff
|
|
|
|
Loader {
|
|
parent: scrollView
|
|
anchors.centerIn: parent
|
|
width: parent.width - Kirigami.Units.gridUnit * 4
|
|
active: visible
|
|
visible: scrollView.placeholderText.length > 0 && !upperSection.visible && !lowerSection.visible
|
|
sourceComponent: PlasmaExtras.PlaceholderMessage {
|
|
iconName: scrollView.iconName
|
|
text: scrollView.placeholderText
|
|
}
|
|
}
|
|
contentItem: Flickable {
|
|
contentHeight: layout.implicitHeight
|
|
clip: true
|
|
|
|
property ListView upperListView: upperSection.visible ? upperSection : lowerSection
|
|
property ListView lowerListView: lowerSection.visible ? lowerSection : upperSection
|
|
|
|
ColumnLayout {
|
|
id: layout
|
|
width: parent.width
|
|
spacing: 0
|
|
ListView {
|
|
id: upperSection
|
|
visible: count && !fullRep.hiddenTypes.includes(scrollView.upperType)
|
|
interactive: false
|
|
Layout.fillWidth: true
|
|
implicitHeight: contentHeight
|
|
model: scrollView.upperModel
|
|
delegate: scrollView.upperDelegate
|
|
focus: visible
|
|
|
|
Keys.onDownPressed: event => {
|
|
if (currentIndex < count - 1) {
|
|
incrementCurrentIndex();
|
|
currentItem.forceActiveFocus();
|
|
} else if (lowerSection.visible) {
|
|
lowerSection.currentIndex = 0;
|
|
lowerSection.currentItem.forceActiveFocus();
|
|
} else {
|
|
raiseMaximumVolumeCheckbox.forceActiveFocus(Qt.TabFocusReason);
|
|
}
|
|
event.accepted = true;
|
|
}
|
|
Keys.onUpPressed: event => {
|
|
if (currentIndex > 0) {
|
|
decrementCurrentIndex();
|
|
currentItem.forceActiveFocus();
|
|
} else {
|
|
tabBar.currentItem.forceActiveFocus(Qt.BacktabFocusReason);
|
|
}
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
KSvg.SvgItem {
|
|
imagePath: "widgets/line"
|
|
elementId: "horizontal-line"
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Kirigami.Units.smallSpacing * 2
|
|
Layout.rightMargin: Layout.leftMargin
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
visible: upperSection.visible && lowerSection.visible
|
|
}
|
|
ListView {
|
|
id: lowerSection
|
|
visible: count && !fullRep.hiddenTypes.includes(scrollView.lowerType)
|
|
interactive: false
|
|
Layout.fillWidth: true
|
|
implicitHeight: contentHeight
|
|
model: scrollView.lowerModel
|
|
delegate: scrollView.lowerDelegate
|
|
focus: visible && !upperSection.visible
|
|
|
|
Keys.onDownPressed: event => {
|
|
if (currentIndex < count - 1) {
|
|
incrementCurrentIndex();
|
|
currentItem.forceActiveFocus();
|
|
} else {
|
|
raiseMaximumVolumeCheckbox.forceActiveFocus(Qt.TabFocusReason);
|
|
}
|
|
event.accepted = true;
|
|
}
|
|
Keys.onUpPressed: event => {
|
|
if (currentIndex > 0) {
|
|
decrementCurrentIndex();
|
|
currentItem.forceActiveFocus();
|
|
} else if (upperSection.visible) {
|
|
upperSection.currentIndex = upperSection.count - 1;
|
|
upperSection.currentItem.forceActiveFocus();
|
|
} else {
|
|
tabBar.currentItem.forceActiveFocus(Qt.BacktabFocusReason);
|
|
}
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
footer: PlasmaExtras.PlasmoidHeading {
|
|
height: fullRep.header.height
|
|
PC3.Switch {
|
|
id: raiseMaximumVolumeCheckbox
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Kirigami.Units.smallSpacing
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
checked: config.raiseMaximumVolume
|
|
|
|
Accessible.onPressAction: raiseMaximumVolumeCheckbox.toggle()
|
|
KeyNavigation.backtab: contentView.currentItem.contentItem.lowerListView.itemAtIndex(contentView.currentItem.contentItem.lowerListView.count - 1)
|
|
Keys.onUpPressed: event => {
|
|
KeyNavigation.backtab.forceActiveFocus(Qt.BacktabFocusReason);
|
|
}
|
|
|
|
text: i18n("Raise maximum volume")
|
|
|
|
onToggled: {
|
|
config.raiseMaximumVolume = checked;
|
|
config.save();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Plasmoid.contextualActions: [
|
|
PlasmaCore.Action {
|
|
text: i18n("Force mute all playback devices")
|
|
icon.name: "audio-volume-muted"
|
|
checkable: true
|
|
checked: globalMute
|
|
onTriggered: {
|
|
GlobalService.globalMute();
|
|
}
|
|
},
|
|
PlasmaCore.Action {
|
|
text: i18n("Show virtual devices")
|
|
icon.name: "audio-card"
|
|
checkable: true
|
|
checked: plasmoid.configuration.showVirtualDevices
|
|
onTriggered: Plasmoid.configuration.showVirtualDevices = !Plasmoid.configuration.showVirtualDevices
|
|
}
|
|
]
|
|
|
|
PlasmaCore.Action {
|
|
id: configureAction
|
|
text: i18n("&Configure Audio Devices…")
|
|
icon.name: "configure"
|
|
shortcut: "alt+d, s"
|
|
visible: KConfig.KAuthorized.authorizeControlModule("kcm_pulseaudio")
|
|
onTriggered: KCMUtils.KCMLauncher.openSystemSettings("kcm_pulseaudio")
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
MicrophoneIndicator.init();
|
|
Plasmoid.setInternalAction("configure", configureAction);
|
|
|
|
// migrate settings if they aren't default
|
|
// this needs to be done per instance of the applet
|
|
if (Plasmoid.configuration.migrated) {
|
|
return;
|
|
}
|
|
if (Plasmoid.configuration.volumeFeedback === false && config.audioFeedback) {
|
|
config.audioFeedback = false;
|
|
config.save();
|
|
}
|
|
if (Plasmoid.configuration.volumeStep && Plasmoid.configuration.volumeStep !== 5 && config.volumeStep === 5) {
|
|
config.volumeStep = Plasmoid.configuration.volumeStep;
|
|
config.save();
|
|
}
|
|
if (Plasmoid.configuration.raiseMaximumVolume === true && !config.raiseMaximumVolume) {
|
|
config.raiseMaximumVolume = true;
|
|
config.save();
|
|
}
|
|
if (Plasmoid.configuration.volumeOsd === false && config.volumeOsd) {
|
|
config.volumeOsd = false;
|
|
config.save();
|
|
}
|
|
if (Plasmoid.configuration.muteOsd === false && config.muteOsd) {
|
|
config.muteOsd = false;
|
|
config.save();
|
|
}
|
|
if (Plasmoid.configuration.micOsd === false && config.microphoneSensitivityOsd) {
|
|
config.microphoneSensitivityOsd = false;
|
|
config.save();
|
|
}
|
|
if (Plasmoid.configuration.globalMute === true && !config.globalMute) {
|
|
config.globalMute = true;
|
|
config.save();
|
|
}
|
|
if (Plasmoid.configuration.globalMuteDevices.length !== 0) {
|
|
for (const device in Plasmoid.configuration.globalMuteDevices) {
|
|
if (!config.globalMuteDevices.includes(device)) {
|
|
config.globalMuteDevices.push(device);
|
|
}
|
|
}
|
|
config.save();
|
|
}
|
|
Plasmoid.configuration.migrated = true;
|
|
}
|
|
}
|