diff --git a/applet/contents/ui/main.qml b/applet/contents/ui/main.qml --- a/applet/contents/ui/main.qml +++ b/applet/contents/ui/main.qml @@ -44,11 +44,11 @@ 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)); @@ -62,32 +62,32 @@ } 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() { diff --git a/src/device.h b/src/device.h --- a/src/device.h +++ b/src/device.h @@ -35,13 +35,23 @@ 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 @@ -77,8 +87,15 @@ } 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; @@ -89,6 +106,7 @@ virtual void setDefault(bool enable) = 0; signals: + void stateChanged(); void nameChanged(); void descriptionChanged(); void cardIndexChanged(); @@ -100,11 +118,14 @@ 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; }; } // QPulseAudio diff --git a/src/device.cpp b/src/device.cpp --- a/src/device.cpp +++ b/src/device.cpp @@ -20,6 +20,11 @@ #include "device.h" +QPulseAudio::Device::State QPulseAudio::Device::state() const +{ + return m_state; +} + QString QPulseAudio::Device::name() const { return m_name; @@ -47,5 +52,22 @@ QPulseAudio::Device::Device(QObject *parent) : VolumeObject(parent) + , m_state(UnknownState) +{ +} + +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/pulseaudio.h b/src/pulseaudio.h --- a/src/pulseaudio.h +++ b/src/pulseaudio.h @@ -79,18 +79,29 @@ { 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 diff --git a/src/pulseaudio.cpp b/src/pulseaudio.cpp --- a/src/pulseaudio.cpp +++ b/src/pulseaudio.cpp @@ -189,17 +189,33 @@ 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) { @@ -211,6 +227,78 @@ 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) {