diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ volumeobject.cpp debug.cpp server.cpp + eventstream.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 @@ -70,10 +70,12 @@ void clientCallback(const pa_client_info *info); void cardCallback(const pa_card_info *info); void serverCallback(const pa_server_info *info); + void streamRestoreCallback(const pa_ext_stream_restore_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, diff --git a/src/context.cpp b/src/context.cpp --- a/src/context.cpp +++ b/src/context.cpp @@ -32,6 +32,7 @@ #include "sinkinput.h" #include "source.h" #include "sourceoutput.h" +#include "eventstream.h" namespace QPulseAudio { @@ -68,6 +69,12 @@ { if (!isGoodState(eol)) 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); @@ -138,6 +145,24 @@ ((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) @@ -326,18 +351,12 @@ return; } - // TODO - /* These calls are not always supported */ - // if ((o = pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, NULL))) { - // pa_operation_unref(o); - - // pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, NULL); - - // if ((o = pa_ext_stream_restore_subscribe(c, 1, NULL, NULL))) - // pa_operation_unref(o); - // } else { - // qCWarning(PLASMAPA) << "Failed to initialize stream_restore extension: " << pa_strerror(pa_context_errno(m_context)); - // } + 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) { @@ -385,6 +404,34 @@ m_server->update(info); } +void Context::streamRestoreCallback(const pa_ext_stream_restore_info *info) +{ + if (qstrcmp(info->name, "sink-input-by-media-role:event") != 0) { + return; + } + + // This is hacky, including pa_ext_stream_restore as "real" stream + // into sink inputs to make it possible to configure Event Sounds pseudostream. + + static const quint32 eventRoleIndex = PA_INVALID_INDEX; + const bool isNew = !m_sinkInputs.data().contains(eventRoleIndex); + + EventStream *obj = qobject_cast(m_sinkInputs.data().value(eventRoleIndex)); + if (!obj) { + obj = new EventStream(this); + m_sinkInputs.data().insert(eventRoleIndex, obj); + } + const int modelIndex = m_sinkInputs.data().keys().indexOf(eventRoleIndex); + Q_ASSERT(modelIndex >= 0); + obj->update(info); + + if (isNew) { + emit m_sinkInputs.added(modelIndex); + } else { + emit m_sinkInputs.updated(modelIndex); + } +} + void Context::setCardProfile(quint32 index, const QString &profile) { qCDebug(PLASMAPA) << index << profile; @@ -419,6 +466,19 @@ } } +void Context::streamRestoreWrite(const pa_ext_stream_restore_info *info) +{ + 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); diff --git a/src/stream.cpp b/src/eventstream.h copy from src/stream.cpp copy to src/eventstream.h --- a/src/stream.cpp +++ b/src/eventstream.h @@ -1,5 +1,5 @@ /* - 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 @@ -18,36 +18,30 @@ License along with this library. If not, see . */ -#include "stream.h" +#ifndef EVENTSTREAM_H +#define EVENTSTREAM_H + +#include "sinkinput.h" namespace QPulseAudio { -Stream::Stream(QObject *parent) - : VolumeObject(parent) +class Q_DECL_EXPORT EventStream : public SinkInput { - m_volumeWritable = false; - m_hasVolume = false; - m_virtualStream = false; -} + Q_OBJECT +public: + EventStream(QObject *parent); -Stream::~Stream() -{ -} + void update(const pa_ext_stream_restore_info *info); + void setVolume(qint64 volume) Q_DECL_OVERRIDE; + void setMuted(bool muted) Q_DECL_OVERRIDE; -QString Stream::name() const -{ - return m_name; -} +private: + void changeVolume(qint64 volume, bool muted); -Client *Stream::client() const -{ - return context()->clients().data().value(m_clientIndex, nullptr); -} - -bool Stream::isVirtualStream() const -{ - return m_virtualStream; -} + QString m_device; +}; } // QPulseAudio + +#endif // EVENTSTREAM_H diff --git a/src/eventstream.cpp b/src/eventstream.cpp new file mode 100644 --- /dev/null +++ b/src/eventstream.cpp @@ -0,0 +1,77 @@ +/* + 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 "eventstream.h" + +namespace QPulseAudio +{ + +EventStream::EventStream(QObject *parent) + : SinkInput(parent) +{ + m_clientIndex = PA_INVALID_INDEX; + m_eventStream = true; + m_hasVolume = true; + m_volumeWritable = true; + m_properties.insert(QStringLiteral("application.icon_name"), + QStringLiteral("preferences-desktop-notification")); +} + +void EventStream::update(const pa_ext_stream_restore_info *info) +{ + updateVolumeObject(info); + + QString infoName = QString::fromUtf8(info->name); + if (m_name != infoName) { + m_name = infoName; + emit nameChanged(); + } + + m_device = QString::fromUtf8(info->device); +} + +void EventStream::setVolume(qint64 volume) +{ + changeVolume(volume, isMuted()); +} + +void EventStream::setMuted(bool muted) +{ + changeVolume(volume(), muted); +} + +void EventStream::changeVolume(qint64 volume, bool muted) +{ + const QByteArray nameData = m_name.toUtf8(); + const QByteArray deviceData = m_device.toUtf8(); + + pa_ext_stream_restore_info info; + info.name = nameData.constData(); + info.channel_map.channels = 1; + info.channel_map.map[0] = PA_CHANNEL_POSITION_MONO; + info.volume = m_volume; + info.volume.values[0] = volume; + info.device = deviceData.isEmpty() ? nullptr : deviceData.constData(); + info.mute = muted; + + context()->streamRestoreWrite(&info); +} + +} // QPulseAudio diff --git a/src/kcm/package/contents/ui/StreamListItem.qml b/src/kcm/package/contents/ui/StreamListItem.qml --- a/src/kcm/package/contents/ui/StreamListItem.qml +++ b/src/kcm/package/contents/ui/StreamListItem.qml @@ -54,19 +54,24 @@ Label { id: inputText Layout.fillWidth: true - text: PulseObject.client ? i18nc("label of stream items", "%1: %2", - PulseObject.client.name, - PulseObject.name) - : PulseObject.name + text: { + if (PulseObject.eventStream) { + return i18n("Notification Sounds"); + } else if (PulseObject.client) { + return i18nc("label of stream items", "%1: %2", PulseObject.client.name, PulseObject.name); + } else { + return PulseObject.name; + } + } elide: Text.ElideRight } DeviceComboBox { id: deviceComboBox Layout.leftMargin: units.smallSpacing Layout.rightMargin: units.smallSpacing Layout.preferredWidth: delegate.width / 3 - visible: count > 1 + visible: !PulseObject.eventStream && count > 1 } MuteButton { diff --git a/src/kcm/package/contents/ui/main.qml b/src/kcm/package/contents/ui/main.qml --- a/src/kcm/package/contents/ui/main.qml +++ b/src/kcm/package/contents/ui/main.qml @@ -39,6 +39,8 @@ title: i18nc("@title:tab", "Applications") SinkInputView { model: PulseObjectFilterModel { + sortRole: "EventStream" + sortOrder: Qt.DescendingOrder sourceModel: SinkInputModel {} } emptyText: i18nc("@label", "No Applications Playing Audio") diff --git a/src/maps.h b/src/maps.h --- a/src/maps.h +++ b/src/maps.h @@ -64,6 +64,7 @@ public: virtual ~MapBase() {} + QMap &data() { return m_data; } const QMap &data() const { return m_data; } int modelIndexForQObject(QObject *qobject) const diff --git a/src/pulseobject.h b/src/pulseobject.h --- a/src/pulseobject.h +++ b/src/pulseobject.h @@ -25,6 +25,7 @@ #include #include +#include namespace QPulseAudio { @@ -56,6 +57,7 @@ } emit propertiesChanged(); } + void updatePulseObject(const pa_ext_stream_restore_info *) { } quint32 index() const; QVariantMap properties() const; diff --git a/src/stream.h b/src/stream.h --- a/src/stream.h +++ b/src/stream.h @@ -41,6 +41,7 @@ Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QPulseAudio::Client *client READ client NOTIFY clientChanged) Q_PROPERTY(bool virtualStream READ isVirtualStream NOTIFY virtualStreamChanged) + Q_PROPERTY(bool eventStream READ isEventStream NOTIFY eventStreamChanged) public: template void updateStream(const PAInfo *info) @@ -72,20 +73,22 @@ QString name() const; Client *client() const; bool isVirtualStream() const; + bool isEventStream() const; signals: void nameChanged(); void clientChanged(); void virtualStreamChanged(); + void eventStreamChanged(); protected: Stream(QObject *parent); virtual ~Stream(); -private: QString m_name; quint32 m_clientIndex; bool m_virtualStream; + bool m_eventStream; }; } // QPulseAudio diff --git a/src/stream.cpp b/src/stream.cpp --- a/src/stream.cpp +++ b/src/stream.cpp @@ -25,10 +25,12 @@ Stream::Stream(QObject *parent) : VolumeObject(parent) + , m_clientIndex(PA_INVALID_INDEX) + , m_virtualStream(false) + , m_eventStream(false) { m_volumeWritable = false; m_hasVolume = false; - m_virtualStream = false; } Stream::~Stream() @@ -50,4 +52,9 @@ return m_virtualStream; } +bool Stream::isEventStream() const +{ + return m_eventStream; +} + } // QPulseAudio