diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eb9b3d1..7c51fa5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,40 +1,41 @@ include_directories(${PULSEAUDIO_INCLUDE_DIR} ${GLIB2_INCLUDE_DIR} ${GCONF_INCLUDE_DIRS} ${GOBJECT_INCLUDE_DIRS}) set(SRC_LIST card.cpp client.cpp context.cpp device.cpp maps.cpp operation.cpp port.cpp profile.cpp pulseaudio.cpp pulseobject.cpp sink.cpp sinkinput.cpp modulemanager.cpp gconfitem.cpp source.cpp sourceoutput.cpp stream.cpp volumeobject.cpp debug.cpp server.cpp streamrestore.cpp + module.cpp ) add_library(QPulseAudioPrivate SHARED ${SRC_LIST}) target_link_libraries(QPulseAudioPrivate Qt5::Core Qt5::Gui ${PULSEAUDIO_LIBRARY} ${PULSEAUDIO_MAINLOOP_LIBRARY} ${GCONF_LDFLAGS} ${GOBJECT_LDFLAGS} ) install(TARGETS QPulseAudioPrivate ${INSTALL_TARGETS_DEFAULT_ARGS}) add_subdirectory(kcm) add_subdirectory(qml) diff --git a/src/context.cpp b/src/context.cpp index ece8cd9..f247a0c 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -1,532 +1,568 @@ /* 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 "context.h" #include "server.h" #include #include "debug.h" #include #include #include "card.h" #include "client.h" #include "sink.h" #include "sinkinput.h" #include "source.h" #include "sourceoutput.h" #include "streamrestore.h" +#include "module.h" namespace QPulseAudio { Context* Context::s_context = nullptr; static bool isGoodState(int eol) { if (eol < 0) { // Error return false; } if (eol > 0) { // End of callback chain return false; } return true; } // -------------------------- static void sink_cb(pa_context *context, const pa_sink_info *info, int eol, void *data) { if (!isGoodState(eol)) return; Q_ASSERT(context); Q_ASSERT(data); ((Context *)data)->sinkCallback(info); } static void sink_input_callback(pa_context *context, const pa_sink_input_info *info, int eol, void *data) { if (!isGoodState(eol)) return; // pulsesink probe is used by gst-pulse only to query sink formats (not for playback) if (qstrcmp(info->name, "pulsesink probe") == 0) { return; } if (const char *id = pa_proplist_gets(info->proplist, "module-stream-restore.id")) { if (qstrcmp(id, "sink-input-by-media-role:event") == 0) { qCDebug(PLASMAPA) << "Ignoring event role sink input."; return; } } Q_ASSERT(context); Q_ASSERT(data); ((Context *)data)->sinkInputCallback(info); } static void source_cb(pa_context *context, const pa_source_info *info, int eol, void *data) { if (!isGoodState(eol)) return; // FIXME: This forces excluding monitors if (info->monitor_of_sink != PA_INVALID_INDEX) return; Q_ASSERT(context); Q_ASSERT(data); ((Context *)data)->sourceCallback(info); } static void source_output_cb(pa_context *context, const pa_source_output_info *info, int eol, void *data) { if (!isGoodState(eol)) return; // FIXME: This forces excluding these apps if (const char *app = pa_proplist_gets(info->proplist, PA_PROP_APPLICATION_ID)) { if (strcmp(app, "org.PulseAudio.pavucontrol") == 0 || strcmp(app, "org.gnome.VolumeControl") == 0 || strcmp(app, "org.kde.kmixd") == 0) return; } Q_ASSERT(context); Q_ASSERT(data); ((Context *)data)->sourceOutputCallback(info); } static void client_cb(pa_context *context, const pa_client_info *info, int eol, void *data) { if (!isGoodState(eol)) return; Q_ASSERT(context); Q_ASSERT(data); ((Context *)data)->clientCallback(info); } static void card_cb(pa_context *context, const pa_card_info *info, int eol, void *data) { if (!isGoodState(eol)) return; Q_ASSERT(context); Q_ASSERT(data); ((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); Q_ASSERT(data); ((Context *)data)->serverCallback(info); } static void context_state_callback(pa_context *context, void *data) { Q_ASSERT(data); ((Context *)data)->contextStateCallback(context); } static void subscribe_cb(pa_context *context, pa_subscription_event_type_t type, uint32_t index, void *data) { Q_ASSERT(data); ((Context *)data)->subscribeCallback(context, type, index); } static void ext_stream_restore_read_cb(pa_context *context, const pa_ext_stream_restore_info *info, int eol, void *data) { if (!isGoodState(eol)) { return; } Q_ASSERT(context); Q_ASSERT(data); ((Context *)data)->streamRestoreCallback(info); } static void ext_stream_restore_subscribe_cb(pa_context *context, void *data) { Q_ASSERT(context); Q_ASSERT(data); if (!PAOperation(pa_ext_stream_restore_read(context, ext_stream_restore_read_cb, data))) { qCWarning(PLASMAPA) << "pa_ext_stream_restore_read() failed"; } } // -------------------------- Context::Context(QObject *parent) : QObject(parent) , m_server(new Server(this)) , m_context(nullptr) , m_mainloop(nullptr) , m_references(0) { connectToDaemon(); } Context::~Context() { if (m_context) { pa_context_unref(m_context); m_context = nullptr; } if (m_mainloop) { pa_glib_mainloop_free(m_mainloop); m_mainloop = nullptr; } reset(); } Context *Context::instance() { if (!s_context) { s_context = new Context; } return s_context; } void Context::ref() { ++m_references; } void Context::unref() { if (--m_references == 0) { delete this; s_context = nullptr; } } void Context::subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index) { Q_ASSERT(context == m_context); switch (type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { case PA_SUBSCRIPTION_EVENT_SINK: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_sinks.removeEntry(index); } else { if (!PAOperation(pa_context_get_sink_info_by_index(context, index, sink_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_sink_info_by_index() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_SOURCE: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_sources.removeEntry(index); } else { if (!PAOperation(pa_context_get_source_info_by_index(context, index, source_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_source_info_by_index() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_SINK_INPUT: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_sinkInputs.removeEntry(index); } else { if (!PAOperation(pa_context_get_sink_input_info(context, index, sink_input_callback, this))) { qCWarning(PLASMAPA) << "pa_context_get_sink_input_info() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_sourceOutputs.removeEntry(index); } else { if (!PAOperation(pa_context_get_source_output_info(context, index, source_output_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_sink_input_info() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_CLIENT: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_clients.removeEntry(index); } else { if (!PAOperation(pa_context_get_client_info(context, index, client_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_client_info() failed"; return; } } break; case PA_SUBSCRIPTION_EVENT_CARD: if ((type & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { m_cards.removeEntry(index); } else { if (!PAOperation(pa_context_get_card_info_by_index(context, index, card_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_card_info_by_index() failed"; return; } } 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"; return; } break; } } void Context::contextStateCallback(pa_context *c) { qCDebug(PLASMAPA) << "state callback"; pa_context_state_t state = pa_context_get_state(c); if (state == PA_CONTEXT_READY) { qCDebug(PLASMAPA) << "ready"; // 1. Register for the stream changes (except during probe) if (m_context == c) { pa_context_set_subscribe_callback(c, subscribe_cb, this); if (!PAOperation(pa_context_subscribe(c, (pa_subscription_mask_t) (PA_SUBSCRIPTION_MASK_SINK| PA_SUBSCRIPTION_MASK_SOURCE| PA_SUBSCRIPTION_MASK_CLIENT| 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; } } if (!PAOperation(pa_context_get_sink_info_list(c, sink_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_sink_info_list() failed"; return; } if (!PAOperation(pa_context_get_source_info_list(c, source_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_source_info_list() failed"; return; } if (!PAOperation(pa_context_get_client_info_list(c, client_cb, this))) { qCWarning(PLASMAPA) << "pa_context_client_info_list() failed"; return; } if (!PAOperation(pa_context_get_card_info_list(c, card_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_card_info_list() failed"; return; } if (!PAOperation(pa_context_get_sink_input_info_list(c, sink_input_callback, this))) { qCWarning(PLASMAPA) << "pa_context_get_sink_input_info_list() failed"; return; } if (!PAOperation(pa_context_get_source_output_info_list(c, source_output_cb, this))) { qCWarning(PLASMAPA) << "pa_context_get_source_output_info_list() failed"; 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; } if (PAOperation(pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, this))) { pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, this); PAOperation(pa_ext_stream_restore_subscribe(c, 1, nullptr, this)); } else { qCWarning(PLASMAPA) << "Failed to initialize stream_restore extension"; } } else if (!PA_CONTEXT_IS_GOOD(state)) { qCWarning(PLASMAPA) << "context kaput"; if (m_context) { pa_context_unref(m_context); m_context = nullptr; } reset(); QTimer::singleShot(1000, this, &Context::connectToDaemon); } } void Context::sinkCallback(const pa_sink_info *info) { // This parenting here is a bit weird m_sinks.updateEntry(info, this); } void Context::sinkInputCallback(const pa_sink_input_info *info) { m_sinkInputs.updateEntry(info, this); } void Context::sourceCallback(const pa_source_info *info) { m_sources.updateEntry(info, this); } void Context::sourceOutputCallback(const pa_source_output_info *info) { m_sourceOutputs.updateEntry(info, this); } void Context::clientCallback(const pa_client_info *info) { m_clients.updateEntry(info, this); } void Context::cardCallback(const pa_card_info *info) { 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) { return; } const int eventRoleIndex = 1; StreamRestore *obj = qobject_cast(m_streamRestores.data().value(eventRoleIndex)); if (!obj) { QVariantMap props; props.insert(QStringLiteral("application.icon_name"), QStringLiteral("preferences-desktop-notification")); obj = new StreamRestore(eventRoleIndex, props, this); m_streamRestores.insert(obj); } obj->update(info); } void Context::serverCallback(const pa_server_info *info) { m_server->update(info); } void Context::setCardProfile(quint32 index, const QString &profile) { if (!m_context) { return; } qCDebug(PLASMAPA) << index << profile; if (!PAOperation(pa_context_set_card_profile_by_index(m_context, index, profile.toUtf8().constData(), nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_context_set_card_profile_by_index failed"; return; } } void Context::setDefaultSink(const QString &name) { if (!m_context) { return; } const QByteArray nameData = name.toUtf8(); if (!PAOperation(pa_context_set_default_sink(m_context, nameData.constData(), nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_context_set_default_sink failed"; } } void Context::setDefaultSource(const QString &name) { if (!m_context) { return; } const QByteArray nameData = name.toUtf8(); if (!PAOperation(pa_context_set_default_source(m_context, nameData.constData(), nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_context_set_default_source failed"; } } void Context::streamRestoreWrite(const pa_ext_stream_restore_info *info) { if (!m_context) { return; } if (!PAOperation(pa_ext_stream_restore_write(m_context, PA_UPDATE_REPLACE, info, 1, true, nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_ext_stream_restore_write failed"; } } void Context::connectToDaemon() { Q_ASSERT(m_context == nullptr); // We require a glib event loop if (!QByteArray(QAbstractEventDispatcher::instance()->metaObject()->className()).contains("EventDispatcherGlib")) { qCWarning(PLASMAPA) << "Disabling PulseAudio integration for lack of GLib event loop"; return; } qCDebug(PLASMAPA) << "Attempting connection to PulseAudio sound daemon"; if (!m_mainloop) { m_mainloop = pa_glib_mainloop_new(nullptr); Q_ASSERT(m_mainloop); } pa_mainloop_api *api = pa_glib_mainloop_get_api(m_mainloop); Q_ASSERT(api); m_context = pa_context_new(api, "QPulse"); Q_ASSERT(m_context); if (pa_context_connect(m_context, NULL, PA_CONTEXT_NOFAIL, nullptr) < 0) { pa_context_unref(m_context); pa_glib_mainloop_free(m_mainloop); m_context = nullptr; m_mainloop = nullptr; return; } pa_context_set_state_callback(m_context, &context_state_callback, this); } void Context::reset() { m_sinks.reset(); m_sinkInputs.reset(); m_sources.reset(); m_sourceOutputs.reset(); m_clients.reset(); + m_cards.reset(); + m_modules.reset(); + m_streamRestores.reset(); m_server->reset(); } } // QPulseAudio diff --git a/src/context.h b/src/context.h index 9579c44..5627de2 100644 --- a/src/context.h +++ b/src/context.h @@ -1,177 +1,180 @@ /* 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 CONTEXT_H #define CONTEXT_H #include #include #include #include #include #include #include #include "maps.h" #include "operation.h" namespace QPulseAudio { class Server; class Q_DECL_EXPORT Context : public QObject { Q_OBJECT public: Context(QObject *parent = nullptr); ~Context(); static Context *instance(); static const qint64 NormalVolume = PA_VOLUME_NORM; static const qint64 MinimalVolume = 0; static const qint64 MaximalVolume = (PA_VOLUME_NORM / 100.0) * 150; void ref(); void unref(); bool isValid() { return m_context && m_mainloop; } const SinkMap &sinks() const { return m_sinks; } const SinkInputMap &sinkInputs() const { return m_sinkInputs; } const SourceMap &sources() const { return m_sources; } 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; } void subscribeCallback(pa_context *context, pa_subscription_event_type_t type, uint32_t index); void contextStateCallback(pa_context *context); void sinkCallback(const pa_sink_info *info); void sinkInputCallback(const pa_sink_input_info *info); void sourceCallback(const pa_source_info *info); 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); void setCardProfile(quint32 index, const QString &profile); void setDefaultSink(const QString &name); void setDefaultSource(const QString &name); void streamRestoreWrite(const pa_ext_stream_restore_info *info); template void setGenericVolume(quint32 index, int channel, qint64 newVolume, pa_cvolume cVolume, PAFunction pa_set_volume) { if (!m_context) { return; } newVolume = qBound(0, newVolume, PA_VOLUME_MAX); pa_cvolume newCVolume = cVolume; if (channel == -1) { // -1 all channels for (int i = 0; i < newCVolume.channels; ++i) { newCVolume.values[i] = newVolume; } } else { Q_ASSERT(newCVolume.channels > channel); newCVolume.values[channel] = newVolume; } if (!PAOperation(pa_set_volume(m_context, index, &newCVolume, nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_set_volume failed"; return; } } template void setGenericMute(quint32 index, bool mute, PAFunction pa_set_mute) { if (!m_context) { return; } if (!PAOperation(pa_set_mute(m_context, index, mute, nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_set_mute failed"; return; } } template void setGenericPort(quint32 index, const QString &portName, PAFunction pa_set_port) { if (!m_context) { return; } if (!PAOperation(pa_set_port(m_context, index, portName.toUtf8().constData(), nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_set_port failed"; return; } } template void setGenericDeviceForStream(quint32 streamIndex, quint32 deviceIndex, PAFunction pa_move_stream_to_device) { if (!m_context) { return; } if (!PAOperation(pa_move_stream_to_device(m_context, streamIndex, deviceIndex, nullptr, nullptr))) { qCWarning(PLASMAPA) << "pa_move_stream_to_device failed"; return; } } private: void connectToDaemon(); void reset(); // Don't forget to add things to reset(). SinkMap m_sinks; SinkInputMap m_sinkInputs; SourceMap m_sources; SourceOutputMap m_sourceOutputs; ClientMap m_clients; CardMap m_cards; + ModuleMap m_modules; StreamRestoreMap m_streamRestores; Server *m_server; pa_context *m_context; pa_glib_mainloop *m_mainloop; int m_references; static Context* s_context; }; } // QPulseAudio #endif // CONTEXT_H diff --git a/src/kcm/package/contents/ui/Advanced.qml b/src/kcm/package/contents/ui/Advanced.qml index 1682e4e..b305cf1 100644 --- a/src/kcm/package/contents/ui/Advanced.qml +++ b/src/kcm/package/contents/ui/Advanced.qml @@ -1,84 +1,94 @@ /* 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.1 import QtQuick.Controls 1.3 import org.kde.plasma.private.volume 0.1 ScrollView { id: scrollView ColumnLayout { Component.onCompleted: { // Normal binding causes binding loops width = Qt.binding(function() { return scrollView.viewport.width; }); } Header { Layout.fillWidth: true enabled: view.count > 0 text: i18n("Device Profiles") disabledText: i18nc("@label", "No Device Profiles Available") } ListView { id: view Layout.fillWidth: true Layout.preferredHeight: contentHeight Layout.margins: units.gridUnit / 2 interactive: false spacing: units.smallSpacing * 2 model: CardModel {} delegate: CardListItem {} } Header { Layout.fillWidth: true text: i18n("Advanced Output Configuration") } ModuleManager { id: moduleManager } CheckBox { Layout.fillWidth: true Layout.topMargin: units.smallSpacing Layout.leftMargin: units.gridUnit / 2 Layout.rightMargin: units.gridUnit / 2 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 { Layout.fillWidth: true Layout.leftMargin: units.gridUnit / 2 Layout.rightMargin: units.gridUnit / 2 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 index 491a72d..a1ae964 100644 --- a/src/maps.h +++ b/src/maps.h @@ -1,173 +1,175 @@ /* 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 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 Q_DECL_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; signals: void added(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 Q_DECL_EXPORT 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); const int modelIndex = m_data.keys().indexOf(object->index()); Q_ASSERT(modelIndex >= 0); 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); emit added(modelIndex); } } void removeEntry(quint32 index) { if (!m_data.contains(index)) { m_pendingRemovals.insert(index); } else { const int modelIndex = m_data.keys().indexOf(index); delete m_data.take(index); 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/module.cpp b/src/module.cpp new file mode 100644 index 0000000..63d495b --- /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/module.h similarity index 51% copy from src/modulemanager.h copy to src/module.h index 290312e..975f3d4 100644 --- a/src/modulemanager.h +++ b/src/module.h @@ -1,63 +1,59 @@ /* - 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 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 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/modulemanager.cpp b/src/modulemanager.cpp index 6c0679a..635adc8 100644 --- a/src/modulemanager.cpp +++ b/src/modulemanager.cpp @@ -1,112 +1,138 @@ /* Copyright 2016 David Edmundson 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 "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); if (enabled) { set(QStringLiteral("name0"), m_moduleName); set(QStringLiteral("args0"), args); set(QStringLiteral("enabled"), true); } else { set(QStringLiteral("enabled"), false); } set(QStringLiteral("locked"), false); } 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() { }; bool ModuleManager::combineSinks() const { return m_combineSinks->isEnabled(); } void ModuleManager::setCombineSinks(bool combineSinks) { m_combineSinks->setEnabled(combineSinks); } bool ModuleManager::switchOnConnect() const { //switch on connect and device-manager do the same task. Only one should be enabled //Note on the first run m_deviceManager will appear to be disabled even though it's actually running //because there is no gconf entry, however m_switchOnConnect will only exist if set by Plasma PA //hence only check this entry return m_switchOnConnect->isEnabled() ; } void ModuleManager::setSwitchOnConnect(bool switchOnConnect) { m_deviceManager->setEnabled(!switchOnConnect); 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/modulemanager.h b/src/modulemanager.h index 290312e..1d839a7 100644 --- a/src/modulemanager.h +++ b/src/modulemanager.h @@ -1,63 +1,69 @@ /* Copyright 2016 David Edmundson 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 MODULEMANAGER_H #define MODULEMANAGER_H #include #include #include "context.h" // Properties need fully qualified classes even with pointers. #include "client.h" class GConfItem; 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 #endif // STREAM_H diff --git a/src/pulseaudio.cpp b/src/pulseaudio.cpp index c0b4e70..71cfa67 100644 --- a/src/pulseaudio.cpp +++ b/src/pulseaudio.cpp @@ -1,362 +1,369 @@ /* 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::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); } 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) { 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); 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); } +ModuleModel::ModuleModel(QObject *parent) + : AbstractModel(&context()->modules(), parent) +{ + initRoleNames(Module::staticMetaObject); +} + } // QPulseAudio diff --git a/src/pulseaudio.h b/src/pulseaudio.h index cfc3e65..9594b90 100644 --- a/src/pulseaudio.h +++ b/src/pulseaudio.h @@ -1,149 +1,156 @@ /* 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 Q_DECL_EXPORT AbstractModel : public QAbstractListModel { 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); Context *context() const; 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); }; +class Q_DECL_EXPORT ModuleModel : public AbstractModel +{ + Q_OBJECT +public: + ModuleModel(QObject *parent = nullptr); +}; + } // QPulseAudio #endif // PULSEAUDIO_H diff --git a/src/qml/plugin.cpp b/src/qml/plugin.cpp index 9cbfc56..2126c3f 100644 --- a/src/qml/plugin.cpp +++ b/src/qml/plugin.cpp @@ -1,67 +1,68 @@ /* 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 "plugin.h" #include #include "pulseaudio.h" #include "client.h" #include "sink.h" #include "source.h" #include "context.h" #include "modulemanager.h" #include "port.h" #include "globalactioncollection.h" #include "volumeosd.h" #include "volumefeedback.h" static QJSValue pulseaudio_singleton(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) QJSValue object = scriptEngine->newObject(); object.setProperty(QStringLiteral("NormalVolume"), (double) QPulseAudio::Context::NormalVolume); object.setProperty(QStringLiteral("MinimalVolume"), (double) QPulseAudio::Context::MinimalVolume); object.setProperty(QStringLiteral("MaximalVolume"), (double) QPulseAudio::Context::MaximalVolume); return object; } void Plugin::registerTypes(const char* uri) { qmlRegisterType(uri, 0, 1, "CardModel"); qmlRegisterType(uri, 0, 1, "SinkModel"); qmlRegisterType(uri, 0, 1, "SinkInputModel"); qmlRegisterType(uri, 0, 1, "SourceModel"); 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"); qmlRegisterType(uri, 0, 1, "VolumeOSD"); qmlRegisterType(uri, 0, 1, "VolumeFeedback"); qmlRegisterSingletonType(uri, 0, 1, "PulseAudio", pulseaudio_singleton); qmlRegisterType(); qmlRegisterType(); qmlRegisterType(); }