forked from Qortal/Brooklyn
520 lines
17 KiB
C++
520 lines
17 KiB
C++
/*
|
|
SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
|
|
SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
|
|
|
|
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
|
*/
|
|
|
|
#include "notificationgroupingproxymodel_p.h"
|
|
|
|
#include <QDateTime>
|
|
|
|
#include "notifications.h"
|
|
|
|
using namespace NotificationManager;
|
|
|
|
NotificationGroupingProxyModel::NotificationGroupingProxyModel(QObject *parent)
|
|
: QAbstractProxyModel(parent)
|
|
{
|
|
}
|
|
|
|
NotificationGroupingProxyModel::~NotificationGroupingProxyModel() = default;
|
|
|
|
bool NotificationGroupingProxyModel::appsMatch(const QModelIndex &a, const QModelIndex &b) const
|
|
{
|
|
const QString aName = a.data(Notifications::ApplicationNameRole).toString();
|
|
const QString bName = b.data(Notifications::ApplicationNameRole).toString();
|
|
|
|
const QString aDesktopEntry = a.data(Notifications::DesktopEntryRole).toString();
|
|
const QString bDesktopEntry = b.data(Notifications::DesktopEntryRole).toString();
|
|
|
|
const QString aOriginName = a.data(Notifications::OriginNameRole).toString();
|
|
const QString bOriginName = b.data(Notifications::OriginNameRole).toString();
|
|
|
|
return !aName.isEmpty() && aName == bName && aDesktopEntry == bDesktopEntry && aOriginName == bOriginName;
|
|
}
|
|
|
|
bool NotificationGroupingProxyModel::isGroup(int row) const
|
|
{
|
|
if (row < 0 || row >= rowMap.count()) {
|
|
return false;
|
|
}
|
|
|
|
return (rowMap.at(row)->count() > 1);
|
|
}
|
|
|
|
bool NotificationGroupingProxyModel::tryToGroup(const QModelIndex &sourceIndex, bool silent)
|
|
{
|
|
// Meat of the matter: Try to add this source row to a sub-list with source rows
|
|
// associated with the same application.
|
|
for (int i = 0; i < rowMap.count(); ++i) {
|
|
const QModelIndex &groupRep = sourceModel()->index(rowMap.at(i)->constFirst(), 0);
|
|
|
|
// Don't match a row with itself.
|
|
if (sourceIndex == groupRep) {
|
|
continue;
|
|
}
|
|
|
|
if (appsMatch(sourceIndex, groupRep)) {
|
|
const QModelIndex parent = index(i, 0);
|
|
|
|
if (!silent) {
|
|
const int newIndex = rowMap.at(i)->count();
|
|
|
|
if (newIndex == 1) {
|
|
beginInsertRows(parent, 0, 1);
|
|
} else {
|
|
beginInsertRows(parent, newIndex, newIndex);
|
|
}
|
|
}
|
|
|
|
rowMap[i]->append(sourceIndex.row());
|
|
|
|
if (!silent) {
|
|
endInsertRows();
|
|
|
|
Q_EMIT dataChanged(parent, parent);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void NotificationGroupingProxyModel::adjustMap(int anchor, int delta)
|
|
{
|
|
for (int i = 0; i < rowMap.count(); ++i) {
|
|
QVector<int> *sourceRows = rowMap.at(i);
|
|
QMutableVectorIterator<int> it(*sourceRows);
|
|
|
|
while (it.hasNext()) {
|
|
it.next();
|
|
|
|
if (it.value() >= anchor) {
|
|
it.setValue(it.value() + delta);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void NotificationGroupingProxyModel::rebuildMap()
|
|
{
|
|
qDeleteAll(rowMap);
|
|
rowMap.clear();
|
|
|
|
const int rows = sourceModel()->rowCount();
|
|
|
|
rowMap.reserve(rows);
|
|
|
|
for (int i = 0; i < rows; ++i) {
|
|
rowMap.append(new QVector<int>{i});
|
|
}
|
|
|
|
checkGrouping(true /* silent */);
|
|
}
|
|
|
|
void NotificationGroupingProxyModel::checkGrouping(bool silent)
|
|
{
|
|
for (int i = (rowMap.count()) - 1; i >= 0; --i) {
|
|
if (isGroup(i)) {
|
|
continue;
|
|
}
|
|
|
|
// FIXME support skip grouping hint, maybe?
|
|
// The new grouping keeps every notification separate, still, so perhaps we don't need to
|
|
|
|
if (tryToGroup(sourceModel()->index(rowMap.at(i)->constFirst(), 0), silent)) {
|
|
beginRemoveRows(QModelIndex(), i, i);
|
|
delete rowMap.takeAt(i); // Safe since we're iterating backwards.
|
|
endRemoveRows();
|
|
}
|
|
}
|
|
}
|
|
|
|
void NotificationGroupingProxyModel::formGroupFor(const QModelIndex &index)
|
|
{
|
|
// Already in group or a group.
|
|
if (index.parent().isValid() || isGroup(index.row())) {
|
|
return;
|
|
}
|
|
|
|
// We need to grab a source index as we may invalidate the index passed
|
|
// in through grouping.
|
|
const QModelIndex &sourceTarget = mapToSource(index);
|
|
|
|
for (int i = (rowMap.count() - 1); i >= 0; --i) {
|
|
const QModelIndex &sourceIndex = sourceModel()->index(rowMap.at(i)->constFirst(), 0);
|
|
|
|
if (!appsMatch(sourceTarget, sourceIndex)) {
|
|
continue;
|
|
}
|
|
|
|
if (tryToGroup(sourceIndex)) {
|
|
beginRemoveRows(QModelIndex(), i, i);
|
|
delete rowMap.takeAt(i); // Safe since we're iterating backwards.
|
|
endRemoveRows();
|
|
}
|
|
}
|
|
}
|
|
|
|
void NotificationGroupingProxyModel::setSourceModel(QAbstractItemModel *sourceModel)
|
|
{
|
|
if (sourceModel == QAbstractProxyModel::sourceModel()) {
|
|
return;
|
|
}
|
|
|
|
beginResetModel();
|
|
|
|
if (QAbstractProxyModel::sourceModel()) {
|
|
QAbstractProxyModel::sourceModel()->disconnect(this);
|
|
}
|
|
|
|
QAbstractProxyModel::setSourceModel(sourceModel);
|
|
|
|
if (sourceModel) {
|
|
rebuildMap();
|
|
|
|
// FIXME move this stuff into separate slot methods
|
|
|
|
connect(sourceModel, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int start, int end) {
|
|
if (parent.isValid()) {
|
|
return;
|
|
}
|
|
|
|
adjustMap(start, (end - start) + 1);
|
|
|
|
for (int i = start; i <= end; ++i) {
|
|
if (!tryToGroup(this->sourceModel()->index(i, 0))) {
|
|
beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count());
|
|
rowMap.append(new QVector<int>{i});
|
|
endInsertRows();
|
|
}
|
|
}
|
|
|
|
checkGrouping();
|
|
});
|
|
|
|
connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) {
|
|
if (parent.isValid()) {
|
|
return;
|
|
}
|
|
|
|
for (int i = first; i <= last; ++i) {
|
|
for (int j = 0; j < rowMap.count(); ++j) {
|
|
const QVector<int> *sourceRows = rowMap.at(j);
|
|
const int mapIndex = sourceRows->indexOf(i);
|
|
|
|
if (mapIndex != -1) {
|
|
// Remove top-level item.
|
|
if (sourceRows->count() == 1) {
|
|
beginRemoveRows(QModelIndex(), j, j);
|
|
delete rowMap.takeAt(j);
|
|
endRemoveRows();
|
|
// Dissolve group.
|
|
} else if (sourceRows->count() == 2) {
|
|
const QModelIndex parent = index(j, 0);
|
|
beginRemoveRows(parent, 0, 1);
|
|
rowMap[j]->remove(mapIndex);
|
|
endRemoveRows();
|
|
|
|
// We're no longer a group parent.
|
|
Q_EMIT dataChanged(parent, parent);
|
|
// Remove group member.
|
|
} else {
|
|
const QModelIndex parent = index(j, 0);
|
|
beginRemoveRows(parent, mapIndex, mapIndex);
|
|
rowMap[j]->remove(mapIndex);
|
|
endRemoveRows();
|
|
|
|
// Various roles of the parent evaluate child data, and the
|
|
// child list has changed.
|
|
Q_EMIT dataChanged(parent, parent);
|
|
|
|
// Signal children count change for all other items in the group.
|
|
Q_EMIT dataChanged(index(0, 0, parent), index(rowMap.count() - 1, 0, parent), {Notifications::GroupChildrenCountRole});
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &parent, int start, int end) {
|
|
if (parent.isValid()) {
|
|
return;
|
|
}
|
|
|
|
adjustMap(start + 1, -((end - start) + 1));
|
|
|
|
checkGrouping();
|
|
});
|
|
|
|
connect(sourceModel, &QAbstractItemModel::modelAboutToBeReset, this, &NotificationGroupingProxyModel::beginResetModel);
|
|
connect(sourceModel, &QAbstractItemModel::modelReset, this, [this] {
|
|
rebuildMap();
|
|
endResetModel();
|
|
});
|
|
|
|
connect(sourceModel,
|
|
&QAbstractItemModel::dataChanged,
|
|
this,
|
|
[this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {
|
|
for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
|
|
const QModelIndex &sourceIndex = this->sourceModel()->index(i, 0);
|
|
QModelIndex proxyIndex = mapFromSource(sourceIndex);
|
|
|
|
if (!proxyIndex.isValid()) {
|
|
return;
|
|
}
|
|
|
|
const QModelIndex parent = proxyIndex.parent();
|
|
|
|
// If a child item changes, its parent may need an update as well as many of
|
|
// the data roles evaluate child data. See data().
|
|
// TODO: Some roles do not need to bubble up as they fall through to the first
|
|
// child in data(); it _might_ be worth adding constraints here later.
|
|
if (parent.isValid()) {
|
|
Q_EMIT dataChanged(parent, parent, roles);
|
|
}
|
|
|
|
Q_EMIT dataChanged(proxyIndex, proxyIndex, roles);
|
|
}
|
|
});
|
|
}
|
|
|
|
endResetModel();
|
|
}
|
|
|
|
QModelIndex NotificationGroupingProxyModel::index(int row, int column, const QModelIndex &parent) const
|
|
{
|
|
if (row < 0 || column != 0) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
if (parent.isValid() && row < rowMap.at(parent.row())->count()) {
|
|
return createIndex(row, column, rowMap.at(parent.row()));
|
|
}
|
|
|
|
if (row < rowMap.count()) {
|
|
return createIndex(row, column, nullptr);
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex NotificationGroupingProxyModel::parent(const QModelIndex &child) const
|
|
{
|
|
if (child.internalPointer() == nullptr) {
|
|
return QModelIndex();
|
|
} else {
|
|
const int parentRow = rowMap.indexOf(static_cast<QVector<int> *>(child.internalPointer()));
|
|
|
|
if (parentRow != -1) {
|
|
return index(parentRow, 0);
|
|
}
|
|
|
|
// If we were asked to find the parent for an internalPointer we can't
|
|
// locate, we have corrupted data: This should not happen.
|
|
Q_ASSERT(parentRow != -1);
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex NotificationGroupingProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
|
|
{
|
|
if (!sourceIndex.isValid() || sourceIndex.model() != sourceModel()) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
for (int i = 0; i < rowMap.count(); ++i) {
|
|
const QVector<int> *sourceRows = rowMap.at(i);
|
|
const int childIndex = sourceRows->indexOf(sourceIndex.row());
|
|
const QModelIndex parent = index(i, 0);
|
|
|
|
if (childIndex == 0) {
|
|
// If the sub-list we found the source row in is larger than 1 (i.e. part
|
|
// of a group, map to the logical child item instead of the parent item
|
|
// the source row also stands in for. The parent is therefore unreachable
|
|
// from mapToSource().
|
|
if (isGroup(i)) {
|
|
return index(0, 0, parent);
|
|
// Otherwise map to the top-level item.
|
|
} else {
|
|
return parent;
|
|
}
|
|
} else if (childIndex != -1) {
|
|
return index(childIndex, 0, parent);
|
|
}
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex NotificationGroupingProxyModel::mapToSource(const QModelIndex &proxyIndex) const
|
|
{
|
|
if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
const QModelIndex &parent = proxyIndex.parent();
|
|
|
|
if (parent.isValid()) {
|
|
if (parent.row() < 0 || parent.row() >= rowMap.count()) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
return sourceModel()->index(rowMap.at(parent.row())->at(proxyIndex.row()), 0);
|
|
} else {
|
|
// Group parents items therefore equate to the first child item; the source
|
|
// row logically appears twice in the proxy.
|
|
// mapFromSource() is not required to handle this well (consider proxies can
|
|
// filter out rows, too) and opts to map to the child item, as the group parent
|
|
// has its Qt::DisplayRole mangled by data(), and it's more useful for trans-
|
|
// lating dataChanged() from the source model.
|
|
// NOTE we changed that to be last
|
|
if (rowMap.isEmpty()) { // FIXME
|
|
// How can this happen? (happens when closing a group)
|
|
return QModelIndex();
|
|
}
|
|
return sourceModel()->index(rowMap.at(proxyIndex.row())->constLast(), 0);
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
int NotificationGroupingProxyModel::rowCount(const QModelIndex &parent) const
|
|
{
|
|
if (!sourceModel()) {
|
|
return 0;
|
|
}
|
|
|
|
if (parent.isValid() && parent.model() == this) {
|
|
// Don't return row count for top-level item at child row: Group members
|
|
// never have further children of their own.
|
|
if (parent.parent().isValid()) {
|
|
return 0;
|
|
}
|
|
|
|
if (parent.row() < 0 || parent.row() >= rowMap.count()) {
|
|
return 0;
|
|
}
|
|
|
|
const int rowCount = rowMap.at(parent.row())->count();
|
|
|
|
// If this sub-list in the map only has one entry, it's a plain item, not
|
|
// parent to a group.
|
|
if (rowCount == 1) {
|
|
return 0;
|
|
} else {
|
|
return rowCount;
|
|
}
|
|
}
|
|
|
|
return rowMap.count();
|
|
}
|
|
|
|
bool NotificationGroupingProxyModel::hasChildren(const QModelIndex &parent) const
|
|
{
|
|
if ((parent.model() && parent.model() != this) || !sourceModel()) {
|
|
return false;
|
|
}
|
|
|
|
return rowCount(parent);
|
|
}
|
|
|
|
int NotificationGroupingProxyModel::columnCount(const QModelIndex &parent) const
|
|
{
|
|
Q_UNUSED(parent)
|
|
|
|
return 1;
|
|
}
|
|
|
|
QVariant NotificationGroupingProxyModel::data(const QModelIndex &proxyIndex, int role) const
|
|
{
|
|
if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
|
|
return QVariant();
|
|
}
|
|
|
|
const QModelIndex &parent = proxyIndex.parent();
|
|
const bool isGroup = (!parent.isValid() && this->isGroup(proxyIndex.row()));
|
|
|
|
// For group parent items, this will map to the last child task.
|
|
const QModelIndex &sourceIndex = mapToSource(proxyIndex);
|
|
|
|
if (!sourceIndex.isValid()) {
|
|
return QVariant();
|
|
}
|
|
|
|
if (isGroup) {
|
|
switch (role) {
|
|
case Notifications::IsGroupRole:
|
|
return true;
|
|
case Notifications::GroupChildrenCountRole:
|
|
return rowCount(proxyIndex);
|
|
case Notifications::IsInGroupRole:
|
|
return false;
|
|
|
|
// Combine all notifications into one for some basic grouping
|
|
case Notifications::BodyRole: {
|
|
QString body;
|
|
for (int i = 0; i < rowCount(proxyIndex); ++i) {
|
|
const QString stringData = index(i, 0, proxyIndex).data(role).toString();
|
|
if (!stringData.isEmpty()) {
|
|
if (!body.isEmpty()) {
|
|
body.append(QLatin1String("<br>"));
|
|
}
|
|
body.append(stringData);
|
|
}
|
|
}
|
|
return body;
|
|
}
|
|
|
|
case Notifications::DesktopEntryRole:
|
|
case Notifications::NotifyRcNameRole:
|
|
case Notifications::OriginNameRole:
|
|
for (int i = 0; i < rowCount(proxyIndex); ++i) {
|
|
const QString stringData = index(i, 0, proxyIndex).data(role).toString();
|
|
if (!stringData.isEmpty()) {
|
|
return stringData;
|
|
}
|
|
}
|
|
return QString();
|
|
|
|
case Notifications::ConfigurableRole: // if there is any configurable child item
|
|
for (int i = 0; i < rowCount(proxyIndex); ++i) {
|
|
if (index(i, 0, proxyIndex).data(Notifications::ConfigurableRole).toBool()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
|
|
case Notifications::ClosableRole: // if there is any closable child item
|
|
for (int i = 0; i < rowCount(proxyIndex); ++i) {
|
|
if (index(i, 0, proxyIndex).data(Notifications::ClosableRole).toBool()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
} else {
|
|
switch (role) {
|
|
case Notifications::IsGroupRole:
|
|
return false;
|
|
// So a notification knows with how many other items it is in a group
|
|
case Notifications::GroupChildrenCountRole:
|
|
if (proxyIndex.parent().isValid()) {
|
|
return rowCount(proxyIndex.parent());
|
|
}
|
|
break;
|
|
case Notifications::IsInGroupRole:
|
|
return parent.isValid();
|
|
}
|
|
}
|
|
|
|
return sourceIndex.data(role);
|
|
}
|