File indexing completed on 2024-05-12 05:04:14

0001 // SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
0002 // SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
0003 // SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
0004 // SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 
0006 #include "notificationgroupingmodel.h"
0007 
0008 NotificationGroupingModel::NotificationGroupingModel(QObject *parent)
0009     : QAbstractProxyModel(parent)
0010 {
0011 }
0012 
0013 bool NotificationGroupingModel::notificationsMatch(const QModelIndex &a, const QModelIndex &b)
0014 {
0015     const QString aId = a.data(AbstractTimelineModel::CustoRoles::IdRole).toString();
0016     const QString bId = b.data(AbstractTimelineModel::CustoRoles::IdRole).toString();
0017 
0018     const QVariant aType = a.data(AbstractTimelineModel::CustoRoles::TypeRole);
0019     const QVariant bType = b.data(AbstractTimelineModel::CustoRoles::TypeRole);
0020 
0021     // it makes no sense to group poll or edit updates
0022     if (aType == bType && (aType == Notification::Type::Poll || aType == Notification::Type::Update || aType == Notification::Type::Status)) {
0023         return false;
0024     }
0025 
0026     return aId == bId && aType == bType;
0027 }
0028 
0029 bool NotificationGroupingModel::isGroup(int row) const
0030 {
0031     if (row < 0 || row >= rowMap.count()) {
0032         return false;
0033     }
0034 
0035     return (rowMap.at(row)->count() > 1);
0036 }
0037 
0038 bool NotificationGroupingModel::tryToGroup(const QModelIndex &sourceIndex, bool silent)
0039 {
0040     // Meat of the matter: Try to add this source row to a sub-list with source rows
0041     // associated with the same application.
0042     for (int i = 0; i < rowMap.count(); ++i) {
0043         const QModelIndex &groupRep = sourceModel()->index(rowMap.at(i)->constFirst(), 0);
0044 
0045         // Don't match a row with itself.
0046         if (sourceIndex == groupRep) {
0047             continue;
0048         }
0049 
0050         if (notificationsMatch(sourceIndex, groupRep)) {
0051             const QModelIndex parent = index(i, 0, QModelIndex());
0052 
0053             if (!silent) {
0054                 const int newIndex = rowMap.at(i)->count();
0055 
0056                 if (newIndex == 1) {
0057                     beginInsertRows(parent, 0, 1);
0058                 } else {
0059                     beginInsertRows(parent, newIndex, newIndex);
0060                 }
0061             }
0062 
0063             rowMap[i]->append(sourceIndex.row());
0064 
0065             if (!silent) {
0066                 endInsertRows();
0067 
0068                 Q_EMIT dataChanged(parent, parent);
0069             }
0070 
0071             return true;
0072         }
0073     }
0074 
0075     return false;
0076 }
0077 
0078 void NotificationGroupingModel::adjustMap(int anchor, int delta)
0079 {
0080     for (int i = 0; i < rowMap.count(); ++i) {
0081         QList<int> *sourceRows = rowMap.at(i);
0082         for (auto it = sourceRows->begin(); it != sourceRows->end(); ++it) {
0083             if ((*it) >= anchor) {
0084                 *it += delta;
0085             }
0086         }
0087     }
0088 }
0089 
0090 void NotificationGroupingModel::rebuildMap()
0091 {
0092     qDeleteAll(rowMap);
0093     rowMap.clear();
0094 
0095     const int rows = sourceModel()->rowCount();
0096 
0097     rowMap.reserve(rows);
0098 
0099     for (int i = 0; i < rows; ++i) {
0100         rowMap.append(new QList<int>{i});
0101     }
0102 
0103     checkGrouping(true);
0104 }
0105 
0106 void NotificationGroupingModel::checkGrouping(bool silent)
0107 {
0108     for (int i = (rowMap.count()) - 1; i >= 0; --i) {
0109         if (isGroup(i)) {
0110             continue;
0111         }
0112 
0113         if (tryToGroup(sourceModel()->index(rowMap.at(i)->constFirst(), 0), silent)) {
0114             beginRemoveRows(QModelIndex(), i, i);
0115             delete rowMap.takeAt(i);
0116             endRemoveRows();
0117         }
0118     }
0119 }
0120 
0121 void NotificationGroupingModel::setSourceModel(QAbstractItemModel *sourceModel)
0122 {
0123     if (sourceModel == QAbstractProxyModel::sourceModel()) {
0124         return;
0125     }
0126 
0127     beginResetModel();
0128 
0129     if (QAbstractProxyModel::sourceModel()) {
0130         QAbstractProxyModel::sourceModel()->disconnect(this);
0131     }
0132 
0133     QAbstractProxyModel::setSourceModel(sourceModel);
0134 
0135     if (sourceModel) {
0136         rebuildMap();
0137 
0138         connect(sourceModel, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int start, int end) {
0139             if (parent.isValid()) {
0140                 return;
0141             }
0142 
0143             adjustMap(start, (end - start) + 1);
0144 
0145             for (int i = start; i <= end; ++i) {
0146                 if (!tryToGroup(this->sourceModel()->index(i, 0))) {
0147                     beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count());
0148                     rowMap.append(new QList<int>{i});
0149                     endInsertRows();
0150                 }
0151             }
0152 
0153             checkGrouping();
0154         });
0155 
0156         connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) {
0157             if (parent.isValid()) {
0158                 return;
0159             }
0160 
0161             for (int i = first; i <= last; ++i) {
0162                 for (int j = 0; j < rowMap.count(); ++j) {
0163                     const QList<int> *sourceRows = rowMap.at(j);
0164                     const int mapIndex = sourceRows->indexOf(i);
0165 
0166                     if (mapIndex != -1) {
0167                         // Remove top-level item.
0168                         if (sourceRows->count() == 1) {
0169                             beginRemoveRows(QModelIndex(), j, j);
0170                             delete rowMap.takeAt(j);
0171                             endRemoveRows();
0172                             // Dissolve group.
0173                         } else if (sourceRows->count() == 2) {
0174                             const QModelIndex parent = index(j, 0, QModelIndex());
0175                             beginRemoveRows(parent, 0, 1);
0176                             rowMap[j]->remove(mapIndex);
0177                             endRemoveRows();
0178 
0179                             // We're no longer a group parent.
0180                             Q_EMIT dataChanged(parent, parent);
0181                             // Remove group member.
0182                         } else {
0183                             const QModelIndex parent = index(j, 0, QModelIndex());
0184                             beginRemoveRows(parent, mapIndex, mapIndex);
0185                             rowMap[j]->remove(mapIndex);
0186                             endRemoveRows();
0187 
0188                             // Various roles of the parent evaluate child data, and the
0189                             // child list has changed.
0190                             Q_EMIT dataChanged(parent, parent);
0191 
0192                             // Signal children count change for all other items in the group.
0193                             Q_EMIT dataChanged(index(0, 0, parent), index(rowMap.count() - 1, 0, parent), {AbstractTimelineModel::NumInGroupRole});
0194                         }
0195 
0196                         break;
0197                     }
0198                 }
0199             }
0200         });
0201 
0202         connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &parent, int start, int end) {
0203             if (parent.isValid()) {
0204                 return;
0205             }
0206 
0207             adjustMap(start + 1, -((end - start) + 1));
0208 
0209             checkGrouping();
0210         });
0211 
0212         connect(sourceModel, &QAbstractItemModel::modelAboutToBeReset, this, &NotificationGroupingModel::beginResetModel);
0213         connect(sourceModel, &QAbstractItemModel::modelReset, this, [this] {
0214             rebuildMap();
0215             endResetModel();
0216         });
0217 
0218         connect(sourceModel,
0219                 &QAbstractItemModel::dataChanged,
0220                 this,
0221                 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
0222                     for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
0223                         const QModelIndex &sourceIndex = this->sourceModel()->index(i, 0);
0224                         QModelIndex proxyIndex = mapFromSource(sourceIndex);
0225 
0226                         if (!proxyIndex.isValid()) {
0227                             return;
0228                         }
0229 
0230                         const QModelIndex parent = proxyIndex.parent();
0231 
0232                         // If a child item changes, its parent may need an update as well as many of
0233                         // the data roles evaluate child data. See data().
0234                         // TODO: Some roles do not need to bubble up as they fall through to the first
0235                         // child in data(); it _might_ be worth adding constraints here later.
0236                         if (parent.isValid()) {
0237                             Q_EMIT dataChanged(parent, parent, roles);
0238                         }
0239 
0240                         Q_EMIT dataChanged(proxyIndex, proxyIndex, roles);
0241                     }
0242                 });
0243     }
0244 
0245     endResetModel();
0246 }
0247 
0248 int NotificationGroupingModel::rowCount(const QModelIndex &parent) const
0249 {
0250     if (!sourceModel()) {
0251         return 0;
0252     }
0253 
0254     if (parent.isValid() && parent.model() == this) {
0255         // Don't return row count for top-level item at child row: Group members
0256         // never have further children of their own.
0257         if (parent.parent().isValid()) {
0258             return 0;
0259         }
0260 
0261         if (parent.row() < 0 || parent.row() >= rowMap.count()) {
0262             return 0;
0263         }
0264 
0265         const int rowCount = rowMap.at(parent.row())->count();
0266         // If this sub-list in the map only has one entry, it's a plain item, not
0267         // parent to a group.
0268         if (rowCount == 1) {
0269             return 0;
0270         } else {
0271             return rowCount;
0272         }
0273     }
0274 
0275     return rowMap.count();
0276 }
0277 
0278 bool NotificationGroupingModel::hasChildren(const QModelIndex &parent) const
0279 {
0280     if ((parent.model() && parent.model() != this) || !sourceModel()) {
0281         return false;
0282     }
0283 
0284     return rowCount(parent);
0285 }
0286 
0287 int NotificationGroupingModel::columnCount(const QModelIndex &parent) const
0288 {
0289     Q_UNUSED(parent);
0290     return 1;
0291 }
0292 
0293 QVariant NotificationGroupingModel::data(const QModelIndex &proxyIndex, int role) const
0294 {
0295     if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
0296         return {};
0297     }
0298 
0299     const QModelIndex &parent = proxyIndex.parent();
0300     const bool isGroup = (!parent.isValid() && this->isGroup(proxyIndex.row()));
0301 
0302     // For group parent items, this will map to the last child task.
0303     const QModelIndex &sourceIndex = mapToSource(proxyIndex);
0304 
0305     if (!sourceIndex.isValid()) {
0306         return {};
0307     }
0308 
0309     if (isGroup) {
0310         switch (role) {
0311         case AbstractTimelineModel::IsGroupRole:
0312             return true;
0313         case AbstractTimelineModel::IsInGroupRole:
0314             return false;
0315         case AbstractTimelineModel::NumInGroupRole:
0316             return rowCount(proxyIndex);
0317         case AbstractTimelineModel::NotificationActorIdentityRole: {
0318             QVariantList authorList;
0319             for (int i = 0; i < qMin(rowCount(proxyIndex), 5); ++i) {
0320                 auto identity = index(i, 0, proxyIndex).data(AbstractTimelineModel::NotificationActorIdentityRole);
0321                 authorList.append(identity);
0322             }
0323             return authorList;
0324         }
0325         }
0326     } else {
0327         switch (role) {
0328         case AbstractTimelineModel::IsGroupRole:
0329             return false;
0330         case AbstractTimelineModel::IsInGroupRole:
0331             return parent.isValid();
0332         case AbstractTimelineModel::NumInGroupRole:
0333             return -1;
0334         }
0335     }
0336 
0337     return sourceIndex.data(role);
0338 }
0339 
0340 QModelIndex NotificationGroupingModel::index(int row, int column, const QModelIndex &parent) const
0341 {
0342     if (row < 0 || column != 0) {
0343         return {};
0344     }
0345 
0346     if (parent.isValid() && row < rowMap.at(parent.row())->count()) {
0347         return createIndex(row, column, rowMap.at(parent.row()));
0348     }
0349 
0350     if (row < rowMap.count()) {
0351         return createIndex(row, column, nullptr);
0352     }
0353 
0354     return {};
0355 }
0356 
0357 QModelIndex NotificationGroupingModel::parent(const QModelIndex &child) const
0358 {
0359     if (child.internalPointer() == nullptr) {
0360         return {};
0361     } else {
0362         const int parentRow = rowMap.indexOf(static_cast<QList<int> *>(child.internalPointer()));
0363 
0364         if (parentRow != -1) {
0365             return index(parentRow, 0, QModelIndex());
0366         }
0367 
0368         // If we were asked to find the parent for an internalPointer we can't
0369         // locate, we have corrupted data: This should not happen.
0370         Q_ASSERT(parentRow != -1);
0371     }
0372 
0373     return {};
0374 }
0375 
0376 QModelIndex NotificationGroupingModel::mapFromSource(const QModelIndex &sourceIndex) const
0377 {
0378     if (!sourceIndex.isValid() || sourceIndex.model() != sourceModel()) {
0379         return {};
0380     }
0381 
0382     for (int i = 0; i < rowMap.count(); ++i) {
0383         const QList<int> *sourceRows = rowMap.at(i);
0384         const int childIndex = sourceRows->indexOf(sourceIndex.row());
0385         const QModelIndex parent = index(i, 0, QModelIndex());
0386 
0387         if (childIndex == 0) {
0388             // If the sub-list we found the source row in is larger than 1 (i.e. part
0389             // of a group, map to the logical child item instead of the parent item
0390             // the source row also stands in for. The parent is therefore unreachable
0391             // from mapToSource().
0392             if (isGroup(i)) {
0393                 return index(0, 0, parent);
0394                 // Otherwise map to the top-level item.
0395             } else {
0396                 return parent;
0397             }
0398         } else if (childIndex != -1) {
0399             return index(childIndex, 0, parent);
0400         }
0401     }
0402 
0403     return {};
0404 }
0405 
0406 QModelIndex NotificationGroupingModel::mapToSource(const QModelIndex &proxyIndex) const
0407 {
0408     if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
0409         return {};
0410     }
0411 
0412     const QModelIndex &parent = proxyIndex.parent();
0413 
0414     if (parent.isValid()) {
0415         if (parent.row() < 0 || parent.row() >= rowMap.count()) {
0416             return {};
0417         }
0418 
0419         return sourceModel()->index(rowMap.at(parent.row())->at(proxyIndex.row()), 0);
0420     } else {
0421         // Group parents items therefore equate to the first child item; the source
0422         // row logically appears twice in the proxy.
0423         // mapFromSource() is not required to handle this well (consider proxies can
0424         // filter out rows, too) and opts to map to the child item, as the group parent
0425         // has its Qt::DisplayRole mangled by data(), and it's more useful for trans-
0426         // lating dataChanged() from the source model.
0427         // NOTE we changed that to be last
0428         if (rowMap.isEmpty()) { // FIXME
0429             // How can this happen? (happens when closing a group)
0430             return {};
0431         }
0432         return sourceModel()->index(rowMap.at(proxyIndex.row())->constLast(), 0);
0433     }
0434 }
0435 
0436 bool NotificationGroupingModel::loading() const
0437 {
0438     auto notificationModel = (NotificationModel *)sourceModel();
0439     return notificationModel->loading();
0440 }
0441 
0442 void NotificationGroupingModel::setSourceModel(NotificationModel *model)
0443 {
0444     setSourceModel(qobject_cast<QAbstractItemModel *>(model));
0445     connect(model, &NotificationModel::loadingChanged, this, &NotificationGroupingModel::loadingChanged);
0446     Q_EMIT sourceModelChanged();
0447 }
0448 
0449 NotificationModel *NotificationGroupingModel::getSourceModel()
0450 {
0451     return qobject_cast<NotificationModel *>(sourceModel());
0452 }
0453 
0454 #include "moc_notificationgroupingmodel.cpp"