diff --git a/src/device.cpp b/src/device.cpp index 75ddb1d..d36f3bd 100644 --- a/src/device.cpp +++ b/src/device.cpp @@ -1,77 +1,82 @@ /* 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; } QString QPulseAudio::Device::formFactor() const { return m_formFactor; } quint32 QPulseAudio::Device::cardIndex() const { return m_cardIndex; } QList QPulseAudio::Device::ports() const { return m_ports; } quint32 QPulseAudio::Device::activePortIndex() const { return m_activePortIndex; } +bool QPulseAudio::Device::isVirtualDevice() const +{ + return m_virtualDevice; +} + 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 e829c52..f67b6ca 100644 --- a/src/device.h +++ b/src/device.h @@ -1,145 +1,155 @@ /* 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 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(QString formFactor READ formFactor NOTIFY formFactorChanged) 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) + Q_PROPERTY(bool virtualDevice READ isVirtualDevice NOTIFY virtualDeviceChanged) 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(); } const char *form_factor = pa_proplist_gets(info->proplist, PA_PROP_DEVICE_FORM_FACTOR); if (form_factor) { QString formFactor = QString::fromUtf8(form_factor); if (m_formFactor != formFactor) { m_formFactor = formFactor; emit formFactorChanged(); } } 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(); } + + const bool isVirtual = !(info->flags & 4); // PA_X_HARDWARE + if (m_virtualDevice != isVirtual) { + m_virtualDevice = isVirtual; + emit virtualDeviceChanged(); + } } State state() const; QString name() const; QString description() const; QString formFactor() 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; + bool isVirtualDevice() const; signals: void stateChanged(); void nameChanged(); void descriptionChanged(); void formFactorChanged(); void cardIndexChanged(); void portsChanged(); void activePortIndexChanged(); void defaultChanged(); + void virtualDeviceChanged(); protected: Device(QObject *parent); private: State stateFromPaState(int value) const; QString m_name; QString m_description; QString m_formFactor; quint32 m_cardIndex = -1; QList m_ports; quint32 m_activePortIndex = -1; State m_state = UnknownState; + bool m_virtualDevice = false; }; } // QPulseAudio #endif // DEVICE_H diff --git a/src/pulseaudio.cpp b/src/pulseaudio.cpp index 0ec084e..70c6a4a 100644 --- a/src/pulseaudio.cpp +++ b/src/pulseaudio.cpp @@ -1,385 +1,385 @@ /* 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 "module.h" #include namespace QPulseAudio { AbstractModel::AbstractModel(const MapBaseQObject *map, QObject *parent) : QAbstractListModel(parent) , m_map(map) { Context::instance()->ref(); connect(m_map, &MapBaseQObject::aboutToBeAdded, this, [this](int index) { beginInsertRows(QModelIndex(), index, index); }); connect(m_map, &MapBaseQObject::added, this, [this](int index) { onDataAdded(index); endInsertRows(); }); connect(m_map, &MapBaseQObject::aboutToBeRemoved, this, [this](int index) { beginRemoveRows(QModelIndex(), index, index); }); connect(m_map, &MapBaseQObject::removed, this, [this](int index) { Q_UNUSED(index); endRemoveRows(); }); } AbstractModel::~AbstractModel() { //deref context after we've deleted this object //see https://bugs.kde.org/show_bug.cgi?id=371215 Context::instance()->unref(); } 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 { if (parent.isValid()) { return 0; } return m_map->count(); } QVariant AbstractModel::data(const QModelIndex &index, int role) const { if (!hasIndex(index.row(), index.column())) { return QVariant(); } QObject *data = m_map->objectAt(index.row()); Q_ASSERT(data); if (role == PulseObjectRole) { return QVariant::fromValue(data); } else if (role == Qt::DisplayRole) { return static_cast(data)->properties().value(QStringLiteral("name")).toString(); } 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) { if (!hasIndex(index.row(), index.column())) { return false; } 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); } Context *AbstractModel::context() const { return Context::instance(); } 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) { 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()); } } 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); 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) { + if (it.value()->isVirtualDevice() || 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); } ModuleModel::ModuleModel(QObject *parent) : AbstractModel(&context()->modules(), parent) { initRoleNames(Module::staticMetaObject); } } // QPulseAudio