diff --git a/applet/contents/ui/main.qml b/applet/contents/ui/main.qml index 3fba8d0..69911ff 100644 --- a/applet/contents/ui/main.qml +++ b/applet/contents/ui/main.qml @@ -1,393 +1,393 @@ /* Copyright 2014-2015 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import QtQuick 2.0 import QtQuick.Layouts 1.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.private.volume 0.1 import "../code/icon.js" as Icon Item { id: main property int maxVolumePercent: Plasmoid.configuration.maximumVolume property int maxVolumeValue: Math.round(maxVolumePercent * PulseAudio.NormalVolume / 100.0) property int volumeStep: Math.round(Plasmoid.configuration.volumeStep * PulseAudio.NormalVolume / 100.0) property string displayName: i18n("Audio Volume") property QtObject draggedStream: null Layout.minimumHeight: units.gridUnit * 12 Layout.minimumWidth: units.gridUnit * 12 Layout.preferredHeight: units.gridUnit * 20 Layout.preferredWidth: units.gridUnit * 20 - Plasmoid.icon: sinkModel.defaultSink ? Icon.name(sinkModel.defaultSink.volume, sinkModel.defaultSink.muted) : Icon.name(0, true) + Plasmoid.icon: sinkModel.preferredSink ? Icon.name(sinkModel.preferredSink.volume, sinkModel.preferredSink.muted) : Icon.name(0, true) Plasmoid.switchWidth: units.gridUnit * 12 Plasmoid.switchHeight: units.gridUnit * 12 Plasmoid.toolTipMainText: displayName - Plasmoid.toolTipSubText: sinkModel.defaultSink ? i18n("Volume at %1%\n%2", volumePercent(sinkModel.defaultSink.volume), sinkModel.defaultSink.description) : "" + Plasmoid.toolTipSubText: sinkModel.preferredSink ? i18n("Volume at %1%\n%2", volumePercent(sinkModel.preferredSink.volume), sinkModel.preferredSink.description) : "" function boundVolume(volume) { return Math.max(PulseAudio.MinimalVolume, Math.min(volume, maxVolumeValue)); } function volumePercent(volume, max) { if (!max) { max = PulseAudio.NormalVolume; } return Math.round(volume / max * 100.0); } function increaseVolume() { - if (!sinkModel.defaultSink) { + if (!sinkModel.preferredSink) { return; } - var volume = boundVolume(sinkModel.defaultSink.volume + volumeStep); - sinkModel.defaultSink.muted = false; - sinkModel.defaultSink.volume = volume; + var volume = boundVolume(sinkModel.preferredSink.volume + volumeStep); + sinkModel.preferredSink.muted = false; + sinkModel.preferredSink.volume = volume; osd.show(volumePercent(volume, maxVolumeValue)); } function decreaseVolume() { - if (!sinkModel.defaultSink) { + if (!sinkModel.preferredSink) { return; } - var volume = boundVolume(sinkModel.defaultSink.volume - volumeStep); - sinkModel.defaultSink.muted = false; - sinkModel.defaultSink.volume = volume; + var volume = boundVolume(sinkModel.preferredSink.volume - volumeStep); + sinkModel.preferredSink.muted = false; + sinkModel.preferredSink.volume = volume; osd.show(volumePercent(volume, maxVolumeValue)); } function muteVolume() { - if (!sinkModel.defaultSink) { + if (!sinkModel.preferredSink) { return; } - var toMute = !sinkModel.defaultSink.muted; - sinkModel.defaultSink.muted = toMute; - osd.show(toMute ? 0 : volumePercent(sinkModel.defaultSink.volume, maxVolumeValue)); + var toMute = !sinkModel.preferredSink.muted; + sinkModel.preferredSink.muted = toMute; + osd.show(toMute ? 0 : volumePercent(sinkModel.preferredSink.volume, maxVolumeValue)); } function increaseMicrophoneVolume() { if (!sourceModel.defaultSource) { return; } var volume = boundVolume(sourceModel.defaultSource.volume + volumeStep); sourceModel.defaultSource.muted = false; sourceModel.defaultSource.volume = volume; osd.showMicrophone(volumePercent(volume)); } function decreaseMicrophoneVolume() { if (!sourceModel.defaultSource) { return; } var volume = boundVolume(sourceModel.defaultSource.volume - volumeStep); sourceModel.defaultSource.muted = false; sourceModel.defaultSource.volume = volume; osd.showMicrophone(volumePercent(volume)); } function muteMicrophone() { if (!sourceModel.defaultSource) { return; } var toMute = !sourceModel.defaultSource.muted; sourceModel.defaultSource.muted = toMute; osd.showMicrophone(toMute? 0 : volumePercent(sourceModel.defaultSource.volume)); } function beginMoveStream(type, stream) { if (type == "sink") { sourceView.visible = false; sourceViewHeader.visible = false; } else if (type == "source") { sinkView.visible = false; sinkViewHeader.visible = false; } tabBar.currentTab = devicesTab; } function endMoveStream() { tabBar.currentTab = streamsTab; sourceView.visible = true; sourceViewHeader.visible = true; sinkView.visible = true; sinkViewHeader.visible = true; } Plasmoid.compactRepresentation: PlasmaCore.IconItem { source: plasmoid.icon active: mouseArea.containsMouse colorGroup: PlasmaCore.ColorScope.colorGroup MouseArea { id: mouseArea property int wheelDelta: 0 property bool wasExpanded: false anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.MiddleButton onPressed: { if (mouse.button == Qt.LeftButton) { wasExpanded = plasmoid.expanded; } else if (mouse.button == Qt.MiddleButton) { muteVolume(); } } onClicked: { if (mouse.button == Qt.LeftButton) { plasmoid.expanded = !wasExpanded; } } onWheel: { var delta = wheel.angleDelta.y || wheel.angleDelta.x; wheelDelta += delta; // Magic number 120 for common "one click" // See: http://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop while (wheelDelta >= 120) { wheelDelta -= 120; increaseVolume(); } while (wheelDelta <= -120) { wheelDelta += 120; decreaseVolume(); } } } } GlobalActionCollection { // KGlobalAccel cannot transition from kmix to something else, so if // the user had a custom shortcut set for kmix those would get lost. // To avoid this we hijack kmix name and actions. Entirely mental but // best we can do to not cause annoyance for the user. // The display name actually is updated to whatever registered last // though, so as far as user visible strings go we should be fine. // As of 2015-07-21: // componentName: kmix // actions: increase_volume, decrease_volume, mute name: "kmix" displayName: main.displayName GlobalAction { objectName: "increase_volume" text: i18n("Increase Volume") shortcut: Qt.Key_VolumeUp onTriggered: increaseVolume() } GlobalAction { objectName: "decrease_volume" text: i18n("Decrease Volume") shortcut: Qt.Key_VolumeDown onTriggered: decreaseVolume() } GlobalAction { objectName: "mute" text: i18n("Mute") shortcut: Qt.Key_VolumeMute onTriggered: muteVolume() } GlobalAction { objectName: "increase_microphone_volume" text: i18n("Increase Microphone Volume") shortcut: Qt.Key_MicVolumeUp onTriggered: increaseMicrophoneVolume() } GlobalAction { objectName: "decrease_microphone_volume" text: i18n("Decrease Microphone Volume") shortcut: Qt.Key_MicVolumeDown onTriggered: decreaseMicrophoneVolume() } GlobalAction { objectName: "mic_mute" text: i18n("Mute Microphone") shortcut: Qt.Key_MicMute onTriggered: muteMicrophone() } } VolumeOSD { id: osd } PlasmaComponents.TabBar { id: tabBar anchors { top: parent.top left: parent.left right: parent.right } PlasmaComponents.TabButton { id: devicesTab text: i18n("Devices") } PlasmaComponents.TabButton { id: streamsTab text: i18n("Applications") } } PlasmaExtras.ScrollArea { id: scrollView; anchors { top: tabBar.bottom topMargin: units.smallSpacing left: parent.left right: parent.right bottom: parent.bottom } horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff flickableItem.boundsBehavior: Flickable.StopAtBounds; Item { width: streamsView.visible ? streamsView.width : devicesView.width height: streamsView.visible ? streamsView.height : devicesView.height ColumnLayout { id: streamsView visible: tabBar.currentTab == streamsTab property int maximumWidth: scrollView.viewport.width width: maximumWidth Layout.maximumWidth: maximumWidth Header { Layout.fillWidth: true visible: sinkInputView.count > 0 text: i18n("Playback Streams") } ListView { id: sinkInputView Layout.fillWidth: true Layout.minimumHeight: contentHeight Layout.maximumHeight: contentHeight model: PulseObjectFilterModel { filters: [ { role: "VirtualStream", value: false } ] sourceModel: SinkInputModel {} } boundsBehavior: Flickable.StopAtBounds; delegate: StreamListItem { type: "sink-input" draggable: sinkView.count > 1 } } Header { Layout.fillWidth: true visible: sourceOutputView.count > 0 text: i18n("Capture Streams") } ListView { id: sourceOutputView Layout.fillWidth: true Layout.minimumHeight: contentHeight Layout.maximumHeight: contentHeight model: PulseObjectFilterModel { filters: [ { role: "VirtualStream", value: false } ] sourceModel: SourceOutputModel {} } boundsBehavior: Flickable.StopAtBounds; delegate: StreamListItem { type: "source-input" draggable: sourceView.count > 1 } } } ColumnLayout { id: devicesView visible: tabBar.currentTab == devicesTab property int maximumWidth: scrollView.viewport.width width: maximumWidth Layout.maximumWidth: maximumWidth Header { id: sinkViewHeader Layout.fillWidth: true visible: sinkView.count > 0 text: i18n("Playback Devices") } ListView { id: sinkView Layout.fillWidth: true Layout.minimumHeight: contentHeight Layout.maximumHeight: contentHeight model: PulseObjectFilterModel { sortRole: "SortByDefault" sortOrder: Qt.DescendingOrder sourceModel: SinkModel { id: sinkModel } } boundsBehavior: Flickable.StopAtBounds; delegate: DeviceListItem { type: "sink" } } Header { id: sourceViewHeader Layout.fillWidth: true visible: sourceView.count > 0 text: i18n("Capture Devices") } ListView { id: sourceView Layout.fillWidth: true Layout.minimumHeight: contentHeight Layout.maximumHeight: contentHeight model: PulseObjectFilterModel { sortRole: "SortByDefault" sortOrder: Qt.DescendingOrder sourceModel: SourceModel { id: sourceModel } } boundsBehavior: Flickable.StopAtBounds; delegate: DeviceListItem { type: "source" } } } } } } diff --git a/src/device.cpp b/src/device.cpp index 5f30d1c..13fc81f 100644 --- a/src/device.cpp +++ b/src/device.cpp @@ -1,51 +1,72 @@ /* Copyright 2014-2015 Harald Sitter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "device.h" +QPulseAudio::Device::State QPulseAudio::Device::state() const +{ + return m_state; +} + QString QPulseAudio::Device::name() const { return m_name; } QString QPulseAudio::Device::description() const { return m_description; } quint32 QPulseAudio::Device::cardIndex() const { return m_cardIndex; } QList QPulseAudio::Device::ports() const { return m_ports; } quint32 QPulseAudio::Device::activePortIndex() const { return m_activePortIndex; } QPulseAudio::Device::Device(QObject *parent) : VolumeObject(parent) { } + +QPulseAudio::Device::State QPulseAudio::Device::stateFromPaState(int value) const +{ + switch (value) { + case -1: // PA_X_INVALID_STATE + return InvalidState; + case 0: // PA_X_RUNNING + return RunningState; + case 1: // PA_X_IDLE + return IdleState; + case 2: // PA_X_SUSPENDED + return SuspendedState; + default: + return UnknownState; + } +} diff --git a/src/device.h b/src/device.h index 289a8d7..f60d8e4 100644 --- a/src/device.h +++ b/src/device.h @@ -1,112 +1,133 @@ /* Copyright 2014-2015 Harald Sitter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef DEVICE_H #define DEVICE_H #include #include #include "volumeobject.h" #include "port.h" #include "pulseobject.h" namespace QPulseAudio { class Q_DECL_EXPORT Device : public VolumeObject { Q_OBJECT + Q_PROPERTY(State state READ state NOTIFY stateChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) Q_PROPERTY(quint32 cardIndex READ cardIndex NOTIFY cardIndexChanged) Q_PROPERTY(QList ports READ ports NOTIFY portsChanged) Q_PROPERTY(quint32 activePortIndex READ activePortIndex WRITE setActivePortIndex NOTIFY activePortIndexChanged) Q_PROPERTY(bool default READ isDefault WRITE setDefault NOTIFY defaultChanged) public: + enum State { + InvalidState = 0, + RunningState, + IdleState, + SuspendedState, + UnknownState + }; + Q_ENUMS(State); + virtual ~Device() {} template void updateDevice(const PAInfo *info) { updateVolumeObject(info); if (m_name != info->name) { m_name = info->name; emit nameChanged(); } if (m_description != info->description) { m_description = info->description; emit descriptionChanged(); } m_cardIndex = info->card; emit cardIndexChanged(); // TODO: this rebuilds the entire port list on every update. would be // nicer if it actually removed what needs removing updates what needs // updating and adds what needs adding. Alas, this is a tad more // involved. qDeleteAll(m_ports); m_ports.clear(); for (auto **ports = info->ports; ports && *ports != nullptr; ++ports) { Port *port = new Port(this); port->setInfo(*ports); m_ports.append(port); if (info->active_port == *ports) { m_activePortIndex = m_ports.length() - 1; } } emit portsChanged(); emit activePortIndexChanged(); + + State infoState = stateFromPaState(info->state); + if (infoState != m_state) { + m_state = infoState; + emit stateChanged(); + } } + State state() const; QString name() const; QString description() const; quint32 cardIndex() const; QList ports() const; quint32 activePortIndex() const; virtual void setActivePortIndex(quint32 port_index) = 0; virtual bool isDefault() const = 0; virtual void setDefault(bool enable) = 0; signals: + void stateChanged(); void nameChanged(); void descriptionChanged(); void cardIndexChanged(); void portsChanged(); void activePortIndexChanged(); void defaultChanged(); protected: Device(QObject *parent); private: + State stateFromPaState(int value) const; + QString m_name; QString m_description; quint32 m_cardIndex = -1; QList m_ports; quint32 m_activePortIndex = -1; + State m_state = UnknownState; }; } // QPulseAudio #endif // DEVICE_H diff --git a/src/pulseaudio.cpp b/src/pulseaudio.cpp index 0106a5f..f58bb6f 100644 --- a/src/pulseaudio.cpp +++ b/src/pulseaudio.cpp @@ -1,262 +1,350 @@ /* Copyright 2014-2015 Harald Sitter Copyright 2016 David Rosca This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "pulseaudio.h" #include "debug.h" #include "card.h" #include "sink.h" #include "sinkinput.h" #include "source.h" #include "sourceoutput.h" #include "server.h" #include "streamrestore.h" #include namespace QPulseAudio { AbstractModel::AbstractModel(const MapBaseQObject *map, QObject *parent) : QAbstractListModel(parent) , m_map(map) { connect(m_map, &MapBaseQObject::added, this, &AbstractModel::onDataAdded); connect(m_map, &MapBaseQObject::removed, this, &AbstractModel::onDataRemoved); } QHash AbstractModel::roleNames() const { if (!m_roles.empty()) { qCDebug(PLASMAPA) << "returning roles" << m_roles; return m_roles; } Q_UNREACHABLE(); return QHash(); } int AbstractModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_map->count(); } QVariant AbstractModel::data(const QModelIndex &index, int role) const { QObject *data = m_map->objectAt(index.row()); Q_ASSERT(data); if (role == PulseObjectRole) { return QVariant::fromValue(data); } int property = m_objectProperties.value(role, -1); if (property == -1) { return QVariant(); } return data->metaObject()->property(property).read(data); } bool AbstractModel::setData(const QModelIndex &index, const QVariant &value, int role) { int propertyIndex = m_objectProperties.value(role, -1); if (propertyIndex == -1) { return false; } QObject *data = m_map->objectAt(index.row()); auto property = data->metaObject()->property(propertyIndex); return property.write(data, value); } int AbstractModel::role(const QByteArray &roleName) const { qCDebug(PLASMAPA) << roleName << m_roles.key(roleName, -1); return m_roles.key(roleName, -1); } void AbstractModel::initRoleNames(const QMetaObject &qobjectMetaObject) { m_roles[PulseObjectRole] = QByteArrayLiteral("PulseObject"); QMetaEnum enumerator; for (int i = 0; i < metaObject()->enumeratorCount(); ++i) { if (metaObject()->enumerator(i).name() == QLatin1String("ItemRole")) { enumerator = metaObject()->enumerator(i); break; } } for (int i = 0; i < enumerator.keyCount(); ++i) { // Clip the Role suffix and glue it in the hash. const int roleLength = 4; QByteArray key(enumerator.key(i)); // Enum values must end in Role or the enum is crap Q_ASSERT(key.right(roleLength) == QByteArrayLiteral("Role")); key.chop(roleLength); m_roles[enumerator.value(i)] = key; } int maxEnumValue = -1; for (auto it = m_roles.constBegin(); it != m_roles.constEnd(); ++it) { if (it.key() > maxEnumValue) { maxEnumValue = it.key(); } } Q_ASSERT(maxEnumValue != -1); auto mo = qobjectMetaObject; for (int i = 0; i < mo.propertyCount(); ++i) { QMetaProperty property = mo.property(i); QString name(property.name()); name.replace(0, 1, name.at(0).toUpper()); m_roles[++maxEnumValue] = name.toLatin1(); m_objectProperties.insert(maxEnumValue, i); if (!property.hasNotifySignal()) { continue; } m_signalIndexToProperties.insert(property.notifySignalIndex(), i); } qCDebug(PLASMAPA) << m_roles; // Connect to property changes also with objects already in model for (int i = 0; i < m_map->count(); ++i) { onDataAdded(i); } } void AbstractModel::propertyChanged() { if (!sender() || senderSignalIndex() == -1) { return; } int propertyIndex = m_signalIndexToProperties.value(senderSignalIndex(), -1); if (propertyIndex == -1) { return; } int role = m_objectProperties.key(propertyIndex, -1); if (role == -1) { return; } int index = m_map->indexOfObject(sender()); qCDebug(PLASMAPA) << "PROPERTY CHANGED (" << index << ") :: " << role << roleNames().value(role); emit dataChanged(createIndex(index, 0), createIndex(index, 0), {role}); } void AbstractModel::onDataAdded(int index) { beginInsertRows(QModelIndex(), index, index); QObject *data = m_map->objectAt(index); const QMetaObject *mo = data->metaObject(); // We have all the data changed notify signals already stored auto keys = m_signalIndexToProperties.keys(); foreach (int index, keys) { QMetaMethod meth = mo->method(index); connect(data, meth, this, propertyChangedMetaMethod()); } endInsertRows(); } void AbstractModel::onDataRemoved(int index) { beginRemoveRows(QModelIndex(), index, index); endRemoveRows(); } QMetaMethod AbstractModel::propertyChangedMetaMethod() const { auto mo = metaObject(); int methodIndex = mo->indexOfMethod("propertyChanged()"); if (methodIndex == -1) { return QMetaMethod(); } return mo->method(methodIndex); } SinkModel::SinkModel(QObject *parent) : AbstractModel(&context()->sinks(), parent) + , m_preferredSink(nullptr) { initRoleNames(Sink::staticMetaObject); - connect(context()->server(), &Server::defaultSinkChanged, this, &SinkModel::defaultSinkChanged); + for (int i = 0; i < context()->sinks().count(); ++i) { + sinkAdded(i); + } + + connect(&context()->sinks(), &MapBaseQObject::added, this, &SinkModel::sinkAdded); + connect(&context()->sinks(), &MapBaseQObject::removed, this, &SinkModel::sinkRemoved); + + connect(context()->server(), &Server::defaultSinkChanged, this, [this]() { + updatePreferredSink(); + emit defaultSinkChanged(); + }); } Sink *SinkModel::defaultSink() const { return context()->server()->defaultSink(); } +Sink *SinkModel::preferredSink() const +{ + return m_preferredSink; +} + QVariant SinkModel::data(const QModelIndex &index, int role) const { if (role == SortByDefaultRole) { // Workaround QTBUG-1548 const QString pulseIndex = data(index, AbstractModel::role(QByteArrayLiteral("Index"))).toString(); const QString defaultDevice = data(index, AbstractModel::role(QByteArrayLiteral("Default"))).toString(); return defaultDevice + pulseIndex; } return AbstractModel::data(index, role); } +void SinkModel::sinkAdded(int index) +{ + Q_ASSERT(qobject_cast(context()->sinks().objectAt(index))); + Sink *sink = static_cast(context()->sinks().objectAt(index)); + connect(sink, &Sink::stateChanged, this, &SinkModel::updatePreferredSink); + + updatePreferredSink(); +} + +void SinkModel::sinkRemoved(int index) +{ + Q_UNUSED(index); + + updatePreferredSink(); +} + +void SinkModel::updatePreferredSink() +{ + Sink *sink = findPreferredSink(); + + if (sink != m_preferredSink) { + qCDebug(PLASMAPA) << "Changing preferred sink to" << sink << (sink ? sink->name() : ""); + m_preferredSink = sink; + emit preferredSinkChanged(); + } +} + +Sink *SinkModel::findPreferredSink() const +{ + const auto &sinks = context()->sinks(); + + // Only one sink is the preferred one + if (sinks.count() == 1) { + return static_cast(sinks.objectAt(0)); + } + + auto lookForState = [this](Device::State state) { + Sink *ret = nullptr; + QMapIterator it(context()->sinks().data()); + while (it.hasNext()) { + it.next(); + if (it.value()->state() != state) { + continue; + } + if (!ret) { + ret = it.value(); + } else if (it.value() == defaultSink()) { + ret = it.value(); + break; + } + } + return ret; + }; + + Sink *preferred = nullptr; + + // Look for playing sinks + prefer default sink + preferred = lookForState(Device::RunningState); + if (preferred) { + return preferred; + } + + // Look for idle sinks + prefer default sink + preferred = lookForState(Device::IdleState); + if (preferred) { + return preferred; + } + + // Fallback to default sink + return defaultSink(); +} + SourceModel::SourceModel(QObject *parent) : AbstractModel(&context()->sources(), parent) { initRoleNames(Source::staticMetaObject); connect(context()->server(), &Server::defaultSourceChanged, this, &SourceModel::defaultSourceChanged); } Source *SourceModel::defaultSource() const { return context()->server()->defaultSource(); } QVariant SourceModel::data(const QModelIndex &index, int role) const { if (role == SortByDefaultRole) { // Workaround QTBUG-1548 const QString pulseIndex = data(index, AbstractModel::role(QByteArrayLiteral("Index"))).toString(); const QString defaultDevice = data(index, AbstractModel::role(QByteArrayLiteral("Default"))).toString(); return defaultDevice + pulseIndex; } return AbstractModel::data(index, role); } SinkInputModel::SinkInputModel(QObject *parent) : AbstractModel(&context()->sinkInputs(), parent) { initRoleNames(SinkInput::staticMetaObject); } SourceOutputModel::SourceOutputModel(QObject *parent) : AbstractModel(&context()->sourceOutputs(), parent) { initRoleNames(SourceOutput::staticMetaObject); } CardModel::CardModel(QObject *parent) : AbstractModel(&context()->cards(), parent) { initRoleNames(Card::staticMetaObject); } StreamRestoreModel::StreamRestoreModel(QObject *parent) : AbstractModel(&context()->streamRestores(), parent) { initRoleNames(StreamRestore::staticMetaObject); } } // QPulseAudio diff --git a/src/pulseaudio.h b/src/pulseaudio.h index 1864a9a..97001b8 100644 --- a/src/pulseaudio.h +++ b/src/pulseaudio.h @@ -1,137 +1,148 @@ /* Copyright 2014-2015 Harald Sitter Copyright 2016 David Rosca This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #ifndef PULSEAUDIO_H #define PULSEAUDIO_H #include #include "maps.h" #include "ref.h" namespace QPulseAudio { class Q_DECL_EXPORT AbstractModel : public QAbstractListModel, public Ref { Q_OBJECT public: enum ItemRole { PulseObjectRole = Qt::UserRole + 1 }; QHash roleNames() const Q_DECL_FINAL; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_FINAL; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; bool setData(const QModelIndex &index, const QVariant &value, int role) Q_DECL_FINAL; Q_INVOKABLE int role(const QByteArray &roleName) const; protected: AbstractModel(const MapBaseQObject *map, QObject *parent); void initRoleNames(const QMetaObject &qobjectMetaObject); private slots: void propertyChanged(); private: void onDataAdded(int index); void onDataRemoved(int index); QMetaMethod propertyChangedMetaMethod() const; const MapBaseQObject *m_map; QHash m_roles; QHash m_objectProperties; QHash m_signalIndexToProperties; private: // Prevent leaf-classes from default constructing as we want to enforce // them passing us a context or explicit nullptrs. AbstractModel() {} }; class Q_DECL_EXPORT CardModel : public AbstractModel { Q_OBJECT public: CardModel(QObject *parent = nullptr); }; class Q_DECL_EXPORT SinkModel : public AbstractModel { Q_OBJECT Q_PROPERTY(QPulseAudio::Sink *defaultSink READ defaultSink NOTIFY defaultSinkChanged) + Q_PROPERTY(QPulseAudio::Sink *preferredSink READ preferredSink NOTIFY preferredSinkChanged) public: enum ItemRole { SortByDefaultRole = PulseObjectRole + 1 }; Q_ENUMS(ItemRole) SinkModel(QObject *parent = nullptr); Sink *defaultSink() const; + Sink *preferredSink() const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; signals: void defaultSinkChanged(); + void preferredSinkChanged(); + +private: + void sinkAdded(int index); + void sinkRemoved(int index); + void updatePreferredSink(); + Sink *findPreferredSink() const; + + Sink *m_preferredSink; }; class Q_DECL_EXPORT SinkInputModel : public AbstractModel { Q_OBJECT public: SinkInputModel(QObject *parent = nullptr); }; class Q_DECL_EXPORT SourceModel : public AbstractModel { Q_OBJECT Q_PROPERTY(QPulseAudio::Source *defaultSource READ defaultSource NOTIFY defaultSourceChanged) public: enum ItemRole { SortByDefaultRole = PulseObjectRole + 1 }; Q_ENUMS(ItemRole) SourceModel(QObject *parent = nullptr); Source *defaultSource() const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; signals: void defaultSourceChanged(); }; class Q_DECL_EXPORT SourceOutputModel : public AbstractModel { Q_OBJECT public: SourceOutputModel(QObject *parent = nullptr); }; class Q_DECL_EXPORT StreamRestoreModel : public AbstractModel { Q_OBJECT public: StreamRestoreModel(QObject *parent = nullptr); }; } // QPulseAudio #endif // PULSEAUDIO_H