389 lines
15 KiB
QML
389 lines
15 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Controls 2.0
|
|
import QtQuick.Layouts 1.15
|
|
import org.kde.plasma.core as PlasmaCore
|
|
import org.kde.plasma.components as PlasmaComponents
|
|
import org.kde.plasma.components as PC3
|
|
import org.kde.kirigami as Kirigami
|
|
import org.kde.kcmutils as KCM// KCMLauncher
|
|
import org.kde.config as KConfig // KAuthorized.authorizeControlModule
|
|
import org.kde.ksvg as KSvg
|
|
import org.kde.plasma.extras as PlasmaExtras
|
|
|
|
import org.kde.plasma.private.volume
|
|
|
|
import "../lib" as Lib
|
|
import "components/volume" as VolumeComponents
|
|
PageTemplate {
|
|
id: volumePage
|
|
|
|
sectionTitle: i18n("Audio Volume")
|
|
|
|
property int contentItemHeight: contentItem.implicitHeight
|
|
|
|
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: true
|
|
|
|
property string currentTab: "devices"
|
|
|
|
// 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")
|
|
|
|
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: !volumePage.showVirtualDevices
|
|
sourceModel: paSinkModel
|
|
}
|
|
|
|
// active input devices
|
|
readonly property PulseObjectFilterModel paSourceFilterModel: PulseObjectFilterModel {
|
|
id: paSourceFilterModel
|
|
filterOutInactiveDevices: true
|
|
filterVirtualDevices: !volumePage.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);
|
|
}
|
|
}
|
|
|
|
VolumeFeedback {
|
|
id: feedback
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: contentItem
|
|
spacing: Kirigami.Units.gridUnit
|
|
anchors.fill: parent
|
|
|
|
RowLayout {
|
|
//anchors.fill: parent
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
PC3.TabBar {
|
|
id: tabBar
|
|
Layout.fillWidth: true
|
|
//Layout.fillHeight: true
|
|
|
|
currentIndex: {
|
|
switch (volumePage.currentTab) {
|
|
case "devices":
|
|
return devicesTab.PC3.TabBar.index;
|
|
case "streams":
|
|
return streamsTab.PC3.TabBar.index;
|
|
}
|
|
}
|
|
|
|
KeyNavigation.down: contentView.currentItem.contentItem.upperListView.itemAtIndex(0)
|
|
|
|
onCurrentIndexChanged: {
|
|
switch (currentIndex) {
|
|
case devicesTab.PC3.TabBar.index:
|
|
volumePage.currentTab = "devices";
|
|
break;
|
|
case streamsTab.PC3.TabBar.index:
|
|
volumePage.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: true// !(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: true // !(plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentDrawsPlasmoidHeading)
|
|
|
|
icon.name: "configure"
|
|
onClicked: KCM.KCMLauncher.openSystemSettings("kcm_pulseaudio")
|
|
|
|
Accessible.name: i18n("Configure Audio Devices")
|
|
PC3.ToolTip {
|
|
text: i18n("Configure Audio Devices")
|
|
}
|
|
}
|
|
}
|
|
|
|
VolumeComponents.HorizontalStackView {
|
|
id: contentView
|
|
initialItem: volumePage.currentTab === "streams" ? streamsView : devicesView
|
|
movementTransitionsEnabled: currentItem !== null
|
|
Layout.fillHeight: true
|
|
Layout.fillWidth: true
|
|
TwoPartView {
|
|
id: devicesView
|
|
upperModel: paSinkFilterModel
|
|
upperType: "sink"
|
|
lowerModel: paSourceFilterModel
|
|
lowerType: "source"
|
|
iconName: "audio-volume-muted"
|
|
placeholderText: volumePage.noDevicePlaceholderMessage
|
|
upperDelegate: VolumeComponents.DeviceListItem {
|
|
width: ListView.view.width
|
|
type: devicesView.upperType
|
|
}
|
|
lowerDelegate: VolumeComponents.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: VolumeComponents.StreamListItem {
|
|
width: ListView.view.width
|
|
type: streamsView.upperType
|
|
devicesModel: paSinkFilterModel
|
|
}
|
|
lowerDelegate: VolumeComponents.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.fill: 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
|
|
anchors.fill: parent
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} |