File indexing completed on 2024-04-28 16:54:36

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