diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,7 @@ debug.cpp server.cpp streamrestore.cpp + module.cpp ) add_library(QPulseAudioPrivate SHARED ${SRC_LIST}) diff --git a/src/context.h b/src/context.h --- a/src/context.h +++ b/src/context.h @@ -62,6 +62,7 @@ const SourceOutputMap &sourceOutputs() const { return m_sourceOutputs; } const ClientMap &clients() const { return m_clients; } const CardMap &cards() const { return m_cards; } + const ModuleMap &modules() const { return m_modules; } const StreamRestoreMap &streamRestores() const { return m_streamRestores; } Server *server() const { return m_server; } @@ -74,6 +75,7 @@ void sourceOutputCallback(const pa_source_output_info *info); void clientCallback(const pa_client_info *info); void cardCallback(const pa_card_info *info); + void moduleCallback(const pa_module_info *info); void streamRestoreCallback(const pa_ext_stream_restore_info *info); void serverCallback(const pa_server_info *info); @@ -162,6 +164,7 @@ SourceOutputMap m_sourceOutputs; ClientMap m_clients; CardMap m_cards; + ModuleMap m_modules; StreamRestoreMap m_streamRestores; Server *m_server; diff --git a/src/context.cpp b/src/context.cpp --- a/src/context.cpp +++ b/src/context.cpp @@ -33,6 +33,7 @@ #include "source.h" #include "sourceoutput.h" #include "streamrestore.h" +#include "module.h" namespace QPulseAudio { @@ -130,6 +131,15 @@ ((Context *)data)->cardCallback(info); } +static void module_info_list_cb(pa_context *context, const pa_module_info *info, int eol, void *data) +{ + if (!isGoodState(eol)) + return; + Q_ASSERT(context); + Q_ASSERT(data); + ((Context *)data)->moduleCallback(info); +} + static void server_cb(pa_context *context, const pa_server_info *info, void *data) { Q_ASSERT(context); @@ -287,6 +297,18 @@ } break; + case PA_SUBSCRIPTION_EVENT_MODULE: + if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + m_modules.removeEntry(index); + } else { + if (!PAOperation(pa_context_get_module_info_list(context, module_info_list_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_module_info_list() failed"; + return; + } + } + break; + + case PA_SUBSCRIPTION_EVENT_SERVER: if (!PAOperation(pa_context_get_server_info(context, server_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_server_info() failed"; @@ -315,6 +337,7 @@ PA_SUBSCRIPTION_MASK_SINK_INPUT| PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT| PA_SUBSCRIPTION_MASK_CARD| + PA_SUBSCRIPTION_MASK_MODULE| PA_SUBSCRIPTION_MASK_SERVER), nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_context_subscribe() failed"; return; @@ -351,6 +374,11 @@ return; } + if (!PAOperation(pa_context_get_module_info_list(c, module_info_list_cb, this))) { + qCWarning(PLASMAPA) << "pa_context_get_module_info_list() failed"; + return; + } + if (!PAOperation(pa_context_get_server_info(c, server_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_server_info() failed"; return; @@ -404,6 +432,11 @@ m_cards.updateEntry(info, this); } +void Context::moduleCallback(const pa_module_info *info) +{ + m_modules.updateEntry(info, this); +} + void Context::streamRestoreCallback(const pa_ext_stream_restore_info *info) { if (qstrcmp(info->name, "sink-input-by-media-role:event") != 0) { @@ -526,6 +559,9 @@ m_sources.reset(); m_sourceOutputs.reset(); m_clients.reset(); + m_cards.reset(); + m_modules.reset(); + m_streamRestores.reset(); m_server->reset(); } diff --git a/src/kcm/package/contents/ui/Advanced.qml b/src/kcm/package/contents/ui/Advanced.qml --- a/src/kcm/package/contents/ui/Advanced.qml +++ b/src/kcm/package/contents/ui/Advanced.qml @@ -70,6 +70,7 @@ text: i18n("Add virtual output device for simultaneous output on all local sound cards") checked: moduleManager.combineSinks onCheckedChanged: moduleManager.combineSinks = checked; + enabled: moduleManager.loadedModules.indexOf("module-gconf") != -1 } CheckBox { @@ -79,6 +80,15 @@ text: i18n("Automatically switch all running streams when a new output becomes available") checked: moduleManager.switchOnConnect onCheckedChanged: moduleManager.switchOnConnect = checked; + enabled: moduleManager.loadedModules.indexOf("module-gconf") != -1 + } + + Label { + Layout.alignment: Qt.AlignHCenter + enabled: false + font.italic: true + text: i18n("Requires 'module-gconf' PulseAudio module") + visible: moduleManager.loadedModules.indexOf("module-gconf") == -1 } } } diff --git a/src/maps.h b/src/maps.h --- a/src/maps.h +++ b/src/maps.h @@ -39,6 +39,7 @@ class Source; class SourceOutput; class StreamRestore; +class Module; /** * @see MapBase @@ -166,6 +167,7 @@ typedef MapBase SourceOutputMap; typedef MapBase ClientMap; typedef MapBase CardMap; +typedef MapBase ModuleMap; typedef MapBase StreamRestoreMap; } // QPulseAudio diff --git a/src/modulemanager.h b/src/module.h copy from src/modulemanager.h copy to src/module.h --- a/src/modulemanager.h +++ b/src/module.h @@ -1,5 +1,5 @@ /* - Copyright 2016 David Edmundson + Copyright 2017 David Rosca This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -18,46 +18,42 @@ License along with this library. If not, see . */ -#ifndef MODULEMANAGER_H -#define MODULEMANAGER_H - -#include +#ifndef MODULE_H +#define MODULE_H #include -#include "context.h" -// Properties need fully qualified classes even with pointers. -#include "client.h" +#include +#include -class GConfItem; +#include "pulseobject.h" namespace QPulseAudio { -class Module; -class Q_DECL_EXPORT ModuleManager : public QObject +class Q_DECL_EXPORT Module : public PulseObject { Q_OBJECT - Q_PROPERTY(bool combineSinks READ combineSinks WRITE setCombineSinks NOTIFY combineSinksChanged) - Q_PROPERTY(bool switchOnConnect READ switchOnConnect WRITE setSwitchOnConnect NOTIFY switchOnConnectChanged) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString argument READ argument NOTIFY argumentChanged) + public: - ModuleManager(QObject *parent = nullptr); - ~ModuleManager(); - bool combineSinks() const; - void setCombineSinks(bool combineSinks); - bool switchOnConnect() const; - void setSwitchOnConnect(bool switchOnConnect); + Module(QObject *parent); + + void update(const pa_module_info *info); + + QString name() const; + QString argument() const; -Q_SIGNALS: - void combineSinksChanged(); - void switchOnConnectChanged(); +signals: + void nameChanged(); + void argumentChanged(); private: - Module *m_combineSinks; - Module *m_switchOnConnect; - Module *m_deviceManager; + QString m_name; + QString m_argument; }; } // QPulseAudio -#endif // STREAM_H +#endif // MODULE_H diff --git a/src/module.cpp b/src/module.cpp new file mode 100644 --- /dev/null +++ b/src/module.cpp @@ -0,0 +1,61 @@ +/* + Copyright 2017 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 "module.h" + +#include "debug.h" + +#include "context.h" + +namespace QPulseAudio +{ + +Module::Module(QObject *parent) + : PulseObject(parent) +{ +} + +void Module::update(const pa_module_info *info) +{ + updatePulseObject(info); + + const QString infoName = QString::fromUtf8(info->name); + if (m_name != infoName) { + m_name = infoName; + emit nameChanged(); + } + const QString infoArgument = QString::fromUtf8(info->argument); + if (m_argument != infoArgument) { + m_argument = infoArgument; + emit argumentChanged(); + } +} + +QString Module::name() const +{ + return m_name; +} + +QString Module::argument() const +{ + return m_argument; +} + +} // QPulseAudio diff --git a/src/modulemanager.h b/src/modulemanager.h --- a/src/modulemanager.h +++ b/src/modulemanager.h @@ -33,29 +33,35 @@ namespace QPulseAudio { -class Module; +class GConfModule; class Q_DECL_EXPORT ModuleManager : public QObject { Q_OBJECT Q_PROPERTY(bool combineSinks READ combineSinks WRITE setCombineSinks NOTIFY combineSinksChanged) Q_PROPERTY(bool switchOnConnect READ switchOnConnect WRITE setSwitchOnConnect NOTIFY switchOnConnectChanged) + Q_PROPERTY(QStringList loadedModules READ loadedModules NOTIFY loadedModulesChanged) public: ModuleManager(QObject *parent = nullptr); ~ModuleManager(); bool combineSinks() const; void setCombineSinks(bool combineSinks); bool switchOnConnect() const; void setSwitchOnConnect(bool switchOnConnect); + QStringList loadedModules() const; Q_SIGNALS: void combineSinksChanged(); void switchOnConnectChanged(); + void loadedModulesChanged(); private: - Module *m_combineSinks; - Module *m_switchOnConnect; - Module *m_deviceManager; + void updateLoadedModules(); + + GConfModule *m_combineSinks; + GConfModule *m_switchOnConnect; + GConfModule *m_deviceManager; + QStringList m_loadedModules; }; } // QPulseAudio diff --git a/src/modulemanager.cpp b/src/modulemanager.cpp --- a/src/modulemanager.cpp +++ b/src/modulemanager.cpp @@ -20,38 +20,41 @@ #include "modulemanager.h" +#include "module.h" #define PA_GCONF_ROOT "/system/pulseaudio" #define PA_GCONF_PATH_MODULES PA_GCONF_ROOT"/modules" #include "gconfitem.h" +#include + namespace QPulseAudio { -class Module : public GConfItem +class GConfModule : public GConfItem { Q_OBJECT public: - Module(const QString &configName, const QString &moduleName, QObject *parent); + GConfModule(const QString &configName, const QString &moduleName, QObject *parent); bool isEnabled() const; void setEnabled(bool enabled, const QVariant &args=QVariant()); private: QString m_moduleName; }; -Module::Module(const QString &configName, const QString &moduleName, QObject *parent) : +GConfModule::GConfModule(const QString &configName, const QString &moduleName, QObject *parent) : GConfItem(QStringLiteral(PA_GCONF_PATH_MODULES"/") + configName, parent), m_moduleName(moduleName) { } -bool Module::isEnabled() const +bool GConfModule::isEnabled() const { return value(QStringLiteral("enabled")).toBool(); } -void Module::setEnabled(bool enabled, const QVariant &args) +void GConfModule::setEnabled(bool enabled, const QVariant &args) { set(QStringLiteral("locked"), true); @@ -68,13 +71,21 @@ ModuleManager::ModuleManager(QObject *parent) : QObject(parent), - m_combineSinks(new Module(QStringLiteral("combine"), QStringLiteral("module-combine"), this)), - m_switchOnConnect(new Module(QStringLiteral("switch-on-connect"), QStringLiteral("module-switch-on-connect"), this)), - m_deviceManager(new Module(QStringLiteral("device-manager"), QStringLiteral("module-device-manager"), this)) + m_combineSinks(new GConfModule(QStringLiteral("combine"), QStringLiteral("module-combine"), this)), + m_switchOnConnect(new GConfModule(QStringLiteral("switch-on-connect"), QStringLiteral("module-switch-on-connect"), this)), + m_deviceManager(new GConfModule(QStringLiteral("device-manager"), QStringLiteral("module-device-manager"), this)) { connect(m_combineSinks, &GConfItem::subtreeChanged, this, &ModuleManager::combineSinksChanged); connect(m_switchOnConnect, &GConfItem::subtreeChanged, this, &ModuleManager::switchOnConnectChanged); connect(m_deviceManager, &GConfItem::subtreeChanged, this, &ModuleManager::switchOnConnectChanged); + + QTimer *updateModulesTimer = new QTimer(this); + updateModulesTimer->setInterval(500); + updateModulesTimer->setSingleShot(true); + connect(updateModulesTimer, &QTimer::timeout, this, &ModuleManager::updateLoadedModules); + connect(&Context::instance()->modules(), &MapBaseQObject::added, updateModulesTimer, static_cast(&QTimer::start)); + connect(&Context::instance()->modules(), &MapBaseQObject::removed, updateModulesTimer, static_cast(&QTimer::start)); + updateLoadedModules(); } ModuleManager::~ModuleManager() @@ -107,6 +118,21 @@ m_switchOnConnect->setEnabled(switchOnConnect); } +QStringList ModuleManager::loadedModules() const +{ + return m_loadedModules; +} + +void ModuleManager::updateLoadedModules() +{ + m_loadedModules.clear(); + const auto modules = Context::instance()->modules().data(); + for (Module *module : modules) { + m_loadedModules.append(module->name()); + } + Q_EMIT loadedModulesChanged(); +} + } #include "modulemanager.moc" diff --git a/src/pulseaudio.h b/src/pulseaudio.h --- a/src/pulseaudio.h +++ b/src/pulseaudio.h @@ -144,6 +144,13 @@ StreamRestoreModel(QObject *parent = nullptr); }; +class Q_DECL_EXPORT ModuleModel : public AbstractModel +{ + Q_OBJECT +public: + ModuleModel(QObject *parent = nullptr); +}; + } // QPulseAudio #endif // PULSEAUDIO_H diff --git a/src/pulseaudio.cpp b/src/pulseaudio.cpp --- a/src/pulseaudio.cpp +++ b/src/pulseaudio.cpp @@ -29,6 +29,7 @@ #include "sourceoutput.h" #include "server.h" #include "streamrestore.h" +#include "module.h" #include @@ -359,4 +360,10 @@ initRoleNames(StreamRestore::staticMetaObject); } +ModuleModel::ModuleModel(QObject *parent) + : AbstractModel(&context()->modules(), parent) +{ + initRoleNames(Module::staticMetaObject); +} + } // QPulseAudio diff --git a/src/qml/plugin.cpp b/src/qml/plugin.cpp --- a/src/qml/plugin.cpp +++ b/src/qml/plugin.cpp @@ -54,6 +54,7 @@ qmlRegisterType(uri, 0, 1, "ModuleManager"); qmlRegisterType(uri, 0, 1, "SourceOutputModel"); qmlRegisterType(uri, 0, 1, "StreamRestoreModel"); + qmlRegisterType(uri, 0, 1, "ModuleModel"); qmlRegisterUncreatableType(uri, 0, 1, "Port", QString()); qmlRegisterType(uri, 0, 1, "GlobalAction"); qmlRegisterType(uri, 0, 1, "GlobalActionCollection");