diff --git a/src/maps.h b/src/maps.h index 7ae646e..fc61523 100644 --- a/src/maps.h +++ b/src/maps.h @@ -1,182 +1,185 @@ /* 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 MAPS_H #define MAPS_H #include "debug.h" #include #include #include #include #include "kf5pulseaudioqt_export.h" #include "sink_p.h" #include "volumeobject_p.h" #include "source_p.h" namespace QPulseAudio { // Used for typedefs. class Card; class Client; class Sink; class SinkInput; class Source; class SourceOutput; class StreamRestore; class Module; /** * @see MapBase * This class is nothing more than the QObject base since moc cannot handle * templates. */ class KF5PULSEAUDIOQT_EXPORT MapBaseQObject : public QObject { Q_OBJECT public: virtual int count() const = 0; virtual QObject *objectAt(int index) const = 0; virtual int indexOfObject(QObject *object) const = 0; Q_SIGNALS: + void aboutToBeAdded(int index); void added(int index); void aboutToBeRemoved(int index); void removed(int index); }; /** * Maps a specific index to a specific object pointer. * This is used to give the unique arbitrary PulseAudio index of a PulseObject a * serialized list index. Namely it enables us to translate a discrete list * index to a pulse index to an object, and any permutation thereof. */ template class MapBase : public MapBaseQObject { public: virtual ~MapBase() {} const QMap &data() const { return m_data; } int count() const Q_DECL_OVERRIDE { return m_data.count(); } int indexOfObject(QObject *object) const Q_DECL_OVERRIDE { int index = 0; QMapIterator it(m_data); while (it.hasNext()) { it.next(); if (it.value() == object) { return index; } index++; } return -1; } QObject *objectAt(int index) const Q_DECL_OVERRIDE { return (m_data.constBegin() + index).value(); } void reset() { while (!m_data.isEmpty()) { removeEntry(m_data.lastKey()); } m_pendingRemovals.clear(); } void insert(Type *object) { Q_ASSERT(!m_data.contains(object->index())); - m_data.insert(object->index(), object); + int modelIndex = 0; + for (auto it = m_data.constBegin(); it != m_data.constEnd(); ++it) { + if (object->index() < it.key()) { + break; + } + modelIndex++; + } - const int modelIndex = m_data.keys().indexOf(object->index()); - Q_ASSERT(modelIndex >= 0); + Q_EMIT aboutToBeAdded(modelIndex); + m_data.insert(object->index(), object); + Q_ASSERT(modelIndex == m_data.keys().indexOf(object->index())); Q_EMIT added(modelIndex); } // Context is passed in as parent because context needs to include the maps // so we'd cause a circular dep if we were to try to use the instance here. // Plus that's weird separation anyway. void updateEntry(const PAInfo *info, QObject *parent) { Q_ASSERT(info); if (m_pendingRemovals.remove(info->index)) { // Was already removed again. return; } - const bool isNew = !m_data.contains(info->index); - auto *obj = m_data.value(info->index, nullptr); if (!obj) { obj = new Type(parent); } obj->update(info); - m_data.insert(info->index, obj); - if (isNew) { - const int modelIndex = m_data.keys().indexOf(info->index); - Q_ASSERT(modelIndex >= 0); - Q_EMIT added(modelIndex); + if (!m_data.contains(info->index)) { + insert(obj); } } void removeEntry(quint32 index) { if (!m_data.contains(index)) { m_pendingRemovals.insert(index); } else { const int modelIndex = m_data.keys().indexOf(index); - emit aboutToBeRemoved(modelIndex); + Q_EMIT aboutToBeRemoved(modelIndex); delete m_data.take(index); Q_EMIT removed(modelIndex); } } protected: QMap m_data; QSet m_pendingRemovals; }; typedef MapBase SinkMap; typedef MapBase SinkInputMap; typedef MapBase SourceMap; typedef MapBase SourceOutputMap; typedef MapBase ClientMap; typedef MapBase CardMap; typedef MapBase ModuleMap; typedef MapBase StreamRestoreMap; } // QPulseAudio #endif // MAPS_H diff --git a/src/pulseaudio.cpp b/src/pulseaudio.cpp index efde07f..9ad85c8 100644 --- a/src/pulseaudio.cpp +++ b/src/pulseaudio.cpp @@ -1,370 +1,382 @@ /* 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(); - //deref context after we've deleted this object - //see https://bugs.kde.org/show_bug.cgi?id=371215 - connect(this, &QObject::destroyed, []() { - Context::instance()->unref(); - }); - connect(m_map, &MapBaseQObject::added, this, &AbstractModel::onDataAdded); + 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 { - Q_UNUSED(parent); + 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) { 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); Q_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(); } 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(); Q_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; Q_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); } ModuleModel::ModuleModel(QObject *parent) : AbstractModel(&context()->modules(), parent) { initRoleNames(Module::staticMetaObject); } } // QPulseAudio diff --git a/src/pulseaudio.h b/src/pulseaudio.h index 12e3363..6c067c6 100644 --- a/src/pulseaudio.h +++ b/src/pulseaudio.h @@ -1,156 +1,157 @@ /* 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" namespace QPulseAudio { class Context; class KF5PULSEAUDIOQT_EXPORT AbstractModel : public QAbstractListModel { Q_OBJECT public: enum ItemRole { PulseObjectRole = Qt::UserRole + 1 }; + ~AbstractModel() override; 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); Context *context() const; private Q_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 KF5PULSEAUDIOQT_EXPORT CardModel : public AbstractModel { Q_OBJECT public: CardModel(QObject *parent = nullptr); }; class KF5PULSEAUDIOQT_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; Q_SIGNALS: void defaultSinkChanged(); void preferredSinkChanged(); private: void sinkAdded(int index); void sinkRemoved(int index); void updatePreferredSink(); Sink *findPreferredSink() const; Sink *m_preferredSink; }; class KF5PULSEAUDIOQT_EXPORT SinkInputModel : public AbstractModel { Q_OBJECT public: SinkInputModel(QObject *parent = nullptr); }; class KF5PULSEAUDIOQT_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; Q_SIGNALS: void defaultSourceChanged(); }; class KF5PULSEAUDIOQT_EXPORT SourceOutputModel : public AbstractModel { Q_OBJECT public: SourceOutputModel(QObject *parent = nullptr); }; class KF5PULSEAUDIOQT_EXPORT StreamRestoreModel : public AbstractModel { Q_OBJECT public: StreamRestoreModel(QObject *parent = nullptr); }; class KF5PULSEAUDIOQT_EXPORT ModuleModel : public AbstractModel { Q_OBJECT public: ModuleModel(QObject *parent = nullptr); }; } // QPulseAudio #endif // PULSEAUDIO_H