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"