diff --git a/applets/notifications/package/contents/ui/CompactRepresentation.qml b/applets/notifications/package/contents/ui/CompactRepresentation.qml index b95a93ed8..f016fefc2 100644 --- a/applets/notifications/package/contents/ui/CompactRepresentation.qml +++ b/applets/notifications/package/contents/ui/CompactRepresentation.qml @@ -1,198 +1,179 @@ /* * Copyright 2018-2019 Kai Uwe Broulik * * 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.8 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents MouseArea { id: compactRoot readonly property bool inPanel: (plasmoid.location === PlasmaCore.Types.TopEdge || plasmoid.location === PlasmaCore.Types.RightEdge || plasmoid.location === PlasmaCore.Types.BottomEdge || plasmoid.location === PlasmaCore.Types.LeftEdge) Layout.minimumWidth: plasmoid.formFactor === PlasmaCore.Types.Horizontal ? height : units.iconSizes.small Layout.minimumHeight: plasmoid.formFactor === PlasmaCore.Types.Vertical ? width : (units.iconSizes.small + 2 * theme.mSize(theme.defaultFont).height) Layout.maximumWidth: inPanel ? units.iconSizeHints.panel : -1 Layout.maximumHeight: inPanel ? units.iconSizeHints.panel : -1 property int activeCount: 0 property int unreadCount: 0 property int jobsCount: 0 property int jobsPercentage: 0 property bool inhibited: false property bool wasExpanded: false onPressed: wasExpanded = plasmoid.expanded onClicked: plasmoid.expanded = !wasExpanded PlasmaCore.Svg { id: notificationSvg imagePath: "icons/notification" colorGroup: PlasmaCore.ColorScope.colorGroup } PlasmaCore.SvgItem { id: notificationIcon anchors.centerIn: parent width: units.roundToIconSize(Math.min(parent.width, parent.height)) height: width svg: notificationSvg visible: opacity > 0 elementId: "notification-disabled" Item { id: jobProgressItem anchors { left: parent.left top: parent.top bottom: parent.bottom } width: notificationIcon.width * (jobsPercentage / 100) clip: true visible: false PlasmaCore.SvgItem { anchors { left: parent.left top: parent.top bottom: parent.bottom } width: notificationIcon.width svg: notificationSvg elementId: "notification-progress-active" } } TightLabel { id: countLabel anchors.centerIn: parent font.pointSize: -1 // FIXME fontSizeMode is awful but FontMetrics also doesn't cut it font.pixelSize: Math.round(parent.height * (0.3 + 0.3 / text.length)) // TODO add animation when it changes? text: compactRoot.unreadCount || "" } PlasmaComponents.BusyIndicator { id: busyIndicator anchors.fill: parent visible: false running: visible } - - PlasmaCore.SvgItem { - id: notificationActiveItem - anchors.fill: parent - - svg: notificationSvg - elementId: "notification-active" - opacity: 0 - scale: 2 - visible: opacity > 0 - } } PlasmaCore.IconItem { id: dndIcon anchors.fill: parent source: "notifications-disabled" opacity: 0 scale: 2 visible: opacity > 0 } states: [ State { // active process when: compactRoot.jobsCount > 0 PropertyChanges { target: notificationIcon elementId: "notification-progress-inactive" } PropertyChanges { target: countLabel text: compactRoot.jobsCount } PropertyChanges { target: busyIndicator visible: true } PropertyChanges { target: jobProgressItem visible: true } }, - State { // active notification - when: compactRoot.activeCount > 0 - PropertyChanges { - target: notificationActiveItem - scale: 1 - opacity: 1 - } - }, State { // do not disturb when: compactRoot.inhibited PropertyChanges { target: dndIcon scale: 1 opacity: 1 } PropertyChanges { target: notificationIcon scale: 0 opacity: 0 } }, State { // unread notifications when: compactRoot.unreadCount > 0 PropertyChanges { target: notificationIcon elementId: "notification-empty" } PropertyChanges { target: countLabel text: compactRoot.unreadCount } } ] transitions: [ Transition { to: "*" // any state NumberAnimation { - targets: [notificationIcon, notificationActiveItem, dndIcon] + targets: [notificationIcon, dndIcon] properties: "opacity,scale" duration: units.longDuration easing.type: Easing.InOutQuad } } ] } diff --git a/libnotificationmanager/notifications.cpp b/libnotificationmanager/notifications.cpp index d7bf492d5..f265e140f 100644 --- a/libnotificationmanager/notifications.cpp +++ b/libnotificationmanager/notifications.cpp @@ -1,850 +1,850 @@ /* * Copyright 2019 Kai Uwe Broulik * * 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 "notifications.h" #include #include #include #include #include #include "notificationsmodel.h" #include "notificationfilterproxymodel_p.h" #include "notificationsortproxymodel_p.h" #include "notificationgroupingproxymodel_p.h" #include "notificationgroupcollapsingproxymodel_p.h" #include "limitedrowcountproxymodel_p.h" #include "jobsmodel.h" #include "settings.h" #include "notification.h" #include "utils_p.h" #include "debug.h" using namespace NotificationManager; class Q_DECL_HIDDEN Notifications::Private { public: explicit Private(Notifications *q); ~Private(); void initSourceModels(); void initProxyModels(); void updateCount(); bool showNotifications = true; bool showJobs = false; Notifications::GroupMode groupMode = Notifications::GroupDisabled; int groupLimit = 0; bool expandUnread = false; int activeNotificationsCount = 0; int expiredNotificationsCount = 0; int unreadNotificationsCount = 0; int activeJobsCount = 0; int jobsPercentage = 0; static bool isGroup(const QModelIndex &idx); static uint notificationId(const QModelIndex &idx); QModelIndex mapFromModel(const QModelIndex &idx) const; // NOTE when you add or re-arrange models make sure to update mapFromModel()! NotificationsModel::Ptr notificationsModel; JobsModel::Ptr jobsModel; QSharedPointer settings() const; KConcatenateRowsProxyModel *notificationsAndJobsModel = nullptr; NotificationFilterProxyModel *filterModel = nullptr; NotificationSortProxyModel *sortModel = nullptr; NotificationGroupingProxyModel *groupingModel = nullptr; NotificationGroupCollapsingProxyModel *groupCollapsingModel = nullptr; KDescendantsProxyModel *flattenModel = nullptr; LimitedRowCountProxyModel *limiterModel = nullptr; private: Notifications *q; }; Notifications::Private::Private(Notifications *q) : q(q) { } Notifications::Private::~Private() { } void Notifications::Private::initSourceModels() { Q_ASSERT(notificationsAndJobsModel); // initProxyModels must be called before initSourceModels if (showNotifications && !notificationsModel) { notificationsModel = NotificationsModel::createNotificationsModel(); connect(notificationsModel.data(), &NotificationsModel::lastReadChanged, q, [this] { updateCount(); emit q->lastReadChanged(); }); notificationsAndJobsModel->addSourceModel(notificationsModel.data()); } else if (!showNotifications && notificationsModel) { notificationsAndJobsModel->removeSourceModel(notificationsModel.data()); disconnect(notificationsModel.data(), nullptr, q, nullptr); // disconnect all notificationsModel = nullptr; } if (showJobs && !jobsModel) { jobsModel = JobsModel::createJobsModel(); notificationsAndJobsModel->addSourceModel(jobsModel.data()); jobsModel->init(); } else if (!showJobs && jobsModel) { notificationsAndJobsModel->removeSourceModel(jobsModel.data()); jobsModel = nullptr; } } void Notifications::Private::initProxyModels() { /* The data flow is as follows: * NOTE when you add or re-arrange models make sure to update mapFromModel()! * * NotificationsModel JobsModel * \\ / * \\ / * KConcatenateRowsProxyModel * ||| * ||| * NotificationFilterProxyModel * (filters by urgency, whitelist, etc) * | * | * NotificationSortProxyModel * (sorts by urgency, date, etc) * | * --- BEGIN: Only when grouping is enabled --- * | * NotificationGroupingProxyModel * (turns list into tree grouped by app) * //\\ * //\\ * NotificationGroupCollapsingProxyModel * (limits number of tree leaves for expand/collapse feature) * /\ * /\ * KDescendantsProxyModel * (flattens tree back into a list for consumption in ListView) * | * --- END: Only when grouping is enabled --- * | * LimitedRowCountProxyModel * (limits the total number of items in the model) * | * | * \o/ <- Happy user seeing their notifications */ if (!notificationsAndJobsModel) { notificationsAndJobsModel = new KConcatenateRowsProxyModel(q); } if (!filterModel) { filterModel = new NotificationFilterProxyModel(); connect(filterModel, &NotificationFilterProxyModel::urgenciesChanged, q, &Notifications::urgenciesChanged); connect(filterModel, &NotificationFilterProxyModel::showExpiredChanged, q, &Notifications::showExpiredChanged); connect(filterModel, &NotificationFilterProxyModel::showDismissedChanged, q, &Notifications::showDismissedChanged); connect(filterModel, &NotificationFilterProxyModel::blacklistedDesktopEntriesChanged, q, &Notifications::blacklistedDesktopEntriesChanged); connect(filterModel, &NotificationFilterProxyModel::blacklistedNotifyRcNamesChanged, q, &Notifications::blacklistedNotifyRcNamesChanged); connect(filterModel, &QAbstractItemModel::rowsInserted, q, [this] { updateCount(); }); connect(filterModel, &QAbstractItemModel::rowsRemoved, q, [this] { updateCount(); }); connect(filterModel, &QAbstractItemModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { Q_UNUSED(topLeft); Q_UNUSED(bottomRight); if (roles.isEmpty() || roles.contains(Notifications::UpdatedRole) || roles.contains(Notifications::ExpiredRole) || roles.contains(Notifications::JobStateRole) || roles.contains(Notifications::PercentageRole)) { updateCount(); } }); filterModel->setSourceModel(notificationsAndJobsModel); } if (!sortModel) { sortModel = new NotificationSortProxyModel(q); connect(sortModel, &NotificationSortProxyModel::sortModeChanged, q, &Notifications::sortModeChanged); } if (!limiterModel) { limiterModel = new LimitedRowCountProxyModel(q); connect(limiterModel, &LimitedRowCountProxyModel::limitChanged, q, &Notifications::limitChanged); } if (groupMode == GroupApplicationsFlat) { if (!groupingModel) { groupingModel = new NotificationGroupingProxyModel(q); groupingModel->setSourceModel(filterModel); } if (!groupCollapsingModel) { groupCollapsingModel = new NotificationGroupCollapsingProxyModel(q); groupCollapsingModel->setLimit(groupLimit); groupCollapsingModel->setExpandUnread(expandUnread); groupCollapsingModel->setLastRead(q->lastRead()); groupCollapsingModel->setSourceModel(groupingModel); } sortModel->setSourceModel(groupCollapsingModel); flattenModel = new KDescendantsProxyModel(q); flattenModel->setSourceModel(sortModel); limiterModel->setSourceModel(flattenModel); } else { sortModel->setSourceModel(filterModel); limiterModel->setSourceModel(sortModel); delete flattenModel; flattenModel = nullptr; delete groupingModel; groupingModel = nullptr; } q->setSourceModel(limiterModel); } void Notifications::Private::updateCount() { int active = 0; int expired = 0; int unread = 0; int jobs = 0; int totalPercentage = 0; // We want to get the numbers after main filtering (urgencies, whitelists, etc) // but before any limiting or group limiting, hence asking the filterModel for advice // at which point notifications and jobs also have already been merged for (int i = 0; i < filterModel->rowCount(); ++i) { const QModelIndex idx = filterModel->index(i, 0); if (idx.data(Notifications::ExpiredRole).toBool()) { ++expired; } else { ++active; } QDateTime date = idx.data(Notifications::UpdatedRole).toDateTime(); if (!date.isValid()) { date = idx.data(Notifications::CreatedRole).toDateTime(); } // TODO Jobs could also be unread? if (notificationsModel) { - if (date > notificationsModel->lastRead()) { + if (!active && date > notificationsModel->lastRead()) { ++unread; } } if (idx.data(Notifications::TypeRole).toInt() == Notifications::JobType) { if (idx.data(Notifications::JobStateRole).toInt() != Notifications::JobStateStopped) { ++jobs; totalPercentage += idx.data(Notifications::PercentageRole).toInt(); } } } if (activeNotificationsCount != active) { activeNotificationsCount = active; emit q->activeNotificationsCountChanged(); } if (expiredNotificationsCount != expired) { expiredNotificationsCount = expired; emit q->expiredNotificationsCountChanged(); } if (unreadNotificationsCount != unread) { unreadNotificationsCount = unread; emit q->unreadNotificationsCountChanged(); } if (activeJobsCount != jobs) { activeJobsCount = jobs; emit q->activeJobsCountChanged(); } const int percentage = (jobs > 0 ? totalPercentage / jobs : 0); if (jobsPercentage != percentage) { jobsPercentage = percentage; emit q->jobsPercentageChanged(); } // TODO don't emit in dataChanged emit q->countChanged(); } bool Notifications::Private::isGroup(const QModelIndex &idx) { return idx.data(Notifications::IsGroupRole).toBool(); } uint Notifications::Private::notificationId(const QModelIndex &idx) { return idx.data(Notifications::IdRole).toUInt(); } QModelIndex Notifications::Private::mapFromModel(const QModelIndex &idx) const { QModelIndex resolvedIdx = idx; QAbstractItemModel *models[] = { notificationsAndJobsModel, filterModel, sortModel, groupingModel, groupCollapsingModel, flattenModel, limiterModel, }; // TODO can we do this with a generic loop like mapFromModel while (resolvedIdx.isValid() && resolvedIdx.model() != q) { const auto *idxModel = resolvedIdx.model(); // HACK try to find the model that uses the index' model as source bool found = false; for (QAbstractItemModel *model : models) { if (!model) { continue; } if (auto *proxyModel = qobject_cast(model)) { if (proxyModel->sourceModel() == idxModel) { resolvedIdx = proxyModel->mapFromSource(resolvedIdx); found = true; break; } } else if (auto *concatenateModel = qobject_cast(model)) { // There's no "sourceModels()" on KConcatenateRowsProxyModel if (idxModel == notificationsModel.data() || idxModel == jobsModel.data()) { resolvedIdx = concatenateModel->mapFromSource(resolvedIdx); found = true; break; } } } if (!found) { break; } } return resolvedIdx; } QSharedPointer Notifications::Private::settings() const { static QWeakPointer s_instance; if (!s_instance) { QSharedPointer ptr(new Settings()); s_instance = ptr.toWeakRef(); return ptr; } return s_instance.toStrongRef(); } Notifications::Notifications(QObject *parent) : QSortFilterProxyModel(parent) , d(new Private(this)) { // The proxy models are always the same, just with different // properties set whereas we want to avoid loading a source model // e.g. notifications or jobs when we're not actually using them d->initProxyModels(); // init source models when used from C++ QMetaObject::invokeMethod(this, [this] { d->initSourceModels(); }, Qt::QueuedConnection); } Notifications::~Notifications() = default; void Notifications::classBegin() { } void Notifications::componentComplete() { // init source models when used from QML d->initSourceModels(); } int Notifications::limit() const { return d->limiterModel->limit(); } void Notifications::setLimit(int limit) { d->limiterModel->setLimit(limit); } int Notifications::groupLimit() const { return d->groupLimit; } void Notifications::setGroupLimit(int limit) { if (d->groupLimit == limit) { return; } d->groupLimit = limit; if (d->groupCollapsingModel) { d->groupCollapsingModel->setLimit(limit); } emit groupLimitChanged(); } bool Notifications::expandUnread() const { return d->expandUnread; } void Notifications::setExpandUnread(bool expand) { if (d->expandUnread == expand) { return; } d->expandUnread = expand; if (d->groupCollapsingModel) { d->groupCollapsingModel->setExpandUnread(expand); } emit expandUnreadChanged(); } bool Notifications::showExpired() const { return d->filterModel->showExpired(); } void Notifications::setShowExpired(bool show) { d->filterModel->setShowExpired(show); } bool Notifications::showDismissed() const { return d->filterModel->showDismissed(); } void Notifications::setShowDismissed(bool show) { d->filterModel->setShowDismissed(show); } QStringList Notifications::blacklistedDesktopEntries() const { return d->filterModel->blacklistedDesktopEntries(); } void Notifications::setBlacklistedDesktopEntries(const QStringList &blacklist) { d->filterModel->setBlackListedDesktopEntries(blacklist); } QStringList Notifications::blacklistedNotifyRcNames() const { return d->filterModel->blacklistedNotifyRcNames(); } void Notifications::setBlacklistedNotifyRcNames(const QStringList &blacklist) { d->filterModel->setBlacklistedNotifyRcNames(blacklist); } QStringList Notifications::whitelistedDesktopEntries() const { return d->filterModel->whitelistedDesktopEntries(); } void Notifications::setWhitelistedDesktopEntries(const QStringList &whitelist) { d->filterModel->setWhiteListedDesktopEntries(whitelist); } QStringList Notifications::whitelistedNotifyRcNames() const { return d->filterModel->whitelistedNotifyRcNames(); } void Notifications::setWhitelistedNotifyRcNames(const QStringList &whitelist) { d->filterModel->setWhitelistedNotifyRcNames(whitelist); } bool Notifications::showNotifications() const { return d->showNotifications; } void Notifications::setShowNotifications(bool show) { if (d->showNotifications == show) { return; } d->showNotifications = show; d->initSourceModels(); emit showNotificationsChanged(); } bool Notifications::showJobs() const { return d->showJobs; } void Notifications::setShowJobs(bool show) { if (d->showJobs == show) { return; } d->showJobs = show; d->initSourceModels(); emit showJobsChanged(); } Notifications::Urgencies Notifications::urgencies() const { return d->filterModel->urgencies(); } void Notifications::setUrgencies(Urgencies urgencies) { d->filterModel->setUrgencies(urgencies); } Notifications::SortMode Notifications::sortMode() const { return d->sortModel->sortMode(); } void Notifications::setSortMode(SortMode sortMode) { d->sortModel->setSortMode(sortMode); } Notifications::GroupMode Notifications::groupMode() const { return d->groupMode; } void Notifications::setGroupMode(GroupMode groupMode) { if (d->groupMode != groupMode) { d->groupMode = groupMode; d->initProxyModels(); emit groupModeChanged(); } } int Notifications::count() const { return rowCount(QModelIndex()); } int Notifications::activeNotificationsCount() const { return d->activeNotificationsCount; } int Notifications::expiredNotificationsCount() const { return d->expiredNotificationsCount; } QDateTime Notifications::lastRead() const { if (d->notificationsModel) { return d->notificationsModel->lastRead(); } return QDateTime(); } void Notifications::setLastRead(const QDateTime &lastRead) { // TODO jobs could also be unread? if (d->notificationsModel) { d->notificationsModel->setLastRead(lastRead); } if (d->groupCollapsingModel) { d->groupCollapsingModel->setLastRead(lastRead); } } void Notifications::resetLastRead() { setLastRead(QDateTime::currentDateTimeUtc()); } int Notifications::unreadNotificationsCount() const { return d->unreadNotificationsCount; } int Notifications::activeJobsCount() const { return d->activeJobsCount; } int Notifications::jobsPercentage() const { return d->jobsPercentage; } QPersistentModelIndex Notifications::makePersistentModelIndex(const QModelIndex &idx) const { return QPersistentModelIndex(idx); } void Notifications::expire(const QModelIndex &idx) { switch (static_cast(idx.data(Notifications::TypeRole).toInt())) { case Notifications::NotificationType: d->notificationsModel->expire(Private::notificationId(idx)); break; case Notifications::JobType: d->jobsModel->expire(Utils::mapToModel(idx, d->jobsModel.data())); break; default: Q_UNREACHABLE(); } } void Notifications::close(const QModelIndex &idx) { if (idx.data(Notifications::IsGroupRole).toBool()) { const QModelIndex groupIdx = Utils::mapToModel(idx, d->groupingModel); if (!groupIdx.isValid()) { qCWarning(NOTIFICATIONMANAGER) << "Failed to find group model index for this item"; return; } Q_ASSERT(groupIdx.model() == d->groupingModel); const int childCount = d->groupingModel->rowCount(groupIdx); for (int i = childCount - 1; i >= 0; --i) { const QModelIndex childIdx = d->groupingModel->index(i, 0, groupIdx); close(childIdx); } return; } if (!idx.data(Notifications::ClosableRole).toBool()) { return; } switch (static_cast(idx.data(Notifications::TypeRole).toInt())) { case Notifications::NotificationType: d->notificationsModel->close(Private::notificationId(idx)); break; case Notifications::JobType: d->jobsModel->close(Utils::mapToModel(idx, d->jobsModel.data())); break; default: Q_UNREACHABLE(); } } void Notifications::configure(const QModelIndex &idx) { if (!d->notificationsModel) { return; } // For groups just configure the application, not the individual event if (Private::isGroup(idx)) { const QString desktopEntry = idx.data(Notifications::DesktopEntryRole).toString(); const QString notifyRcName = idx.data(Notifications::NotifyRcNameRole).toString(); d->notificationsModel->configure(desktopEntry, notifyRcName, QString() /*eventId*/); return; } d->notificationsModel->configure(Private::notificationId(idx)); } void Notifications::invokeDefaultAction(const QModelIndex &idx) { if (d->notificationsModel) { d->notificationsModel->invokeDefaultAction(Private::notificationId(idx)); } } void Notifications::invokeAction(const QModelIndex &idx, const QString &actionId) { if (d->notificationsModel) { d->notificationsModel->invokeAction(Private::notificationId(idx), actionId); } } void Notifications::startTimeout(const QModelIndex &idx) { startTimeout(Private::notificationId(idx)); } void Notifications::startTimeout(uint notificationId) { if (d->notificationsModel) { d->notificationsModel->startTimeout(notificationId); } } void Notifications::stopTimeout(const QModelIndex &idx) { if (d->notificationsModel) { d->notificationsModel->stopTimeout(Private::notificationId(idx)); } } void Notifications::suspendJob(const QModelIndex &idx) { if (d->jobsModel) { d->jobsModel->suspend(Utils::mapToModel(idx, d->jobsModel.data())); } } void Notifications::resumeJob(const QModelIndex &idx) { if (d->jobsModel) { d->jobsModel->resume(Utils::mapToModel(idx, d->jobsModel.data())); } } void Notifications::killJob(const QModelIndex &idx) { if (d->jobsModel) { d->jobsModel->kill(Utils::mapToModel(idx, d->jobsModel.data())); } } void Notifications::clear(ClearFlags flags) { if (d->notificationsModel) { d->notificationsModel->clear(flags); } if (d->jobsModel) { d->jobsModel->clear(flags); } } QModelIndex Notifications::groupIndex(const QModelIndex &idx) const { if (idx.data(Notifications::IsGroupRole).toBool()) { return idx; } if (idx.data(Notifications::IsInGroupRole).toBool()) { QModelIndex groupingIdx = Utils::mapToModel(idx, d->groupingModel); return d->mapFromModel(groupingIdx.parent()); } qCWarning(NOTIFICATIONMANAGER) << "Cannot get group index for item that isn't a group or inside one"; return QModelIndex(); } void Notifications::collapseAllGroups() { if (d->groupCollapsingModel) { d->groupCollapsingModel->collapseAll(); } } QVariant Notifications::data(const QModelIndex &index, int role) const { return QSortFilterProxyModel::data(index, role); } bool Notifications::setData(const QModelIndex &index, const QVariant &value, int role) { return QSortFilterProxyModel::setData(index, value, role); } bool Notifications::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } bool Notifications::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { return QSortFilterProxyModel::lessThan(source_left, source_right); } int Notifications::rowCount(const QModelIndex &parent) const { return QSortFilterProxyModel::rowCount(parent); } QHash Notifications::roleNames() const { static QHash s_roles; if (s_roles.isEmpty()) { s_roles = QSortFilterProxyModel::roleNames(); // This generates role names from the Roles enum in the form of: FooRole -> foo const QMetaEnum e = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("Roles")); for (int i = 0; i < e.keyCount(); ++i) { const int value = e.value(i); QByteArray key(e.key(i)); key[0] = key[0] + 32; // lower case first letter key.chop(4); // strip "Role" suffix s_roles.insert(value, key); } s_roles.insert(IdRole, QByteArrayLiteral("notificationId")); // id is QML-reserved } return s_roles; }