diff --git a/src/context.cpp b/src/context.cpp --- a/src/context.cpp +++ b/src/context.cpp @@ -486,22 +486,7 @@ 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); + m_streamRestores.updateEntryByName(info, this); } void Context::serverCallback(const pa_server_info *info) diff --git a/src/kcm/package/contents/ui/Applications.qml b/src/kcm/package/contents/ui/Applications.qml --- a/src/kcm/package/contents/ui/Applications.qml +++ b/src/kcm/package/contents/ui/Applications.qml @@ -57,6 +57,11 @@ } delegate: StreamListItem { deviceModel: sinkModel + Component.onCompleted: { + seperatorVisible = Qt.binding(function() { + return sinkInputView.count > 0; + }); + } } } 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 @@ -57,7 +57,7 @@ text: { if (isEventStream) { return i18n("Notification Sounds"); - } else if (Client) { + } else if (typeof Client !== "undefined" && Client) { return i18nc("label of stream items", "%1: %2", Client.name, Name); } else { return Name; @@ -84,15 +84,9 @@ } } + property alias seperatorVisible: seperator.visible ListItemSeperator { + id: seperator view: delegate.ListView.view - - Component.onCompleted: { - if (isEventStream) { - visible = Qt.binding(function() { - return sinkInputView.count > 0; - }); - } - } } } diff --git a/src/kcm/package/contents/ui/StreamRestoreTab.qml b/src/kcm/package/contents/ui/StreamRestoreTab.qml new file mode 100644 --- /dev/null +++ b/src/kcm/package/contents/ui/StreamRestoreTab.qml @@ -0,0 +1,74 @@ +/* + Copyright 2014-2015 Harald Sitter + Copyright 2016 David Rosca + + 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.core 2.1 as PlasmaCore + +import org.kde.plasma.private.volume 0.1 + +ColumnLayout { + TextField { + id: filterField + Layout.fillWidth: true + placeholderText: i18n("Search Streams") + onTextChanged: filterModel.updateFilters() + } + + ScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true + + ListView { + id: eventStreamView + interactive: false + spacing: units.largeSpacing + model: PlasmaCore.SortFilterModel { + id: filterModel + sourceModel: StreamRestoreModel {} + filterCallback: filterModel.doFiltering + + function doFiltering(source_row, value) { + var idx = sourceModel.index(source_row, 0); + var streamName = sourceModel.data(idx, sourceModel.role("Name")); + if (streamName.toLowerCase().indexOf(filterField.text.toLowerCase()) >= 0) { + return true; + } + return false; + } + function emptyFilter(source_row, value) { + return true; // Show all + } + function updateFilters() { + // Manually trigger setFilterCallback() which will invalidate the filter. + filterModel.filterCallback = filterModel.emptyFilter; + filterModel.filterCallback = filterModel.doFiltering; + } + } + delegate: StreamListItem { + deviceModel: sinkModel + } + } + } + +} 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 @@ -69,6 +69,10 @@ Applications {} } Tab { + title: i18nc("@title:tab", "Stream Restore") + StreamRestoreTab {} + } + Tab { title: i18nc("@title:tab", "Advanced") Advanced {} } diff --git a/src/maps.h b/src/maps.h --- a/src/maps.h +++ b/src/maps.h @@ -168,7 +168,113 @@ typedef MapBase ClientMap; typedef MapBase CardMap; typedef MapBase ModuleMap; -typedef MapBase StreamRestoreMap; + + + +/** + * Maps a specific name to a specific object pointer. + * This is used to give the unique arbitrary PulseAudio name of a PulseObject a + * serialized list index. Namely it enables us to translate a discrete list + * index to a pulse name to an object, and any permutation thereof. + */ +template +class StringMapBase : public MapBaseQObject +{ +public: + virtual ~StringMapBase() {} + + 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->name())); + + m_data.insert(object->name(), object); + + const int modelIndex = m_data.keys().indexOf(object->name()); + 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 updateEntryByName(const PAInfo *info, QObject *parent) + { + Q_ASSERT(info); + + QString infoName = QString::fromUtf8(info->name); + + if (m_pendingRemovals.remove(infoName)) { + // Was already removed again. + return; + } + + const bool isNew = !m_data.contains(infoName); + + auto *obj = m_data.value(infoName, nullptr); + if (!obj) { + obj = new Type(parent); + } + obj->update(info); + m_data.insert(infoName, obj); + + if (isNew) { + const int modelIndex = m_data.keys().indexOf(infoName); + Q_ASSERT(modelIndex >= 0); + emit added(modelIndex); + } + } + + void removeEntry(QString name) + { + if (!m_data.contains(name)) { + m_pendingRemovals.insert(name); + } else { + const int modelIndex = m_data.keys().indexOf(name); + delete m_data.take(name); + emit removed(modelIndex); + } + } + +protected: + QMap m_data; + QSet m_pendingRemovals; +}; + +typedef StringMapBase StreamRestoreMap; } // QPulseAudio diff --git a/src/streamrestore.h b/src/streamrestore.h --- a/src/streamrestore.h +++ b/src/streamrestore.h @@ -41,7 +41,7 @@ Q_PROPERTY(QList channelVolumes READ channelVolumes NOTIFY channelVolumesChanged) Q_PROPERTY(quint32 deviceIndex READ deviceIndex WRITE setDeviceIndex NOTIFY deviceIndexChanged) public: - StreamRestore(quint32 index, const QVariantMap &properties, QObject *parent); + StreamRestore(QObject *parent); void update(const pa_ext_stream_restore_info *info); diff --git a/src/streamrestore.cpp b/src/streamrestore.cpp --- a/src/streamrestore.cpp +++ b/src/streamrestore.cpp @@ -25,15 +25,14 @@ namespace QPulseAudio { -StreamRestore::StreamRestore(quint32 index, const QVariantMap &properties, QObject *parent) +StreamRestore::StreamRestore(QObject *parent) : PulseObject(parent) , m_muted(false) { memset(&m_volume, 0, sizeof(m_volume)); memset(&m_channelMap, 0, sizeof(m_channelMap)); - m_index = index; - m_properties = properties; + m_properties.insert(QStringLiteral("application.icon_name"), QStringLiteral("preferences-desktop-notification")); } void StreamRestore::update(const pa_ext_stream_restore_info *info)