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"