File indexing completed on 2024-05-12 09:40:45
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 QList<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 QList<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 QList<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 QList<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 QList<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<QList<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 QList<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 }