File indexing completed on 2024-05-05 05:38:35

0001 /*
0002     SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "taskgroupingproxymodel.h"
0008 #include "abstracttasksmodel.h"
0009 #include "tasktools.h"
0010 
0011 #include <QSet>
0012 #include <QTime>
0013 
0014 namespace TaskManager
0015 {
0016 class Q_DECL_HIDDEN TaskGroupingProxyModel::Private
0017 {
0018 public:
0019     Private(TaskGroupingProxyModel *q);
0020     ~Private();
0021 
0022     AbstractTasksModelIface *abstractTasksSourceModel = nullptr;
0023 
0024     TasksModel::GroupMode groupMode = TasksModel::GroupApplications;
0025     bool groupDemandingAttention = false;
0026     int windowTasksThreshold = -1;
0027 
0028     QList<QList<int> *> rowMap;
0029 
0030     QSet<QString> blacklistedAppIds;
0031     QSet<QString> blacklistedLauncherUrls;
0032 
0033     bool isGroup(int row);
0034     bool any(const QModelIndex &parent, int role);
0035     bool all(const QModelIndex &parent, int role);
0036 
0037     void sourceRowsAboutToBeInserted(const QModelIndex &parent, int first, int last);
0038     void sourceRowsInserted(const QModelIndex &parent, int start, int end);
0039     void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last);
0040     void sourceRowsRemoved(const QModelIndex &parent, int start, int end);
0041     void sourceModelAboutToBeReset();
0042     void sourceModelReset();
0043     void sourceDataChanged(QModelIndex topLeft, QModelIndex bottomRight, const QList<int> &roles = QList<int>());
0044     void adjustMap(int anchor, int delta);
0045 
0046     void rebuildMap();
0047     bool shouldGroupTasks();
0048     void checkGrouping(bool silent = false);
0049     bool isBlacklisted(const QModelIndex &sourceIndex);
0050     bool tryToGroup(const QModelIndex &sourceIndex, bool silent = false);
0051     void formGroupFor(const QModelIndex &index);
0052     void breakGroupFor(const QModelIndex &index, bool silent = false);
0053 
0054 private:
0055     TaskGroupingProxyModel *q;
0056 };
0057 
0058 TaskGroupingProxyModel::Private::Private(TaskGroupingProxyModel *q)
0059     : q(q)
0060 {
0061 }
0062 
0063 TaskGroupingProxyModel::Private::~Private()
0064 {
0065     qDeleteAll(rowMap);
0066 }
0067 
0068 bool TaskGroupingProxyModel::Private::isGroup(int row)
0069 {
0070     if (row < 0 || row >= rowMap.count()) {
0071         return false;
0072     }
0073 
0074     return (rowMap.at(row)->count() > 1);
0075 }
0076 
0077 bool TaskGroupingProxyModel::Private::any(const QModelIndex &parent, int role)
0078 {
0079     bool is = false;
0080 
0081     for (int i = 0; i < q->rowCount(parent); ++i) {
0082         if (q->index(i, 0, parent).data(role).toBool()) {
0083             return true;
0084         }
0085     }
0086 
0087     return is;
0088 }
0089 
0090 bool TaskGroupingProxyModel::Private::all(const QModelIndex &parent, int role)
0091 {
0092     bool is = true;
0093 
0094     for (int i = 0; i < q->rowCount(parent); ++i) {
0095         if (!q->index(i, 0, parent).data(role).toBool()) {
0096             return false;
0097         }
0098     }
0099 
0100     return is;
0101 }
0102 
0103 void TaskGroupingProxyModel::Private::sourceRowsAboutToBeInserted(const QModelIndex &parent, int first, int last)
0104 {
0105     Q_UNUSED(parent)
0106     Q_UNUSED(first)
0107     Q_UNUSED(last)
0108 }
0109 
0110 void TaskGroupingProxyModel::Private::sourceRowsInserted(const QModelIndex &parent, int start, int end)
0111 {
0112     // We only support flat source models.
0113     if (parent.isValid()) {
0114         return;
0115     }
0116 
0117     adjustMap(start, (end - start) + 1);
0118 
0119     bool shouldGroup = shouldGroupTasks(); // Can be slightly expensive; cache return value.
0120 
0121     for (int i = start; i <= end; ++i) {
0122         if (!shouldGroup || !tryToGroup(q->sourceModel()->index(i, 0))) {
0123             q->beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count());
0124             rowMap.append(new QList<int>{i});
0125             q->endInsertRows();
0126         }
0127     }
0128 
0129     checkGrouping();
0130 }
0131 
0132 void TaskGroupingProxyModel::Private::sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
0133 {
0134     // We only support flat source models.
0135     if (parent.isValid()) {
0136         return;
0137     }
0138 
0139     for (int i = first; i <= last; ++i) {
0140         for (int j = 0; j < rowMap.count(); ++j) {
0141             const QList<int> *sourceRows = rowMap.at(j);
0142             const int mapIndex = sourceRows->indexOf(i);
0143 
0144             if (mapIndex != -1) {
0145                 // Remove top-level item.
0146                 if (sourceRows->count() == 1) {
0147                     q->beginRemoveRows(QModelIndex(), j, j);
0148                     delete rowMap.takeAt(j);
0149                     q->endRemoveRows();
0150                     // Dissolve group.
0151                 } else if (sourceRows->count() == 2) {
0152                     const QModelIndex parent = q->index(j, 0);
0153                     q->beginRemoveRows(parent, 0, 1);
0154                     rowMap[j]->remove(mapIndex);
0155                     q->endRemoveRows();
0156 
0157                     // We're no longer a group parent.
0158                     Q_EMIT q->dataChanged(parent, parent);
0159                     // Remove group member.
0160                 } else {
0161                     const QModelIndex parent = q->index(j, 0);
0162                     q->beginRemoveRows(parent, mapIndex, mapIndex);
0163                     rowMap[j]->remove(mapIndex);
0164                     q->endRemoveRows();
0165 
0166                     // Various roles of the parent evaluate child data, and the
0167                     // child list has changed.
0168                     Q_EMIT q->dataChanged(parent, parent);
0169                 }
0170 
0171                 break;
0172             }
0173         }
0174     }
0175 }
0176 
0177 void TaskGroupingProxyModel::Private::sourceRowsRemoved(const QModelIndex &parent, int start, int end)
0178 {
0179     // We only support flat source models.
0180     if (parent.isValid()) {
0181         return;
0182     }
0183 
0184     adjustMap(start + 1, -((end - start) + 1));
0185 
0186     checkGrouping();
0187 }
0188 
0189 void TaskGroupingProxyModel::Private::sourceModelAboutToBeReset()
0190 {
0191     q->beginResetModel();
0192 }
0193 
0194 void TaskGroupingProxyModel::Private::sourceModelReset()
0195 {
0196     rebuildMap();
0197 
0198     q->endResetModel();
0199 }
0200 
0201 void TaskGroupingProxyModel::Private::sourceDataChanged(QModelIndex topLeft, QModelIndex bottomRight, const QList<int> &roles)
0202 {
0203     for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
0204         const QModelIndex &sourceIndex = q->sourceModel()->index(i, 0);
0205         QModelIndex proxyIndex = q->mapFromSource(sourceIndex);
0206 
0207         if (!proxyIndex.isValid()) {
0208             return;
0209         }
0210 
0211         const QModelIndex parent = proxyIndex.parent();
0212 
0213         // If a child item changes, its parent may need an update as well as many of
0214         // the data roles evaluate child data. See data().
0215         // TODO: Some roles do not need to bubble up as they fall through to the first
0216         // child in data(); it _might_ be worth adding constraints here later.
0217         if (parent.isValid()) {
0218             Q_EMIT q->dataChanged(parent, parent, roles);
0219         }
0220 
0221         // When Private::groupDemandingAttention is false, tryToGroup() exempts tasks
0222         // which demand attention from being grouped. Therefore if this task is no longer
0223         // demanding attention, we need to try grouping it now.
0224         if (!parent.isValid() && !groupDemandingAttention && roles.contains(AbstractTasksModel::IsDemandingAttention)
0225             && !sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) {
0226             if (shouldGroupTasks() && tryToGroup(sourceIndex)) {
0227                 q->beginRemoveRows(QModelIndex(), proxyIndex.row(), proxyIndex.row());
0228                 delete rowMap.takeAt(proxyIndex.row());
0229                 q->endRemoveRows();
0230             } else {
0231                 Q_EMIT q->dataChanged(proxyIndex, proxyIndex, roles);
0232             }
0233         } else {
0234             Q_EMIT q->dataChanged(proxyIndex, proxyIndex, roles);
0235         }
0236     }
0237 }
0238 
0239 void TaskGroupingProxyModel::Private::adjustMap(int anchor, int delta)
0240 {
0241     for (int i = 0; i < rowMap.count(); ++i) {
0242         QList<int> *sourceRows = rowMap.at(i);
0243         for (auto it = sourceRows->begin(); it != sourceRows->end(); ++it) {
0244             if ((*it) >= anchor) {
0245                 *it += delta;
0246             }
0247         }
0248     }
0249 }
0250 
0251 void TaskGroupingProxyModel::Private::rebuildMap()
0252 {
0253     qDeleteAll(rowMap);
0254     rowMap.clear();
0255 
0256     const int rows = q->sourceModel()->rowCount();
0257 
0258     rowMap.reserve(rows);
0259 
0260     for (int i = 0; i < rows; ++i) {
0261         rowMap.append(new QList<int>{i});
0262     }
0263 
0264     checkGrouping(true /* silent */);
0265 }
0266 
0267 bool TaskGroupingProxyModel::Private::shouldGroupTasks()
0268 {
0269     if (groupMode == TasksModel::GroupDisabled) {
0270         return false;
0271     }
0272 
0273     if (windowTasksThreshold != -1) {
0274         // We're going to check the number of window tasks in the source model
0275         // against the grouping threshold. In practice that means we're ignoring
0276         // launcher and startup tasks. Startup tasks because they're very short-
0277         // lived (i.e. forming/breaking groups as they come and go would be very
0278         // noisy) and launcher tasks because we expect consumers to budget for
0279         // them in the threshold they set.
0280         int windowTasksCount = 0;
0281 
0282         for (int i = 0; i < q->sourceModel()->rowCount(); ++i) {
0283             const QModelIndex &idx = q->sourceModel()->index(i, 0);
0284 
0285             if (idx.data(AbstractTasksModel::IsWindow).toBool()) {
0286                 ++windowTasksCount;
0287             }
0288         }
0289 
0290         return (windowTasksCount > windowTasksThreshold);
0291     }
0292 
0293     return true;
0294 }
0295 
0296 void TaskGroupingProxyModel::Private::checkGrouping(bool silent)
0297 {
0298     if (shouldGroupTasks()) {
0299         for (int i = (rowMap.count()) - 1; i >= 0; --i) {
0300             if (isGroup(i)) {
0301                 continue;
0302             }
0303 
0304             if (tryToGroup(q->sourceModel()->index(rowMap.at(i)->constFirst(), 0), silent)) {
0305                 q->beginRemoveRows(QModelIndex(), i, i);
0306                 delete rowMap.takeAt(i); // Safe since we're iterating backwards.
0307                 q->endRemoveRows();
0308             }
0309         }
0310     } else {
0311         for (int i = (rowMap.count()) - 1; i >= 0; --i) {
0312             breakGroupFor(q->index(i, 0), silent);
0313         }
0314     }
0315 }
0316 
0317 bool TaskGroupingProxyModel::Private::isBlacklisted(const QModelIndex &sourceIndex)
0318 {
0319     // Check app id against blacklist.
0320     if (blacklistedAppIds.count() && blacklistedAppIds.contains(sourceIndex.data(AbstractTasksModel::AppId).toString())) {
0321         return true;
0322     }
0323 
0324     // Check launcher URL (sans query items) against blacklist.
0325     if (blacklistedLauncherUrls.count()) {
0326         const QUrl &launcherUrl = sourceIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
0327         const QString &launcherUrlString = launcherUrl.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery);
0328 
0329         if (blacklistedLauncherUrls.contains(launcherUrlString)) {
0330             return true;
0331         }
0332     }
0333 
0334     return false;
0335 }
0336 
0337 bool TaskGroupingProxyModel::Private::tryToGroup(const QModelIndex &sourceIndex, bool silent)
0338 {
0339     // NOTE: We only group window tasks at this time. If this ever changes, the
0340     // implementation of data() will have to be adjusted significantly, as for
0341     // many roles it currently falls through to the first child item when dealing
0342     // with requests for the parent (e.g. IsWindow).
0343     if (!sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) {
0344         return false;
0345     }
0346 
0347     // If Private::groupDemandingAttention is false and this task is demanding
0348     // attention, don't group it at this time. We'll instead try to group it once
0349     // it no longer demands attention (see sourceDataChanged()).
0350     if (!groupDemandingAttention && sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) {
0351         return false;
0352     }
0353 
0354     // Blacklist checks.
0355     if (isBlacklisted(sourceIndex)) {
0356         return false;
0357     }
0358 
0359     // Meat of the matter: Try to add this source row to a sub-list with source rows
0360     // associated with the same application.
0361     for (int i = 0; i < rowMap.count(); ++i) {
0362         const QModelIndex &groupRep = q->sourceModel()->index(rowMap.at(i)->constFirst(), 0);
0363 
0364         // Don't match a row with itself.
0365         if (sourceIndex == groupRep) {
0366             continue;
0367         }
0368 
0369         // Don't group windows with anything other than windows.
0370         if (!groupRep.data(AbstractTasksModel::IsWindow).toBool()) {
0371             continue;
0372         }
0373 
0374         if (appsMatch(sourceIndex, groupRep)) {
0375             const QModelIndex parent = q->index(i, 0);
0376 
0377             if (!silent) {
0378                 const int newIndex = rowMap.at(i)->count();
0379 
0380                 if (newIndex == 1) {
0381                     q->beginInsertRows(parent, 0, 1);
0382                 } else {
0383                     q->beginInsertRows(parent, newIndex, newIndex);
0384                 }
0385             }
0386 
0387             rowMap[i]->append(sourceIndex.row());
0388 
0389             if (!silent) {
0390                 q->endInsertRows();
0391 
0392                 Q_EMIT q->dataChanged(parent, parent);
0393             }
0394 
0395             return true;
0396         }
0397     }
0398 
0399     return false;
0400 }
0401 
0402 void TaskGroupingProxyModel::Private::formGroupFor(const QModelIndex &index)
0403 {
0404     // Already in group or a group.
0405     if (index.parent().isValid() || isGroup(index.row())) {
0406         return;
0407     }
0408 
0409     // We need to grab a source index as we may invalidate the index passed
0410     // in through grouping.
0411     const QModelIndex &sourceTarget = q->mapToSource(index);
0412 
0413     for (int i = (rowMap.count() - 1); i >= 0; --i) {
0414         const QModelIndex &sourceIndex = q->sourceModel()->index(rowMap.at(i)->constFirst(), 0);
0415 
0416         if (!appsMatch(sourceTarget, sourceIndex)) {
0417             continue;
0418         }
0419 
0420         if (tryToGroup(sourceIndex)) {
0421             q->beginRemoveRows(QModelIndex(), i, i);
0422             delete rowMap.takeAt(i); // Safe since we're iterating backwards.
0423             q->endRemoveRows();
0424         }
0425     }
0426 }
0427 
0428 void TaskGroupingProxyModel::Private::breakGroupFor(const QModelIndex &index, bool silent)
0429 {
0430     const int row = index.row();
0431 
0432     if (!isGroup(row)) {
0433         return;
0434     }
0435 
0436     // The first child will move up to the top level.
0437     QList<int> extraChildren = rowMap.at(row)->mid(1);
0438 
0439     // NOTE: We're going to do remove+insert transactions instead of a
0440     // single reparenting move transaction to save on complexity in the
0441     // proxies above us.
0442     // TODO: This could technically be optimized, though it's very
0443     // unlikely to be ever worth it.
0444     if (!silent) {
0445         q->beginRemoveRows(index, 0, extraChildren.count());
0446     }
0447 
0448     rowMap[row]->resize(1);
0449 
0450     if (!silent) {
0451         q->endRemoveRows();
0452 
0453         // We're no longer a group parent.
0454         Q_EMIT q->dataChanged(index, index);
0455 
0456         q->beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count() + (extraChildren.count() - 1));
0457     }
0458 
0459     for (int i = 0; i < extraChildren.count(); ++i) {
0460         rowMap.append(new QList<int>{extraChildren.at(i)});
0461     }
0462 
0463     if (!silent) {
0464         q->endInsertRows();
0465     }
0466 }
0467 
0468 TaskGroupingProxyModel::TaskGroupingProxyModel(QObject *parent)
0469     : QAbstractProxyModel(parent)
0470     , d(new Private(this))
0471 {
0472 }
0473 
0474 TaskGroupingProxyModel::~TaskGroupingProxyModel()
0475 {
0476 }
0477 
0478 QModelIndex TaskGroupingProxyModel::index(int row, int column, const QModelIndex &parent) const
0479 {
0480     if (row < 0 || column != 0) {
0481         return QModelIndex();
0482     }
0483 
0484     if (parent.isValid() && row < d->rowMap.at(parent.row())->count()) {
0485         return createIndex(row, column, d->rowMap.at(parent.row()));
0486     }
0487 
0488     if (row < d->rowMap.count()) {
0489         return createIndex(row, column, nullptr);
0490     }
0491 
0492     return QModelIndex();
0493 }
0494 
0495 QModelIndex TaskGroupingProxyModel::parent(const QModelIndex &child) const
0496 {
0497     if (child.internalPointer() == nullptr) {
0498         return QModelIndex();
0499     } else {
0500         const int parentRow = d->rowMap.indexOf(static_cast<QList<int> *>(child.internalPointer()));
0501 
0502         if (parentRow != -1) {
0503             return index(parentRow, 0);
0504         }
0505 
0506         // If we were asked to find the parent for an internalPointer we can't
0507         // locate, we have corrupted data: This should not happen.
0508         Q_ASSERT(parentRow != -1);
0509     }
0510 
0511     return QModelIndex();
0512 }
0513 
0514 QModelIndex TaskGroupingProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
0515 {
0516     if (!sourceIndex.isValid() || sourceIndex.model() != sourceModel()) {
0517         return QModelIndex();
0518     }
0519 
0520     for (int i = 0; i < d->rowMap.count(); ++i) {
0521         const QList<int> *sourceRows = d->rowMap.at(i);
0522         const int childIndex = sourceRows->indexOf(sourceIndex.row());
0523         const QModelIndex parent = index(i, 0);
0524 
0525         if (childIndex == 0) {
0526             // If the sub-list we found the source row in is larger than 1 (i.e. part
0527             // of a group, map to the logical child item instead of the parent item
0528             // the source row also stands in for. The parent is therefore unreachable
0529             // from mapToSource().
0530             if (d->isGroup(i)) {
0531                 return index(0, 0, parent);
0532                 // Otherwise map to the top-level item.
0533             } else {
0534                 return parent;
0535             }
0536         } else if (childIndex != -1) {
0537             return index(childIndex, 0, parent);
0538         }
0539     }
0540 
0541     return QModelIndex();
0542 }
0543 
0544 QModelIndex TaskGroupingProxyModel::mapToSource(const QModelIndex &proxyIndex) const
0545 {
0546     if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
0547         return QModelIndex();
0548     }
0549 
0550     const QModelIndex &parent = proxyIndex.parent();
0551 
0552     if (parent.isValid()) {
0553         if (parent.row() < 0 || parent.row() >= d->rowMap.count()) {
0554             return QModelIndex();
0555         }
0556 
0557         return sourceModel()->index(d->rowMap.at(parent.row())->at(proxyIndex.row()), 0);
0558     } else {
0559         // Group parents items therefore equate to the first child item; the source
0560         // row logically appears twice in the proxy.
0561         // mapFromSource() is not required to handle this well (consider proxies can
0562         // filter out rows, too) and opts to map to the child item, as the group parent
0563         // has its Qt::DisplayRole mangled by data(), and it's more useful for trans-
0564         // lating dataChanged() from the source model.
0565         return sourceModel()->index(d->rowMap.at(proxyIndex.row())->at(0), 0);
0566     }
0567 
0568     return QModelIndex();
0569 }
0570 
0571 int TaskGroupingProxyModel::rowCount(const QModelIndex &parent) const
0572 {
0573     if (!sourceModel()) {
0574         return 0;
0575     }
0576 
0577     if (parent.isValid() && parent.model() == this) {
0578         // Don't return row count for top-level item at child row: Group members
0579         // never have further children of their own.
0580         if (parent.parent().isValid()) {
0581             return 0;
0582         }
0583 
0584         if (parent.row() < 0 || parent.row() >= d->rowMap.count()) {
0585             return 0;
0586         }
0587 
0588         const uint rowCount = d->rowMap.at(parent.row())->count();
0589 
0590         // If this sub-list in the map only has one entry, it's a plain item, not
0591         // parent to a group.
0592         if (rowCount == 1) {
0593             return 0;
0594         } else {
0595             return rowCount;
0596         }
0597     }
0598 
0599     return d->rowMap.count();
0600 }
0601 
0602 bool TaskGroupingProxyModel::hasChildren(const QModelIndex &parent) const
0603 {
0604     if ((parent.model() && parent.model() != this) || !sourceModel()) {
0605         return false;
0606     }
0607 
0608     return rowCount(parent);
0609 }
0610 
0611 int TaskGroupingProxyModel::columnCount(const QModelIndex &parent) const
0612 {
0613     Q_UNUSED(parent)
0614 
0615     return 1;
0616 }
0617 
0618 QVariant TaskGroupingProxyModel::data(const QModelIndex &proxyIndex, int role) const
0619 {
0620     if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
0621         return QVariant();
0622     }
0623 
0624     const QModelIndex &parent = proxyIndex.parent();
0625     const bool isWindowGroup = (!parent.isValid() && d->isGroup(proxyIndex.row()));
0626 
0627     // For group parent items, this will map to the first child task.
0628     const QModelIndex &sourceIndex = mapToSource(proxyIndex);
0629 
0630     if (!sourceIndex.isValid()) {
0631         return QVariant();
0632     }
0633 
0634     if (role == AbstractTasksModel::IsGroupable) {
0635         return !d->isBlacklisted(sourceIndex);
0636     }
0637 
0638     if (isWindowGroup) {
0639         // For group parent items, DisplayRole is mapped to AppName of the first child.
0640         if (role == Qt::DisplayRole) {
0641             const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString();
0642 
0643             // Groups are formed by app id or launcher URL; neither requires
0644             // AppName to be available. If it's not, fall back to the app id
0645             /// rather than an empty string.
0646             if (appName.isEmpty()) {
0647                 return sourceIndex.data(AbstractTasksModel::AppId);
0648             }
0649 
0650             return appName;
0651         } else if (role == AbstractTasksModel::WinIdList) {
0652             QVariantList winIds;
0653 
0654             for (int i = 0; i < rowCount(proxyIndex); ++i) {
0655                 winIds.append(index(i, 0, proxyIndex).data(AbstractTasksModel::WinIdList).toList());
0656             }
0657 
0658             return winIds;
0659         } else if (role == AbstractTasksModel::MimeType) {
0660             return QStringLiteral("windowsystem/multiple-winids");
0661         } else if (role == AbstractTasksModel::MimeData) {
0662             // FIXME TODO: Implement.
0663             return QVariant();
0664         } else if (role == AbstractTasksModel::IsGroupParent) {
0665             return true;
0666         } else if (role == AbstractTasksModel::ChildCount) {
0667             return rowCount(proxyIndex);
0668         } else if (role == AbstractTasksModel::IsActive) {
0669             return d->any(proxyIndex, AbstractTasksModel::IsActive);
0670         } else if (role == AbstractTasksModel::IsClosable) {
0671             return d->all(proxyIndex, AbstractTasksModel::IsClosable);
0672         } else if (role == AbstractTasksModel::IsMovable) {
0673             // Moving groups makes no sense.
0674             return false;
0675         } else if (role == AbstractTasksModel::IsResizable) {
0676             // Resizing groups makes no sense.
0677             return false;
0678         } else if (role == AbstractTasksModel::IsMaximizable) {
0679             return d->all(proxyIndex, AbstractTasksModel::IsMaximizable);
0680         } else if (role == AbstractTasksModel::IsMaximized) {
0681             return d->all(proxyIndex, AbstractTasksModel::IsMaximized);
0682         } else if (role == AbstractTasksModel::IsMinimizable) {
0683             return d->all(proxyIndex, AbstractTasksModel::IsMinimizable);
0684         } else if (role == AbstractTasksModel::IsMinimized) {
0685             return d->all(proxyIndex, AbstractTasksModel::IsMinimized);
0686         } else if (role == AbstractTasksModel::IsKeepAbove) {
0687             return d->all(proxyIndex, AbstractTasksModel::IsKeepAbove);
0688         } else if (role == AbstractTasksModel::IsKeepBelow) {
0689             return d->all(proxyIndex, AbstractTasksModel::IsKeepBelow);
0690         } else if (role == AbstractTasksModel::IsFullScreenable) {
0691             return d->all(proxyIndex, AbstractTasksModel::IsFullScreenable);
0692         } else if (role == AbstractTasksModel::IsFullScreen) {
0693             return d->all(proxyIndex, AbstractTasksModel::IsFullScreen);
0694         } else if (role == AbstractTasksModel::IsShadeable) {
0695             return d->all(proxyIndex, AbstractTasksModel::IsShadeable);
0696         } else if (role == AbstractTasksModel::IsShaded) {
0697             return d->all(proxyIndex, AbstractTasksModel::IsShaded);
0698         } else if (role == AbstractTasksModel::IsVirtualDesktopsChangeable) {
0699             return d->all(proxyIndex, AbstractTasksModel::IsVirtualDesktopsChangeable);
0700         } else if (role == AbstractTasksModel::VirtualDesktops) {
0701             QStringList desktops;
0702 
0703             for (int i = 0; i < rowCount(proxyIndex); ++i) {
0704                 desktops.append(index(i, 0, proxyIndex).data(AbstractTasksModel::VirtualDesktops).toStringList());
0705             }
0706 
0707             desktops.removeDuplicates();
0708             return desktops;
0709         } else if (role == AbstractTasksModel::ScreenGeometry) {
0710             // TODO: Nothing needs this for now and it would add complexity to
0711             // make it a list; skip it until needed. Once it is, do it similarly
0712             // to the AbstractTasksModel::VirtualDesktop case.
0713             return QVariant();
0714         } else if (role == AbstractTasksModel::Activities) {
0715             QStringList activities;
0716 
0717             for (int i = 0; i < rowCount(proxyIndex); ++i) {
0718                 activities.append(index(i, 0, proxyIndex).data(AbstractTasksModel::Activities).toStringList());
0719             }
0720 
0721             activities.removeDuplicates();
0722             return activities;
0723         } else if (role == AbstractTasksModel::IsDemandingAttention) {
0724             return d->any(proxyIndex, AbstractTasksModel::IsDemandingAttention);
0725         } else if (role == AbstractTasksModel::SkipTaskbar) {
0726             return d->all(proxyIndex, AbstractTasksModel::SkipTaskbar);
0727         } else if (role == AbstractTasksModel::LastActivated) {
0728             // Find the last activated task in the single group
0729             const int groupSize = d->rowMap.at(proxyIndex.row())->size();
0730             QTime lastActivated = mapToSource(index(0, 0, proxyIndex)).data(AbstractTasksModel::LastActivated).toTime();
0731 
0732             for (int i = 1; i < groupSize; i++) {
0733                 const QTime activated = mapToSource(index(i, 0, proxyIndex)).data(AbstractTasksModel::LastActivated).toTime();
0734 
0735                 if (lastActivated < activated) {
0736                     lastActivated = activated;
0737                 }
0738             }
0739 
0740             return lastActivated;
0741         }
0742     }
0743 
0744     return sourceIndex.data(role);
0745 }
0746 
0747 void TaskGroupingProxyModel::setSourceModel(QAbstractItemModel *sourceModel)
0748 {
0749     if (sourceModel == QAbstractProxyModel::sourceModel()) {
0750         return;
0751     }
0752 
0753     beginResetModel();
0754 
0755     if (QAbstractProxyModel::sourceModel()) {
0756         QAbstractProxyModel::sourceModel()->disconnect(this);
0757     }
0758 
0759     QAbstractProxyModel::setSourceModel(sourceModel);
0760     d->abstractTasksSourceModel = dynamic_cast<AbstractTasksModelIface *>(sourceModel);
0761 
0762     if (sourceModel) {
0763         d->rebuildMap();
0764 
0765         using namespace std::placeholders;
0766         auto dd = d.get();
0767         connect(sourceModel,
0768                 &QSortFilterProxyModel::rowsAboutToBeInserted,
0769                 this,
0770                 std::bind(&TaskGroupingProxyModel::Private::sourceRowsAboutToBeInserted, dd, _1, _2, _3));
0771         connect(sourceModel, &QSortFilterProxyModel::rowsInserted, this, std::bind(&TaskGroupingProxyModel::Private::sourceRowsInserted, dd, _1, _2, _3));
0772         connect(sourceModel,
0773                 &QSortFilterProxyModel::rowsAboutToBeRemoved,
0774                 this,
0775                 std::bind(&TaskGroupingProxyModel::Private::sourceRowsAboutToBeRemoved, dd, _1, _2, _3));
0776         connect(sourceModel, &QSortFilterProxyModel::rowsRemoved, this, std::bind(&TaskGroupingProxyModel::Private::sourceRowsRemoved, dd, _1, _2, _3));
0777         connect(sourceModel, &QSortFilterProxyModel::modelAboutToBeReset, this, std::bind(&TaskGroupingProxyModel::Private::sourceModelAboutToBeReset, dd));
0778         connect(sourceModel, &QSortFilterProxyModel::modelReset, this, std::bind(&TaskGroupingProxyModel::Private::sourceModelReset, dd));
0779         connect(sourceModel, &QSortFilterProxyModel::dataChanged, this, std::bind(&TaskGroupingProxyModel::Private::sourceDataChanged, dd, _1, _2, _3));
0780     } else {
0781         qDeleteAll(d->rowMap);
0782         d->rowMap.clear();
0783     }
0784 
0785     endResetModel();
0786 }
0787 
0788 TasksModel::GroupMode TaskGroupingProxyModel::groupMode() const
0789 {
0790     return d->groupMode;
0791 }
0792 
0793 void TaskGroupingProxyModel::setGroupMode(TasksModel::GroupMode mode)
0794 {
0795     if (d->groupMode != mode) {
0796         d->groupMode = mode;
0797 
0798         d->checkGrouping();
0799 
0800         Q_EMIT groupModeChanged();
0801     }
0802 }
0803 
0804 bool TaskGroupingProxyModel::groupDemandingAttention() const
0805 {
0806     return d->groupDemandingAttention;
0807 }
0808 
0809 void TaskGroupingProxyModel::setGroupDemandingAttention(bool group)
0810 {
0811     if (d->groupDemandingAttention != group) {
0812         d->groupDemandingAttention = group;
0813 
0814         d->checkGrouping();
0815 
0816         Q_EMIT groupDemandingAttentionChanged();
0817     }
0818 }
0819 
0820 int TaskGroupingProxyModel::windowTasksThreshold() const
0821 {
0822     return d->windowTasksThreshold;
0823 }
0824 
0825 void TaskGroupingProxyModel::setWindowTasksThreshold(int threshold)
0826 {
0827     if (d->windowTasksThreshold != threshold) {
0828         d->windowTasksThreshold = threshold;
0829 
0830         d->checkGrouping();
0831 
0832         Q_EMIT windowTasksThresholdChanged();
0833     }
0834 }
0835 
0836 QStringList TaskGroupingProxyModel::blacklistedAppIds() const
0837 {
0838     return d->blacklistedAppIds.values();
0839 }
0840 
0841 void TaskGroupingProxyModel::setBlacklistedAppIds(const QStringList &list)
0842 {
0843     const QSet<QString> &set = QSet<QString>(list.cbegin(), list.cend());
0844 
0845     if (d->blacklistedAppIds != set) {
0846         d->blacklistedAppIds = set;
0847 
0848         // checkGrouping() will gather and group up what's newly-allowed under the changed
0849         // blacklist.
0850         d->checkGrouping();
0851 
0852         // Now break apart what we need to.
0853         for (int i = (d->rowMap.count() - 1); i >= 0; --i) {
0854             if (d->isGroup(i)) {
0855                 const QModelIndex &groupRep = index(i, 0);
0856 
0857                 if (set.contains(groupRep.data(AbstractTasksModel::AppId).toString())) {
0858                     d->breakGroupFor(groupRep); // Safe since we're iterating backwards.
0859                 }
0860             }
0861         }
0862 
0863         Q_EMIT blacklistedAppIdsChanged();
0864     }
0865 }
0866 
0867 QStringList TaskGroupingProxyModel::blacklistedLauncherUrls() const
0868 {
0869     return d->blacklistedLauncherUrls.values();
0870 }
0871 
0872 void TaskGroupingProxyModel::setBlacklistedLauncherUrls(const QStringList &list)
0873 {
0874     const QSet<QString> &set = QSet<QString>(list.cbegin(), list.cend());
0875 
0876     if (d->blacklistedLauncherUrls != set) {
0877         d->blacklistedLauncherUrls = set;
0878 
0879         // checkGrouping() will gather and group up what's newly-allowed under the changed
0880         // blacklist.
0881         d->checkGrouping();
0882 
0883         // Now break apart what we need to.
0884         for (int i = (d->rowMap.count() - 1); i >= 0; --i) {
0885             if (d->isGroup(i)) {
0886                 const QModelIndex &groupRep = index(i, 0);
0887                 const QUrl &launcherUrl = groupRep.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
0888                 const QString &launcherUrlString = launcherUrl.toString(QUrl::RemoveQuery);
0889 
0890                 if (set.contains(launcherUrlString)) {
0891                     d->breakGroupFor(groupRep); // Safe since we're iterating backwards.
0892                 }
0893             }
0894         }
0895 
0896         Q_EMIT blacklistedLauncherUrlsChanged();
0897     }
0898 }
0899 
0900 void TaskGroupingProxyModel::requestActivate(const QModelIndex &index)
0901 {
0902     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
0903         return;
0904     }
0905 
0906     if (index.parent().isValid() || !d->isGroup(index.row())) {
0907         d->abstractTasksSourceModel->requestActivate(mapToSource(index));
0908     }
0909 }
0910 
0911 void TaskGroupingProxyModel::requestNewInstance(const QModelIndex &index)
0912 {
0913     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
0914         return;
0915     }
0916 
0917     d->abstractTasksSourceModel->requestNewInstance(mapToSource(index));
0918 }
0919 
0920 void TaskGroupingProxyModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls)
0921 {
0922     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
0923         return;
0924     }
0925 
0926     d->abstractTasksSourceModel->requestOpenUrls(mapToSource(index), urls);
0927 }
0928 
0929 void TaskGroupingProxyModel::requestClose(const QModelIndex &index)
0930 {
0931     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
0932         return;
0933     }
0934 
0935     if (index.parent().isValid() || !d->isGroup(index.row())) {
0936         d->abstractTasksSourceModel->requestClose(mapToSource(index));
0937     } else {
0938         const int row = index.row();
0939 
0940         for (int i = (rowCount(index) - 1); i >= 1; --i) {
0941             const QModelIndex &sourceChild = mapToSource(this->index(i, 0, index));
0942             d->abstractTasksSourceModel->requestClose(sourceChild);
0943         }
0944 
0945         d->abstractTasksSourceModel->requestClose(mapToSource(TaskGroupingProxyModel::index(row, 0)));
0946     }
0947 }
0948 
0949 void TaskGroupingProxyModel::requestMove(const QModelIndex &index)
0950 {
0951     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
0952         return;
0953     }
0954 
0955     if (index.parent().isValid() || !d->isGroup(index.row())) {
0956         d->abstractTasksSourceModel->requestMove(mapToSource(index));
0957     }
0958 }
0959 
0960 void TaskGroupingProxyModel::requestResize(const QModelIndex &index)
0961 {
0962     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
0963         return;
0964     }
0965 
0966     if (index.parent().isValid() || !d->isGroup(index.row())) {
0967         d->abstractTasksSourceModel->requestResize(mapToSource(index));
0968     }
0969 }
0970 
0971 void TaskGroupingProxyModel::requestToggleMinimized(const QModelIndex &index)
0972 {
0973     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
0974         return;
0975     }
0976 
0977     if (index.parent().isValid() || !d->isGroup(index.row())) {
0978         d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(index));
0979     } else {
0980         const bool goalState = !index.data(AbstractTasksModel::IsHidden).toBool();
0981 
0982         for (int i = 0; i < rowCount(index); ++i) {
0983             const QModelIndex &child = this->index(i, 0, index);
0984 
0985             if (child.data(AbstractTasksModel::IsHidden).toBool() != goalState) {
0986                 d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(child));
0987             }
0988         }
0989     }
0990 }
0991 
0992 void TaskGroupingProxyModel::requestToggleMaximized(const QModelIndex &index)
0993 {
0994     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
0995         return;
0996     }
0997 
0998     if (index.parent().isValid() || !d->isGroup(index.row())) {
0999         d->abstractTasksSourceModel->requestToggleMaximized(mapToSource(index));
1000     } else {
1001         const bool goalState = !index.data(AbstractTasksModel::IsMaximized).toBool();
1002 
1003         QModelIndexList inStackingOrder;
1004 
1005         for (int i = 0; i < rowCount(index); ++i) {
1006             const QModelIndex &child = this->index(i, 0, index);
1007 
1008             if (child.data(AbstractTasksModel::IsMaximized).toBool() != goalState) {
1009                 inStackingOrder << mapToSource(child);
1010             }
1011         }
1012 
1013         std::sort(inStackingOrder.begin(), inStackingOrder.end(), [](const QModelIndex &a, const QModelIndex &b) {
1014             return (a.data(AbstractTasksModel::StackingOrder).toInt() < b.data(AbstractTasksModel::StackingOrder).toInt());
1015         });
1016 
1017         for (const QModelIndex &sourceChild : std::as_const(inStackingOrder)) {
1018             d->abstractTasksSourceModel->requestToggleMaximized(sourceChild);
1019         }
1020     }
1021 }
1022 
1023 void TaskGroupingProxyModel::requestToggleKeepAbove(const QModelIndex &index)
1024 {
1025     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
1026         return;
1027     }
1028 
1029     if (index.parent().isValid() || !d->isGroup(index.row())) {
1030         d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(index));
1031     } else {
1032         const bool goalState = !index.data(AbstractTasksModel::IsKeepAbove).toBool();
1033 
1034         for (int i = 0; i < rowCount(index); ++i) {
1035             const QModelIndex &child = this->index(i, 0, index);
1036 
1037             if (child.data(AbstractTasksModel::IsKeepAbove).toBool() != goalState) {
1038                 d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(child));
1039             }
1040         }
1041     }
1042 }
1043 
1044 void TaskGroupingProxyModel::requestToggleKeepBelow(const QModelIndex &index)
1045 {
1046     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
1047         return;
1048     }
1049 
1050     if (index.parent().isValid() || !d->isGroup(index.row())) {
1051         d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(index));
1052     } else {
1053         const bool goalState = !index.data(AbstractTasksModel::IsKeepBelow).toBool();
1054 
1055         for (int i = 0; i < rowCount(index); ++i) {
1056             const QModelIndex &child = this->index(i, 0, index);
1057 
1058             if (child.data(AbstractTasksModel::IsKeepBelow).toBool() != goalState) {
1059                 d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(child));
1060             }
1061         }
1062     }
1063 }
1064 
1065 void TaskGroupingProxyModel::requestToggleFullScreen(const QModelIndex &index)
1066 {
1067     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
1068         return;
1069     }
1070 
1071     if (index.parent().isValid() || !d->isGroup(index.row())) {
1072         d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(index));
1073     } else {
1074         const bool goalState = !index.data(AbstractTasksModel::IsFullScreen).toBool();
1075 
1076         for (int i = 0; i < rowCount(index); ++i) {
1077             const QModelIndex &child = this->index(i, 0, index);
1078 
1079             if (child.data(AbstractTasksModel::IsFullScreen).toBool() != goalState) {
1080                 d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(child));
1081             }
1082         }
1083     }
1084 }
1085 
1086 void TaskGroupingProxyModel::requestToggleShaded(const QModelIndex &index)
1087 {
1088     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
1089         return;
1090     }
1091 
1092     if (index.parent().isValid() || !d->isGroup(index.row())) {
1093         d->abstractTasksSourceModel->requestToggleShaded(mapToSource(index));
1094     } else {
1095         const bool goalState = !index.data(AbstractTasksModel::IsShaded).toBool();
1096 
1097         for (int i = 0; i < rowCount(index); ++i) {
1098             const QModelIndex &child = this->index(i, 0, index);
1099 
1100             if (child.data(AbstractTasksModel::IsShaded).toBool() != goalState) {
1101                 d->abstractTasksSourceModel->requestToggleShaded(mapToSource(child));
1102             }
1103         }
1104     }
1105 }
1106 
1107 void TaskGroupingProxyModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops)
1108 {
1109     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
1110         return;
1111     }
1112 
1113     if (index.parent().isValid() || !d->isGroup(index.row())) {
1114         d->abstractTasksSourceModel->requestVirtualDesktops(mapToSource(index), desktops);
1115     } else {
1116         QList<QModelIndex> groupChildren;
1117 
1118         const int childCount = rowCount(index);
1119 
1120         groupChildren.reserve(childCount);
1121 
1122         for (int i = (childCount - 1); i >= 0; --i) {
1123             groupChildren.append(mapToSource(this->index(i, 0, index)));
1124         }
1125 
1126         for (const QModelIndex &idx : groupChildren) {
1127             d->abstractTasksSourceModel->requestVirtualDesktops(idx, desktops);
1128         }
1129     }
1130 }
1131 
1132 void TaskGroupingProxyModel::requestNewVirtualDesktop(const QModelIndex &index)
1133 {
1134     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
1135         return;
1136     }
1137 
1138     if (index.parent().isValid() || !d->isGroup(index.row())) {
1139         d->abstractTasksSourceModel->requestNewVirtualDesktop(mapToSource(index));
1140     } else {
1141         QList<QModelIndex> groupChildren;
1142 
1143         const int childCount = rowCount(index);
1144 
1145         groupChildren.reserve(childCount);
1146 
1147         for (int i = (childCount - 1); i >= 0; --i) {
1148             groupChildren.append(mapToSource(this->index(i, 0, index)));
1149         }
1150 
1151         for (const QModelIndex &idx : groupChildren) {
1152             d->abstractTasksSourceModel->requestNewVirtualDesktop(idx);
1153         }
1154     }
1155 }
1156 
1157 void TaskGroupingProxyModel::requestActivities(const QModelIndex &index, const QStringList &activities)
1158 {
1159     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
1160         return;
1161     }
1162 
1163     if (index.parent().isValid() || !d->isGroup(index.row())) {
1164         d->abstractTasksSourceModel->requestActivities(mapToSource(index), activities);
1165     } else {
1166         QList<QModelIndex> groupChildren;
1167 
1168         const int childCount = rowCount(index);
1169 
1170         groupChildren.reserve(childCount);
1171 
1172         for (int i = (childCount - 1); i >= 0; --i) {
1173             groupChildren.append(mapToSource(this->index(i, 0, index)));
1174         }
1175 
1176         for (const QModelIndex &idx : groupChildren) {
1177             d->abstractTasksSourceModel->requestActivities(idx, activities);
1178         }
1179     }
1180 }
1181 
1182 void TaskGroupingProxyModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate)
1183 {
1184     if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
1185         return;
1186     }
1187 
1188     if (index.parent().isValid() || !d->isGroup(index.row())) {
1189         d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate);
1190     } else {
1191         for (int i = 0; i < rowCount(index); ++i) {
1192             d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(this->index(i, 0, index)), geometry, delegate);
1193         }
1194     }
1195 }
1196 
1197 void TaskGroupingProxyModel::requestToggleGrouping(const QModelIndex &index)
1198 {
1199     const QString &appId = index.data(AbstractTasksModel::AppId).toString();
1200     const QUrl &launcherUrl = index.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
1201     const QString &launcherUrlString = launcherUrl.toString(QUrl::RemoveQuery);
1202 
1203     if (d->blacklistedAppIds.contains(appId) || d->blacklistedLauncherUrls.contains(launcherUrlString)) {
1204         d->blacklistedAppIds.remove(appId);
1205         d->blacklistedLauncherUrls.remove(launcherUrlString);
1206 
1207         if (d->groupMode != TasksModel::GroupDisabled) {
1208             d->formGroupFor(index.parent().isValid() ? index.parent() : index);
1209         }
1210     } else {
1211         d->blacklistedAppIds.insert(appId);
1212         d->blacklistedLauncherUrls.insert(launcherUrlString);
1213 
1214         if (d->groupMode != TasksModel::GroupDisabled) {
1215             d->breakGroupFor(index.parent().isValid() ? index.parent() : index);
1216         }
1217     }
1218 
1219     // Update IsGroupable data role for all relevant top-level items. We don't need to update
1220     // for group members since they've just been inserted -- it's logically impossible to
1221     // toggle grouping _on_ from a group member.
1222     for (int i = 0; i < d->rowMap.count(); ++i) {
1223         if (!d->isGroup(i)) {
1224             const QModelIndex &idx = TaskGroupingProxyModel::index(i, 0);
1225 
1226             if (idx.data(AbstractTasksModel::AppId).toString() == appId
1227                 || launcherUrlsMatch(idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(), launcherUrl, IgnoreQueryItems)) {
1228                 Q_EMIT dataChanged(idx, idx, QList<int>{AbstractTasksModel::IsGroupable});
1229             }
1230         }
1231     }
1232 
1233     Q_EMIT blacklistedAppIdsChanged();
1234     Q_EMIT blacklistedLauncherUrlsChanged();
1235 }
1236 
1237 }
1238 
1239 #include "moc_taskgroupingproxymodel.cpp"