File indexing completed on 2024-05-05 17:44:53
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 "tasksmodel.h" 0008 #include "activityinfo.h" 0009 #include "concatenatetasksproxymodel.h" 0010 #include "flattentaskgroupsproxymodel.h" 0011 #include "taskfilterproxymodel.h" 0012 #include "taskgroupingproxymodel.h" 0013 #include "tasktools.h" 0014 #include "virtualdesktopinfo.h" 0015 0016 #include "launchertasksmodel.h" 0017 #include "startuptasksmodel.h" 0018 #include "windowtasksmodel.h" 0019 0020 #include "launchertasksmodel_p.h" 0021 0022 #include <QGuiApplication> 0023 #include <QTime> 0024 #include <QTimer> 0025 #include <QUrl> 0026 #include <QVector> 0027 0028 #include <numeric> 0029 0030 namespace TaskManager 0031 { 0032 class Q_DECL_HIDDEN TasksModel::Private 0033 { 0034 public: 0035 Private(TasksModel *q); 0036 ~Private(); 0037 0038 static int instanceCount; 0039 0040 static WindowTasksModel *windowTasksModel; 0041 static StartupTasksModel *startupTasksModel; 0042 LauncherTasksModel *launcherTasksModel = nullptr; 0043 ConcatenateTasksProxyModel *concatProxyModel = nullptr; 0044 TaskFilterProxyModel *filterProxyModel = nullptr; 0045 TaskGroupingProxyModel *groupingProxyModel = nullptr; 0046 FlattenTaskGroupsProxyModel *flattenGroupsProxyModel = nullptr; 0047 AbstractTasksModelIface *abstractTasksSourceModel = nullptr; 0048 0049 bool anyTaskDemandsAttention = false; 0050 0051 int launcherCount = 0; 0052 0053 SortMode sortMode = SortAlpha; 0054 bool separateLaunchers = true; 0055 bool launchInPlace = false; 0056 bool launchersEverSet = false; 0057 bool launcherSortingDirty = false; 0058 bool launcherCheckNeeded = false; 0059 QList<int> sortedPreFilterRows; 0060 QVector<int> sortRowInsertQueue; 0061 bool sortRowInsertQueueStale = false; 0062 QHash<QString, int> activityTaskCounts; 0063 static VirtualDesktopInfo *virtualDesktopInfo; 0064 static int virtualDesktopInfoUsers; 0065 static ActivityInfo *activityInfo; 0066 static int activityInfoUsers; 0067 0068 bool groupInline = false; 0069 int groupingWindowTasksThreshold = -1; 0070 0071 bool usedByQml = false; 0072 bool componentComplete = false; 0073 0074 void initModels(); 0075 void initLauncherTasksModel(); 0076 void updateAnyTaskDemandsAttention(); 0077 void updateManualSortMap(); 0078 void consolidateManualSortMapForGroup(const QModelIndex &groupingProxyIndex); 0079 void updateGroupInline(); 0080 QModelIndex preFilterIndex(const QModelIndex &sourceIndex) const; 0081 void updateActivityTaskCounts(); 0082 void forceResort(); 0083 bool lessThan(const QModelIndex &left, const QModelIndex &right, bool sortOnlyLaunchers = false) const; 0084 0085 private: 0086 TasksModel *q; 0087 }; 0088 0089 class TasksModel::TasksModelLessThan 0090 { 0091 public: 0092 inline TasksModelLessThan(const QAbstractItemModel *s, TasksModel *p, bool sortOnlyLaunchers) 0093 : sourceModel(s) 0094 , tasksModel(p) 0095 , sortOnlyLaunchers(sortOnlyLaunchers) 0096 { 0097 } 0098 0099 inline bool operator()(int r1, int r2) const 0100 { 0101 QModelIndex i1 = sourceModel->index(r1, 0); 0102 QModelIndex i2 = sourceModel->index(r2, 0); 0103 return tasksModel->d->lessThan(i1, i2, sortOnlyLaunchers); 0104 } 0105 0106 private: 0107 const QAbstractItemModel *sourceModel; 0108 const TasksModel *tasksModel; 0109 bool sortOnlyLaunchers; 0110 }; 0111 0112 int TasksModel::Private::instanceCount = 0; 0113 WindowTasksModel *TasksModel::Private::windowTasksModel = nullptr; 0114 StartupTasksModel *TasksModel::Private::startupTasksModel = nullptr; 0115 VirtualDesktopInfo *TasksModel::Private::virtualDesktopInfo = nullptr; 0116 int TasksModel::Private::virtualDesktopInfoUsers = 0; 0117 ActivityInfo *TasksModel::Private::activityInfo = nullptr; 0118 int TasksModel::Private::activityInfoUsers = 0; 0119 0120 TasksModel::Private::Private(TasksModel *q) 0121 : q(q) 0122 { 0123 ++instanceCount; 0124 } 0125 0126 TasksModel::Private::~Private() 0127 { 0128 --instanceCount; 0129 0130 if (sortMode == SortActivity) { 0131 --activityInfoUsers; 0132 } 0133 0134 if (!instanceCount) { 0135 delete windowTasksModel; 0136 windowTasksModel = nullptr; 0137 delete startupTasksModel; 0138 startupTasksModel = nullptr; 0139 delete virtualDesktopInfo; 0140 virtualDesktopInfo = nullptr; 0141 delete activityInfo; 0142 activityInfo = nullptr; 0143 } 0144 } 0145 0146 void TasksModel::Private::initModels() 0147 { 0148 // NOTE: Overview over the entire model chain assembled here: 0149 // WindowTasksModel, StartupTasksModel, LauncherTasksModel 0150 // -> concatProxyModel concatenates them into a single list. 0151 // -> filterProxyModel filters by state (e.g. virtual desktop). 0152 // -> groupingProxyModel groups by application (we go from flat list to tree). 0153 // -> flattenGroupsProxyModel (optionally, if groupInline == true) flattens groups out. 0154 // -> TasksModel collapses (top-level) items into task lifecycle abstraction; sorts. 0155 0156 concatProxyModel = new ConcatenateTasksProxyModel(q); 0157 0158 if (!windowTasksModel) { 0159 windowTasksModel = new WindowTasksModel(); 0160 } 0161 0162 concatProxyModel->addSourceModel(windowTasksModel); 0163 0164 QObject::connect(windowTasksModel, &QAbstractItemModel::rowsInserted, q, [this]() { 0165 if (sortMode == SortActivity) { 0166 updateActivityTaskCounts(); 0167 } 0168 }); 0169 0170 QObject::connect(windowTasksModel, &QAbstractItemModel::rowsRemoved, q, [this]() { 0171 if (sortMode == SortActivity) { 0172 updateActivityTaskCounts(); 0173 forceResort(); 0174 } 0175 // the active task may have potentially changed, so signal that so that users 0176 // will recompute it 0177 Q_EMIT q->activeTaskChanged(); 0178 }); 0179 0180 QObject::connect(windowTasksModel, 0181 &QAbstractItemModel::dataChanged, 0182 q, 0183 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) { 0184 Q_UNUSED(topLeft) 0185 Q_UNUSED(bottomRight) 0186 0187 if (sortMode == SortActivity && roles.contains(AbstractTasksModel::Activities)) { 0188 updateActivityTaskCounts(); 0189 } 0190 0191 if (roles.contains(AbstractTasksModel::IsActive)) { 0192 Q_EMIT q->activeTaskChanged(); 0193 } 0194 0195 // In manual sort mode, updateManualSortMap() may consult the sortRowInsertQueue 0196 // for new tasks to sort in. Hidden tasks remain in the queue to potentially sort 0197 // them later, when they are are actually revealed to the user. 0198 // This is particularly useful in concert with taskmanagerrulesrc's SkipTaskbar 0199 // key, which is used to hide window tasks which update from bogus to useful 0200 // window metadata early in startup. The role change then coincides with positive 0201 // app identification, which is when updateManualSortMap() becomes able to sort the 0202 // task adjacent to its launcher when required to do so. 0203 if (sortMode == SortManual && roles.contains(AbstractTasksModel::SkipTaskbar)) { 0204 updateManualSortMap(); 0205 } 0206 }); 0207 0208 if (!startupTasksModel) { 0209 startupTasksModel = new StartupTasksModel(); 0210 } 0211 0212 concatProxyModel->addSourceModel(startupTasksModel); 0213 0214 // If we're in manual sort mode, we need to seed the sort map on pending row 0215 // insertions. 0216 QObject::connect(concatProxyModel, &QAbstractItemModel::rowsAboutToBeInserted, q, [this](const QModelIndex &parent, int start, int end) { 0217 Q_UNUSED(parent) 0218 0219 if (sortMode != SortManual) { 0220 return; 0221 } 0222 0223 const int delta = (end - start) + 1; 0224 for (auto it = sortedPreFilterRows.begin(); it != sortedPreFilterRows.end(); ++it) { 0225 if ((*it) >= start) { 0226 *it += delta; 0227 } 0228 } 0229 0230 for (int i = start; i <= end; ++i) { 0231 sortedPreFilterRows.append(i); 0232 0233 if (!separateLaunchers) { 0234 if (sortRowInsertQueueStale) { 0235 sortRowInsertQueue.clear(); 0236 sortRowInsertQueueStale = false; 0237 } 0238 0239 sortRowInsertQueue.append(sortedPreFilterRows.count() - 1); 0240 } 0241 } 0242 }); 0243 0244 // If we're in manual sort mode, we need to update the sort map on row insertions. 0245 QObject::connect(concatProxyModel, &QAbstractItemModel::rowsInserted, q, [this](const QModelIndex &parent, int start, int end) { 0246 Q_UNUSED(parent) 0247 Q_UNUSED(start) 0248 Q_UNUSED(end) 0249 0250 if (sortMode == SortManual) { 0251 updateManualSortMap(); 0252 } 0253 }); 0254 0255 // If we're in manual sort mode, we need to update the sort map after row removals. 0256 QObject::connect(concatProxyModel, &QAbstractItemModel::rowsRemoved, q, [this](const QModelIndex &parent, int first, int last) { 0257 Q_UNUSED(parent) 0258 0259 if (sortMode != SortManual) { 0260 return; 0261 } 0262 0263 if (sortRowInsertQueueStale) { 0264 sortRowInsertQueue.clear(); 0265 sortRowInsertQueueStale = false; 0266 } 0267 0268 for (int i = first; i <= last; ++i) { 0269 sortedPreFilterRows.removeOne(i); 0270 } 0271 0272 const int delta = (last - first) + 1; 0273 for (auto it = sortedPreFilterRows.begin(); it != sortedPreFilterRows.end(); ++it) { 0274 if ((*it) > last) { 0275 *it -= delta; 0276 } 0277 } 0278 }); 0279 0280 filterProxyModel = new TaskFilterProxyModel(q); 0281 filterProxyModel->setSourceModel(concatProxyModel); 0282 QObject::connect(filterProxyModel, &TaskFilterProxyModel::virtualDesktopChanged, q, &TasksModel::virtualDesktopChanged); 0283 QObject::connect(filterProxyModel, &TaskFilterProxyModel::screenGeometryChanged, q, &TasksModel::screenGeometryChanged); 0284 QObject::connect(filterProxyModel, &TaskFilterProxyModel::activityChanged, q, &TasksModel::activityChanged); 0285 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByVirtualDesktopChanged, q, &TasksModel::filterByVirtualDesktopChanged); 0286 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByScreenChanged, q, &TasksModel::filterByScreenChanged); 0287 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByActivityChanged, q, &TasksModel::filterByActivityChanged); 0288 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterMinimizedChanged, q, &TasksModel::filterMinimizedChanged); 0289 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterNotMinimizedChanged, q, &TasksModel::filterNotMinimizedChanged); 0290 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterNotMaximizedChanged, q, &TasksModel::filterNotMaximizedChanged); 0291 QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterHiddenChanged, q, &TasksModel::filterHiddenChanged); 0292 0293 groupingProxyModel = new TaskGroupingProxyModel(q); 0294 groupingProxyModel->setSourceModel(filterProxyModel); 0295 QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::groupModeChanged, q, &TasksModel::groupModeChanged); 0296 QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedAppIdsChanged, q, &TasksModel::groupingAppIdBlacklistChanged); 0297 QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedLauncherUrlsChanged, q, &TasksModel::groupingLauncherUrlBlacklistChanged); 0298 0299 QObject::connect(groupingProxyModel, &QAbstractItemModel::rowsInserted, q, [this](const QModelIndex &parent, int first, int last) { 0300 if (parent.isValid()) { 0301 if (sortMode == SortManual) { 0302 consolidateManualSortMapForGroup(parent); 0303 } 0304 0305 // Existence of a group means everything below this has already been done. 0306 return; 0307 } 0308 0309 bool demandsAttentionUpdateNeeded = false; 0310 0311 for (int i = first; i <= last; ++i) { 0312 const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0); 0313 const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString(); 0314 0315 if (sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) { 0316 demandsAttentionUpdateNeeded = true; 0317 } 0318 0319 // When we get a window we have a startup for, cause the startup to be re-filtered. 0320 if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) { 0321 const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString(); 0322 0323 for (int j = 0; j < filterProxyModel->rowCount(); ++j) { 0324 QModelIndex filterIndex = filterProxyModel->index(j, 0); 0325 0326 if (!filterIndex.data(AbstractTasksModel::IsStartup).toBool()) { 0327 continue; 0328 } 0329 0330 if ((!appId.isEmpty() && appId == filterIndex.data(AbstractTasksModel::AppId).toString()) 0331 || (!appName.isEmpty() && appName == filterIndex.data(AbstractTasksModel::AppName).toString())) { 0332 Q_EMIT filterProxyModel->dataChanged(filterIndex, filterIndex); 0333 } 0334 } 0335 } 0336 0337 // When we get a window or startup we have a launcher for, cause the launcher to be re-filtered. 0338 if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool() || sourceIndex.data(AbstractTasksModel::IsStartup).toBool()) { 0339 for (int j = 0; j < filterProxyModel->rowCount(); ++j) { 0340 const QModelIndex &filterIndex = filterProxyModel->index(j, 0); 0341 0342 if (!filterIndex.data(AbstractTasksModel::IsLauncher).toBool()) { 0343 continue; 0344 } 0345 0346 if (appsMatch(sourceIndex, filterIndex)) { 0347 Q_EMIT filterProxyModel->dataChanged(filterIndex, filterIndex); 0348 } 0349 } 0350 } 0351 } 0352 0353 if (!anyTaskDemandsAttention && demandsAttentionUpdateNeeded) { 0354 updateAnyTaskDemandsAttention(); 0355 } 0356 }); 0357 0358 QObject::connect(groupingProxyModel, &QAbstractItemModel::rowsAboutToBeRemoved, q, [this](const QModelIndex &parent, int first, int last) { 0359 // We can ignore group members. 0360 if (parent.isValid()) { 0361 return; 0362 } 0363 0364 for (int i = first; i <= last; ++i) { 0365 const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0); 0366 0367 // When a window or startup task is removed, we have to trigger a re-filter of 0368 // our launchers to (possibly) pop them back in. 0369 // NOTE: An older revision of this code compared the window and startup tasks 0370 // to the launchers to figure out which launchers should be re-filtered. This 0371 // was fine until we discovered that certain applications (e.g. Google Chrome) 0372 // change their window metadata specifically during tear-down, sometimes 0373 // breaking TaskTools::appsMatch (it's a race) and causing the associated 0374 // launcher to remain hidden. Therefore we now consider any top-level window or 0375 // startup task removal a trigger to re-filter all launchers. We don't do this 0376 // in response to the window metadata changes (even though it would be strictly 0377 // more correct, as then-ending identity match-up was what caused the launcher 0378 // to be hidden) because we don't want the launcher and window/startup task to 0379 // briefly co-exist in the model. 0380 if (!launcherCheckNeeded && launcherTasksModel 0381 && (sourceIndex.data(AbstractTasksModel::IsWindow).toBool() || sourceIndex.data(AbstractTasksModel::IsStartup).toBool())) { 0382 launcherCheckNeeded = true; 0383 } 0384 } 0385 }); 0386 0387 QObject::connect(filterProxyModel, &QAbstractItemModel::rowsRemoved, q, [this](const QModelIndex &parent, int first, int last) { 0388 Q_UNUSED(parent) 0389 Q_UNUSED(first) 0390 Q_UNUSED(last) 0391 0392 if (launcherCheckNeeded) { 0393 for (int i = 0; i < filterProxyModel->rowCount(); ++i) { 0394 const QModelIndex &idx = filterProxyModel->index(i, 0); 0395 0396 if (idx.data(AbstractTasksModel::IsLauncher).toBool()) { 0397 Q_EMIT filterProxyModel->dataChanged(idx, idx); 0398 } 0399 } 0400 0401 launcherCheckNeeded = false; 0402 } 0403 0404 // One of the removed tasks might have been demanding attention, but 0405 // we can't check the state after the window has been closed already, 0406 // so we always have to do a full update. 0407 if (anyTaskDemandsAttention) { 0408 updateAnyTaskDemandsAttention(); 0409 } 0410 }); 0411 0412 // Update anyTaskDemandsAttention on source data changes. 0413 QObject::connect(groupingProxyModel, 0414 &QAbstractItemModel::dataChanged, 0415 q, 0416 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) { 0417 Q_UNUSED(bottomRight) 0418 0419 // We can ignore group members. 0420 if (topLeft.parent().isValid()) { 0421 return; 0422 } 0423 0424 if (roles.isEmpty() || roles.contains(AbstractTasksModel::IsDemandingAttention)) { 0425 updateAnyTaskDemandsAttention(); 0426 } 0427 0428 if (roles.isEmpty() || roles.contains(AbstractTasksModel::AppId)) { 0429 for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { 0430 const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0); 0431 0432 // When a window task changes identity to one we have a launcher for, cause 0433 // the launcher to be re-filtered. 0434 if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) { 0435 for (int i = 0; i < filterProxyModel->rowCount(); ++i) { 0436 const QModelIndex &filterIndex = filterProxyModel->index(i, 0); 0437 0438 if (!filterIndex.data(AbstractTasksModel::IsLauncher).toBool()) { 0439 continue; 0440 } 0441 0442 if (appsMatch(sourceIndex, filterIndex)) { 0443 Q_EMIT filterProxyModel->dataChanged(filterIndex, filterIndex); 0444 } 0445 } 0446 } 0447 } 0448 } 0449 }); 0450 0451 // Update anyTaskDemandsAttention on source model resets. 0452 QObject::connect(groupingProxyModel, &QAbstractItemModel::modelReset, q, [this]() { 0453 updateAnyTaskDemandsAttention(); 0454 }); 0455 } 0456 0457 void TasksModel::Private::updateAnyTaskDemandsAttention() 0458 { 0459 bool taskFound = false; 0460 0461 for (int i = 0; i < groupingProxyModel->rowCount(); ++i) { 0462 if (groupingProxyModel->index(i, 0).data(AbstractTasksModel::IsDemandingAttention).toBool()) { 0463 taskFound = true; 0464 break; 0465 } 0466 } 0467 0468 if (taskFound != anyTaskDemandsAttention) { 0469 anyTaskDemandsAttention = taskFound; 0470 Q_EMIT q->anyTaskDemandsAttentionChanged(); 0471 } 0472 } 0473 0474 void TasksModel::Private::initLauncherTasksModel() 0475 { 0476 if (launcherTasksModel) { 0477 return; 0478 } 0479 0480 launcherTasksModel = new LauncherTasksModel(q); 0481 QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, q, &TasksModel::launcherListChanged); 0482 QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, q, &TasksModel::updateLauncherCount); 0483 0484 // TODO: On the assumptions that adding/removing launchers is a rare event and 0485 // the HasLaunchers data role is rarely used, this refreshes it for all rows in 0486 // the model. If those assumptions are proven wrong later, this could be 0487 // optimized to only refresh non-launcher rows matching the inserted or about- 0488 // to-be-removed launcherTasksModel rows using TaskTools::appsMatch(). 0489 QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, q, [this]() { 0490 Q_EMIT q->dataChanged(q->index(0, 0), q->index(q->rowCount() - 1, 0), QVector<int>{AbstractTasksModel::HasLauncher}); 0491 }); 0492 0493 // data() implements AbstractTasksModel::HasLauncher by checking with 0494 // TaskTools::appsMatch, which evaluates ::AppId and ::LauncherUrlWithoutIcon. 0495 QObject::connect(q, &QAbstractItemModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) { 0496 if (roles.contains(AbstractTasksModel::AppId) || roles.contains(AbstractTasksModel::LauncherUrlWithoutIcon)) { 0497 for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { 0498 const QModelIndex &index = q->index(i, 0); 0499 0500 if (!index.data(AbstractTasksModel::IsLauncher).toBool()) { 0501 Q_EMIT q->dataChanged(index, index, QVector<int>{AbstractTasksModel::HasLauncher}); 0502 } 0503 } 0504 } 0505 }); 0506 0507 concatProxyModel->addSourceModel(launcherTasksModel); 0508 } 0509 0510 void TasksModel::Private::updateManualSortMap() 0511 { 0512 // Empty map; full sort. 0513 if (sortedPreFilterRows.isEmpty()) { 0514 sortedPreFilterRows.reserve(concatProxyModel->rowCount()); 0515 0516 for (int i = 0; i < concatProxyModel->rowCount(); ++i) { 0517 sortedPreFilterRows.append(i); 0518 } 0519 0520 // Full sort. 0521 TasksModelLessThan lt(concatProxyModel, q, false); 0522 std::stable_sort(sortedPreFilterRows.begin(), sortedPreFilterRows.end(), lt); 0523 0524 // Consolidate sort map entries for groups. 0525 if (q->groupMode() != GroupDisabled) { 0526 for (int i = 0; i < groupingProxyModel->rowCount(); ++i) { 0527 const QModelIndex &groupingIndex = groupingProxyModel->index(i, 0); 0528 0529 if (groupingIndex.data(AbstractTasksModel::IsGroupParent).toBool()) { 0530 consolidateManualSortMapForGroup(groupingIndex); 0531 } 0532 } 0533 } 0534 0535 return; 0536 } 0537 0538 // Existing map; check whether launchers need sorting by launcher list position. 0539 if (separateLaunchers) { 0540 // Sort only launchers. 0541 TasksModelLessThan lt(concatProxyModel, q, true); 0542 std::stable_sort(sortedPreFilterRows.begin(), sortedPreFilterRows.end(), lt); 0543 // Otherwise process any entries in the insert queue and move them intelligently 0544 // in the sort map. 0545 } else { 0546 QMutableVectorIterator<int> i(sortRowInsertQueue); 0547 0548 while (i.hasNext()) { 0549 i.next(); 0550 0551 const int row = i.value(); 0552 const QModelIndex &idx = concatProxyModel->index(sortedPreFilterRows.at(row), 0); 0553 0554 // If a window task is currently hidden, we may want to keep it in the queue 0555 // to sort it in later once it gets revealed. 0556 // This is important in concert with taskmanagerrulesrc's SkipTaskbar key, which 0557 // is used to hide window tasks which update from bogus to useful window metadata 0558 // early in startup. Once the task no longer uses bogus metadata listed in the 0559 // config key, its SkipTaskbar role changes to false, and then is it possible to 0560 // sort the task adjacent to its launcher in the code below. 0561 if (idx.data(AbstractTasksModel::IsWindow).toBool() && idx.data(AbstractTasksModel::SkipTaskbar).toBool()) { 0562 // Since we're going to keep a row in the queue for now, make sure to 0563 // mark the queue as stale so it's cleared on appends or row removals 0564 // when they follow this sorting attempt. This frees us from having to 0565 // update the indices in the queue to keep them valid. 0566 // This means windowing system changes such as the opening or closing 0567 // of a window task which happen during the time period that a window 0568 // task has known bogus metadata, can upset what we're trying to 0569 // achieve with this exception. However, due to the briefness of the 0570 // time period and usage patterns, this is improbable, making this 0571 // likely good enough. If it turns out not to be, this decision may be 0572 // revisited later. 0573 sortRowInsertQueueStale = true; 0574 0575 break; 0576 } else { 0577 i.remove(); 0578 } 0579 0580 bool moved = false; 0581 0582 // Try to move the task up to its right-most app sibling, unless this 0583 // is us sorting in a launcher list for the first time. 0584 if (launchersEverSet && !idx.data(AbstractTasksModel::IsLauncher).toBool()) { 0585 for (int j = (row - 1); j >= 0; --j) { 0586 const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(j), 0); 0587 0588 // Once we got a match, check if the filter model accepts the potential 0589 // sibling. We don't want to sort new tasks in next to tasks it will 0590 // filter out once it sees it anyway. 0591 if (appsMatch(concatProxyIndex, idx) && filterProxyModel->acceptsRow(concatProxyIndex.row())) { 0592 sortedPreFilterRows.move(row, j + 1); 0593 moved = true; 0594 0595 break; 0596 } 0597 } 0598 } 0599 0600 int insertPos = 0; 0601 0602 // If unsuccessful or skipped, and the new task is a launcher, put after 0603 // the rightmost launcher or launcher-backed task in the map, or failing 0604 // that at the start of the map. 0605 if (!moved && idx.data(AbstractTasksModel::IsLauncher).toBool()) { 0606 for (int j = 0; j < row; ++j) { 0607 const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(j), 0); 0608 0609 if (concatProxyIndex.data(AbstractTasksModel::IsLauncher).toBool() 0610 || launcherTasksModel->launcherPosition(concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()) != -1) { 0611 insertPos = j + 1; 0612 } else { 0613 break; 0614 } 0615 } 0616 0617 sortedPreFilterRows.move(row, insertPos); 0618 moved = true; 0619 } 0620 0621 // If we sorted in a launcher and it's the first time we're sorting in a 0622 // launcher list, move existing windows to the launcher position now. 0623 if (moved && !launchersEverSet) { 0624 for (int j = (sortedPreFilterRows.count() - 1); j >= 0; --j) { 0625 const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(j), 0); 0626 0627 if (!concatProxyIndex.data(AbstractTasksModel::IsLauncher).toBool() 0628 && idx.data(AbstractTasksModel::LauncherUrlWithoutIcon) == concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon)) { 0629 sortedPreFilterRows.move(j, insertPos); 0630 0631 if (insertPos > j) { 0632 --insertPos; 0633 } 0634 } 0635 } 0636 } 0637 } 0638 } 0639 } 0640 0641 void TasksModel::Private::consolidateManualSortMapForGroup(const QModelIndex &groupingProxyIndex) 0642 { 0643 // Consolidates sort map entries for a group's items to be contiguous 0644 // after the group's first item and the same order as in groupingProxyModel. 0645 0646 const int childCount = groupingProxyModel->rowCount(groupingProxyIndex); 0647 0648 if (!childCount) { 0649 return; 0650 } 0651 0652 const QModelIndex &leader = groupingProxyModel->index(0, 0, groupingProxyIndex); 0653 const QModelIndex &preFilterLeader = filterProxyModel->mapToSource(groupingProxyModel->mapToSource(leader)); 0654 0655 // We're moving the trailing children to the sort map position of 0656 // the first child, so we're skipping the first child. 0657 for (int i = 1; i < childCount; ++i) { 0658 const QModelIndex &child = groupingProxyModel->index(i, 0, groupingProxyIndex); 0659 const QModelIndex &preFilterChild = filterProxyModel->mapToSource(groupingProxyModel->mapToSource(child)); 0660 const int leaderPos = sortedPreFilterRows.indexOf(preFilterLeader.row()); 0661 const int childPos = sortedPreFilterRows.indexOf(preFilterChild.row()); 0662 const int insertPos = (leaderPos + i) + ((leaderPos + i) > childPos ? -1 : 0); 0663 sortedPreFilterRows.move(childPos, insertPos); 0664 } 0665 } 0666 0667 void TasksModel::Private::updateGroupInline() 0668 { 0669 if (usedByQml && !componentComplete) { 0670 return; 0671 } 0672 0673 bool hadSourceModel = (q->sourceModel() != nullptr); 0674 0675 if (q->groupMode() != GroupDisabled && groupInline) { 0676 if (flattenGroupsProxyModel) { 0677 return; 0678 } 0679 0680 // Exempting tasks which demand attention from grouping is not 0681 // necessary when all group children are shown inline anyway 0682 // and would interfere with our sort-tasks-together goals. 0683 groupingProxyModel->setGroupDemandingAttention(true); 0684 0685 // Likewise, ignore the window tasks threshold when making 0686 // grouping decisions. 0687 groupingProxyModel->setWindowTasksThreshold(-1); 0688 0689 flattenGroupsProxyModel = new FlattenTaskGroupsProxyModel(q); 0690 flattenGroupsProxyModel->setSourceModel(groupingProxyModel); 0691 0692 abstractTasksSourceModel = flattenGroupsProxyModel; 0693 q->setSourceModel(flattenGroupsProxyModel); 0694 0695 if (sortMode == SortManual) { 0696 forceResort(); 0697 } 0698 } else { 0699 if (hadSourceModel && !flattenGroupsProxyModel) { 0700 return; 0701 } 0702 0703 groupingProxyModel->setGroupDemandingAttention(false); 0704 groupingProxyModel->setWindowTasksThreshold(groupingWindowTasksThreshold); 0705 0706 abstractTasksSourceModel = groupingProxyModel; 0707 q->setSourceModel(groupingProxyModel); 0708 0709 delete flattenGroupsProxyModel; 0710 flattenGroupsProxyModel = nullptr; 0711 0712 if (hadSourceModel && sortMode == SortManual) { 0713 forceResort(); 0714 } 0715 } 0716 0717 // Minor optimization: We only make these connections after we populate for 0718 // the first time to avoid some churn. 0719 if (!hadSourceModel) { 0720 QObject::connect(q, &QAbstractItemModel::rowsInserted, q, &TasksModel::updateLauncherCount, Qt::UniqueConnection); 0721 QObject::connect(q, &QAbstractItemModel::rowsRemoved, q, &TasksModel::updateLauncherCount, Qt::UniqueConnection); 0722 QObject::connect(q, &QAbstractItemModel::modelReset, q, &TasksModel::updateLauncherCount, Qt::UniqueConnection); 0723 0724 QObject::connect(q, &QAbstractItemModel::rowsInserted, q, &TasksModel::countChanged, Qt::UniqueConnection); 0725 QObject::connect(q, &QAbstractItemModel::rowsRemoved, q, &TasksModel::countChanged, Qt::UniqueConnection); 0726 QObject::connect(q, &QAbstractItemModel::modelReset, q, &TasksModel::countChanged, Qt::UniqueConnection); 0727 } 0728 } 0729 0730 QModelIndex TasksModel::Private::preFilterIndex(const QModelIndex &sourceIndex) const 0731 { 0732 // Only in inline grouping mode, we have an additional proxy layer. 0733 if (flattenGroupsProxyModel) { 0734 return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(flattenGroupsProxyModel->mapToSource(sourceIndex))); 0735 } else { 0736 return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(sourceIndex)); 0737 } 0738 } 0739 0740 void TasksModel::Private::updateActivityTaskCounts() 0741 { 0742 // Collects the number of window tasks on each activity. 0743 0744 activityTaskCounts.clear(); 0745 0746 if (!windowTasksModel || !activityInfo) { 0747 return; 0748 } 0749 0750 foreach (const QString &activity, activityInfo->runningActivities()) { 0751 activityTaskCounts.insert(activity, 0); 0752 } 0753 0754 for (int i = 0; i < windowTasksModel->rowCount(); ++i) { 0755 const QModelIndex &windowIndex = windowTasksModel->index(i, 0); 0756 const QStringList &activities = windowIndex.data(AbstractTasksModel::Activities).toStringList(); 0757 0758 if (activities.isEmpty()) { 0759 QMutableHashIterator<QString, int> it(activityTaskCounts); 0760 0761 while (it.hasNext()) { 0762 it.next(); 0763 it.setValue(it.value() + 1); 0764 } 0765 } else { 0766 foreach (const QString &activity, activities) { 0767 ++activityTaskCounts[activity]; 0768 } 0769 } 0770 } 0771 } 0772 0773 void TasksModel::Private::forceResort() 0774 { 0775 // HACK: This causes QSortFilterProxyModel to run all rows through 0776 // our lessThan() implementation again. 0777 q->setDynamicSortFilter(false); 0778 q->setDynamicSortFilter(true); 0779 } 0780 0781 bool TasksModel::Private::lessThan(const QModelIndex &left, const QModelIndex &right, bool sortOnlyLaunchers) const 0782 { 0783 // Launcher tasks go first. 0784 // When launchInPlace is enabled, startup and window tasks are sorted 0785 // as the launchers they replace (see also move()). 0786 0787 if (separateLaunchers) { 0788 if (left.data(AbstractTasksModel::IsLauncher).toBool() && right.data(AbstractTasksModel::IsLauncher).toBool()) { 0789 return (left.row() < right.row()); 0790 } else if (left.data(AbstractTasksModel::IsLauncher).toBool() && !right.data(AbstractTasksModel::IsLauncher).toBool()) { 0791 if (launchInPlace) { 0792 const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); 0793 const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); 0794 0795 if (rightPos != -1) { 0796 return (leftPos < rightPos); 0797 } 0798 } 0799 0800 return true; 0801 } else if (!left.data(AbstractTasksModel::IsLauncher).toBool() && right.data(AbstractTasksModel::IsLauncher).toBool()) { 0802 if (launchInPlace) { 0803 const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); 0804 const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); 0805 0806 if (leftPos != -1) { 0807 return (leftPos < rightPos); 0808 } 0809 } 0810 0811 return false; 0812 } else if (launchInPlace) { 0813 const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); 0814 const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); 0815 0816 if (leftPos != -1 && rightPos != -1) { 0817 return (leftPos < rightPos); 0818 } else if (leftPos != -1 && rightPos == -1) { 0819 return true; 0820 } else if (leftPos == -1 && rightPos != -1) { 0821 return false; 0822 } 0823 } 0824 } 0825 0826 // If told to stop after launchers we fall through to the existing map if it exists. 0827 if (sortOnlyLaunchers && !sortedPreFilterRows.isEmpty()) { 0828 return (sortedPreFilterRows.indexOf(left.row()) < sortedPreFilterRows.indexOf(right.row())); 0829 } 0830 0831 // Sort other cases by sort mode. 0832 switch (sortMode) { 0833 case SortLastActivated: { 0834 QTime leftSortTime, rightSortTime; 0835 0836 // Check if the task is in a group 0837 if (left.parent().isValid()) { 0838 leftSortTime = left.parent().data(AbstractTasksModel::LastActivated).toTime(); 0839 } else { 0840 leftSortTime = left.data(AbstractTasksModel::LastActivated).toTime(); 0841 } 0842 0843 if (!leftSortTime.isValid()) { 0844 leftSortTime = left.data(Qt::DisplayRole).toTime(); 0845 } 0846 0847 if (right.parent().isValid()) { 0848 rightSortTime = right.parent().data(AbstractTasksModel::LastActivated).toTime(); 0849 } else { 0850 rightSortTime = right.data(AbstractTasksModel::LastActivated).toTime(); 0851 } 0852 0853 if (!rightSortTime.isValid()) { 0854 rightSortTime = right.data(Qt::DisplayRole).toTime(); 0855 } 0856 0857 if (leftSortTime != rightSortTime) { 0858 // Move latest to leftmost 0859 return leftSortTime > rightSortTime; 0860 } 0861 0862 Q_FALLTHROUGH(); 0863 } 0864 0865 case SortVirtualDesktop: { 0866 const bool leftAll = left.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool(); 0867 const bool rightAll = right.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool(); 0868 0869 if (leftAll && !rightAll) { 0870 return true; 0871 } else if (rightAll && !leftAll) { 0872 return false; 0873 } 0874 0875 if (!(leftAll && rightAll)) { 0876 const QVariantList &leftDesktops = left.data(AbstractTasksModel::VirtualDesktops).toList(); 0877 QVariant leftDesktop; 0878 int leftDesktopPos = virtualDesktopInfo->numberOfDesktops(); 0879 0880 for (const QVariant &desktop : leftDesktops) { 0881 const int desktopPos = virtualDesktopInfo->position(desktop); 0882 0883 if (desktopPos <= leftDesktopPos) { 0884 leftDesktop = desktop; 0885 leftDesktopPos = desktopPos; 0886 } 0887 } 0888 0889 const QVariantList &rightDesktops = right.data(AbstractTasksModel::VirtualDesktops).toList(); 0890 QVariant rightDesktop; 0891 int rightDesktopPos = virtualDesktopInfo->numberOfDesktops(); 0892 0893 for (const QVariant &desktop : rightDesktops) { 0894 const int desktopPos = virtualDesktopInfo->position(desktop); 0895 0896 if (desktopPos <= rightDesktopPos) { 0897 rightDesktop = desktop; 0898 rightDesktopPos = desktopPos; 0899 } 0900 } 0901 0902 if (!leftDesktop.isNull() && !rightDesktop.isNull() && (leftDesktop != rightDesktop)) { 0903 return (virtualDesktopInfo->position(leftDesktop) < virtualDesktopInfo->position(rightDesktop)); 0904 } else if (!leftDesktop.isNull() && rightDesktop.isNull()) { 0905 return false; 0906 } else if (leftDesktop.isNull() && !rightDesktop.isNull()) { 0907 return true; 0908 } 0909 } 0910 } 0911 // fall through 0912 case SortActivity: { 0913 // updateActivityTaskCounts() counts the number of window tasks on each 0914 // activity. This will sort tasks by comparing a cumulative score made 0915 // up of the task counts for each activity a task is assigned to, and 0916 // otherwise fall through to alphabetical sorting. 0917 const QStringList &leftActivities = left.data(AbstractTasksModel::Activities).toStringList(); 0918 int leftScore = std::accumulate(leftActivities.cbegin(), leftActivities.cend(), -1, [this](int a, const QString &activity) { 0919 return a + activityTaskCounts[activity]; 0920 }); 0921 0922 const QStringList &rightActivities = right.data(AbstractTasksModel::Activities).toStringList(); 0923 int rightScore = std::accumulate(rightActivities.cbegin(), rightActivities.cend(), -1, [this](int a, const QString &activity) { 0924 return a + activityTaskCounts[activity]; 0925 }); 0926 0927 if (leftScore == -1 || rightScore == -1) { 0928 const int sumScore = std::accumulate(activityTaskCounts.constBegin(), activityTaskCounts.constEnd(), 0); 0929 0930 if (leftScore == -1) { 0931 leftScore = sumScore; 0932 } 0933 0934 if (rightScore == -1) { 0935 rightScore = sumScore; 0936 } 0937 } 0938 0939 if (leftScore != rightScore) { 0940 return (leftScore > rightScore); 0941 } 0942 } 0943 // Fall through to source order if sorting is disabled or manual, or alphabetical by app name otherwise. 0944 // This marker comment makes gcc/clang happy: 0945 // fall through 0946 default: { 0947 if (sortMode == SortDisabled) { 0948 return (left.row() < right.row()); 0949 } else { 0950 // The overall goal of alphabetic sorting is to sort tasks belonging to the 0951 // same app together, while sorting the resulting sets alphabetically among 0952 // themselves by the app name. The following code tries to achieve this by 0953 // going for AppName first, and falling back to DisplayRole - which for 0954 // window-type tasks generally contains the window title - if AppName is 0955 // not available. When comparing tasks with identical resulting sort strings, 0956 // we sort them by the source model order (i.e. insertion/creation). Older 0957 // versions of this code compared tasks by a concatenation of AppName and 0958 // DisplayRole at all times, but always sorting by the window title does more 0959 // than our goal description - and can cause tasks within an app's set to move 0960 // around when window titles change, which is a nuisance for users (especially 0961 // in case of tabbed apps that have the window title reflect the active tab, 0962 // e.g. web browsers). To recap, the common case is "sort by AppName, then 0963 // insertion order", only swapping out AppName for DisplayRole (i.e. window 0964 // title) when necessary. 0965 0966 QString leftSortString = left.data(AbstractTasksModel::AppName).toString(); 0967 0968 if (leftSortString.isEmpty()) { 0969 leftSortString = left.data(Qt::DisplayRole).toString(); 0970 } 0971 0972 QString rightSortString = right.data(AbstractTasksModel::AppName).toString(); 0973 0974 if (rightSortString.isEmpty()) { 0975 rightSortString = right.data(Qt::DisplayRole).toString(); 0976 } 0977 0978 const int sortResult = leftSortString.localeAwareCompare(rightSortString); 0979 0980 // If the string are identical fall back to source model (creation/append) order. 0981 if (sortResult == 0) { 0982 return (left.row() < right.row()); 0983 } 0984 0985 return (sortResult < 0); 0986 } 0987 } 0988 } 0989 } 0990 0991 TasksModel::TasksModel(QObject *parent) 0992 : QSortFilterProxyModel(parent) 0993 , d(new Private(this)) 0994 { 0995 d->initModels(); 0996 0997 // Start sorting. 0998 sort(0); 0999 1000 connect(this, &TasksModel::sourceModelChanged, this, &TasksModel::countChanged); 1001 1002 // Private::updateGroupInline() sets our source model, populating the model. We 1003 // delay running this until the QML runtime had a chance to call our implementation 1004 // of QQmlParserStatus::classBegin(), setting Private::usedByQml to true. If used 1005 // by QML, Private::updateGroupInline() will abort if the component is not yet 1006 // complete, instead getting called through QQmlParserStatus::componentComplete() 1007 // only after all properties have been set. This avoids delegate churn in Qt Quick 1008 // views using the model. If not used by QML, Private::updateGroupInline() will run 1009 // directly. 1010 QTimer::singleShot(0, this, [this]() { 1011 d->updateGroupInline(); 1012 }); 1013 } 1014 1015 TasksModel::~TasksModel() 1016 { 1017 } 1018 1019 QHash<int, QByteArray> TasksModel::roleNames() const 1020 { 1021 if (d->windowTasksModel) { 1022 return d->windowTasksModel->roleNames(); 1023 } 1024 1025 return QHash<int, QByteArray>(); 1026 } 1027 1028 int TasksModel::rowCount(const QModelIndex &parent) const 1029 { 1030 return QSortFilterProxyModel::rowCount(parent); 1031 } 1032 1033 QVariant TasksModel::data(const QModelIndex &proxyIndex, int role) const 1034 { 1035 if (role == AbstractTasksModel::HasLauncher && proxyIndex.isValid() && proxyIndex.row() < rowCount()) { 1036 if (proxyIndex.data(AbstractTasksModel::IsLauncher).toBool()) { 1037 return true; 1038 } else { 1039 if (!d->launcherTasksModel) { 1040 return false; 1041 } 1042 for (int i = 0; i < d->launcherTasksModel->rowCount(); ++i) { 1043 const QModelIndex &launcherIndex = d->launcherTasksModel->index(i, 0); 1044 1045 if (appsMatch(proxyIndex, launcherIndex)) { 1046 return true; 1047 } 1048 } 1049 1050 return false; 1051 } 1052 } else if (rowCount(proxyIndex) && role == AbstractTasksModel::WinIdList) { 1053 QVariantList winIds; 1054 1055 for (int i = 0; i < rowCount(proxyIndex); ++i) { 1056 winIds.append(index(i, 0, proxyIndex).data(AbstractTasksModel::WinIdList).toList()); 1057 } 1058 1059 return winIds; 1060 } 1061 1062 return QSortFilterProxyModel::data(proxyIndex, role); 1063 } 1064 1065 void TasksModel::updateLauncherCount() 1066 { 1067 if (!d->launcherTasksModel) { 1068 return; 1069 } 1070 1071 int count = 0; 1072 1073 for (int i = 0; i < rowCount(); ++i) { 1074 if (index(i, 0).data(AbstractTasksModel::IsLauncher).toBool()) { 1075 ++count; 1076 } 1077 } 1078 1079 if (d->launcherCount != count) { 1080 d->launcherCount = count; 1081 Q_EMIT launcherCountChanged(); 1082 } 1083 } 1084 1085 int TasksModel::launcherCount() const 1086 { 1087 return d->launcherCount; 1088 } 1089 1090 bool TasksModel::anyTaskDemandsAttention() const 1091 { 1092 return d->anyTaskDemandsAttention; 1093 } 1094 1095 QVariant TasksModel::virtualDesktop() const 1096 { 1097 return d->filterProxyModel->virtualDesktop(); 1098 } 1099 1100 void TasksModel::setVirtualDesktop(const QVariant &desktop) 1101 { 1102 d->filterProxyModel->setVirtualDesktop(desktop); 1103 } 1104 1105 QRect TasksModel::screenGeometry() const 1106 { 1107 return d->filterProxyModel->screenGeometry(); 1108 } 1109 1110 void TasksModel::setScreenGeometry(const QRect &geometry) 1111 { 1112 d->filterProxyModel->setScreenGeometry(geometry); 1113 } 1114 1115 QString TasksModel::activity() const 1116 { 1117 return d->filterProxyModel->activity(); 1118 } 1119 1120 void TasksModel::setActivity(const QString &activity) 1121 { 1122 d->filterProxyModel->setActivity(activity); 1123 } 1124 1125 bool TasksModel::filterByVirtualDesktop() const 1126 { 1127 return d->filterProxyModel->filterByVirtualDesktop(); 1128 } 1129 1130 void TasksModel::setFilterByVirtualDesktop(bool filter) 1131 { 1132 d->filterProxyModel->setFilterByVirtualDesktop(filter); 1133 } 1134 1135 bool TasksModel::filterByScreen() const 1136 { 1137 return d->filterProxyModel->filterByScreen(); 1138 } 1139 1140 void TasksModel::setFilterByScreen(bool filter) 1141 { 1142 d->filterProxyModel->setFilterByScreen(filter); 1143 } 1144 1145 bool TasksModel::filterByActivity() const 1146 { 1147 return d->filterProxyModel->filterByActivity(); 1148 } 1149 1150 void TasksModel::setFilterByActivity(bool filter) 1151 { 1152 d->filterProxyModel->setFilterByActivity(filter); 1153 } 1154 1155 bool TasksModel::filterMinimized() const 1156 { 1157 return d->filterProxyModel->filterMinimized(); 1158 } 1159 1160 void TasksModel::setFilterMinimized(bool filter) 1161 { 1162 d->filterProxyModel->setFilterMinimized(filter); 1163 } 1164 1165 bool TasksModel::filterNotMinimized() const 1166 { 1167 return d->filterProxyModel->filterNotMinimized(); 1168 } 1169 1170 void TasksModel::setFilterNotMinimized(bool filter) 1171 { 1172 d->filterProxyModel->setFilterNotMinimized(filter); 1173 } 1174 1175 bool TasksModel::filterNotMaximized() const 1176 { 1177 return d->filterProxyModel->filterNotMaximized(); 1178 } 1179 1180 void TasksModel::setFilterNotMaximized(bool filter) 1181 { 1182 d->filterProxyModel->setFilterNotMaximized(filter); 1183 } 1184 1185 bool TasksModel::filterHidden() const 1186 { 1187 return d->filterProxyModel->filterHidden(); 1188 } 1189 1190 void TasksModel::setFilterHidden(bool filter) 1191 { 1192 d->filterProxyModel->setFilterHidden(filter); 1193 } 1194 1195 TasksModel::SortMode TasksModel::sortMode() const 1196 { 1197 return d->sortMode; 1198 } 1199 1200 void TasksModel::setSortMode(SortMode mode) 1201 { 1202 if (d->sortMode != mode) { 1203 if (mode == SortManual) { 1204 d->updateManualSortMap(); 1205 } else if (d->sortMode == SortManual) { 1206 d->sortedPreFilterRows.clear(); 1207 } 1208 1209 if (mode == SortVirtualDesktop) { 1210 if (!d->virtualDesktopInfo) { 1211 d->virtualDesktopInfo = new VirtualDesktopInfo(); 1212 } 1213 1214 ++d->virtualDesktopInfoUsers; 1215 1216 setSortRole(AbstractTasksModel::VirtualDesktops); 1217 } else if (d->sortMode == SortVirtualDesktop) { 1218 --d->virtualDesktopInfoUsers; 1219 1220 if (!d->virtualDesktopInfoUsers) { 1221 delete d->virtualDesktopInfo; 1222 d->virtualDesktopInfo = nullptr; 1223 } 1224 1225 setSortRole(Qt::DisplayRole); 1226 } 1227 1228 if (mode == SortActivity) { 1229 if (!d->activityInfo) { 1230 d->activityInfo = new ActivityInfo(); 1231 } 1232 1233 ++d->activityInfoUsers; 1234 1235 d->updateActivityTaskCounts(); 1236 setSortRole(AbstractTasksModel::Activities); 1237 } else if (d->sortMode == SortActivity) { 1238 --d->activityInfoUsers; 1239 1240 if (!d->activityInfoUsers) { 1241 delete d->activityInfo; 1242 d->activityInfo = nullptr; 1243 } 1244 1245 d->activityTaskCounts.clear(); 1246 setSortRole(Qt::DisplayRole); 1247 } 1248 1249 if (mode == SortLastActivated) { 1250 setSortRole(AbstractTasksModel::LastActivated); 1251 } 1252 1253 d->sortMode = mode; 1254 1255 d->forceResort(); 1256 1257 Q_EMIT sortModeChanged(); 1258 } 1259 } 1260 1261 bool TasksModel::separateLaunchers() const 1262 { 1263 return d->separateLaunchers; 1264 } 1265 1266 void TasksModel::setSeparateLaunchers(bool separate) 1267 { 1268 if (d->separateLaunchers != separate) { 1269 d->separateLaunchers = separate; 1270 1271 d->updateManualSortMap(); 1272 d->forceResort(); 1273 1274 Q_EMIT separateLaunchersChanged(); 1275 } 1276 } 1277 1278 bool TasksModel::launchInPlace() const 1279 { 1280 return d->launchInPlace; 1281 } 1282 1283 void TasksModel::setLaunchInPlace(bool launchInPlace) 1284 { 1285 if (d->launchInPlace != launchInPlace) { 1286 d->launchInPlace = launchInPlace; 1287 1288 d->forceResort(); 1289 1290 Q_EMIT launchInPlaceChanged(); 1291 } 1292 } 1293 1294 TasksModel::GroupMode TasksModel::groupMode() const 1295 { 1296 if (!d->groupingProxyModel) { 1297 return GroupDisabled; 1298 } 1299 1300 return d->groupingProxyModel->groupMode(); 1301 } 1302 1303 void TasksModel::setGroupMode(GroupMode mode) 1304 { 1305 if (d->groupingProxyModel) { 1306 if (mode == GroupDisabled && d->flattenGroupsProxyModel) { 1307 d->flattenGroupsProxyModel->setSourceModel(nullptr); 1308 } 1309 1310 d->groupingProxyModel->setGroupMode(mode); 1311 d->updateGroupInline(); 1312 } 1313 } 1314 1315 bool TasksModel::groupInline() const 1316 { 1317 return d->groupInline; 1318 } 1319 1320 void TasksModel::setGroupInline(bool groupInline) 1321 { 1322 if (d->groupInline != groupInline) { 1323 d->groupInline = groupInline; 1324 1325 d->updateGroupInline(); 1326 1327 Q_EMIT groupInlineChanged(); 1328 } 1329 } 1330 1331 int TasksModel::groupingWindowTasksThreshold() const 1332 { 1333 return d->groupingWindowTasksThreshold; 1334 } 1335 1336 void TasksModel::setGroupingWindowTasksThreshold(int threshold) 1337 { 1338 if (d->groupingWindowTasksThreshold != threshold) { 1339 d->groupingWindowTasksThreshold = threshold; 1340 1341 if (!d->groupInline && d->groupingProxyModel) { 1342 d->groupingProxyModel->setWindowTasksThreshold(threshold); 1343 } 1344 1345 Q_EMIT groupingWindowTasksThresholdChanged(); 1346 } 1347 } 1348 1349 QStringList TasksModel::groupingAppIdBlacklist() const 1350 { 1351 if (!d->groupingProxyModel) { 1352 return QStringList(); 1353 } 1354 1355 return d->groupingProxyModel->blacklistedAppIds(); 1356 } 1357 1358 void TasksModel::setGroupingAppIdBlacklist(const QStringList &list) 1359 { 1360 if (d->groupingProxyModel) { 1361 d->groupingProxyModel->setBlacklistedAppIds(list); 1362 } 1363 } 1364 1365 QStringList TasksModel::groupingLauncherUrlBlacklist() const 1366 { 1367 if (!d->groupingProxyModel) { 1368 return QStringList(); 1369 } 1370 1371 return d->groupingProxyModel->blacklistedLauncherUrls(); 1372 } 1373 1374 void TasksModel::setGroupingLauncherUrlBlacklist(const QStringList &list) 1375 { 1376 if (d->groupingProxyModel) { 1377 d->groupingProxyModel->setBlacklistedLauncherUrls(list); 1378 } 1379 } 1380 1381 bool TasksModel::taskReorderingEnabled() const 1382 { 1383 return dynamicSortFilter(); 1384 } 1385 1386 void TasksModel::setTaskReorderingEnabled(bool enabled) 1387 { 1388 enabled ? setDynamicSortFilter(true) : setDynamicSortFilter(false); 1389 1390 Q_EMIT taskReorderingEnabledChanged(); 1391 } 1392 1393 QStringList TasksModel::launcherList() const 1394 { 1395 if (d->launcherTasksModel) { 1396 return d->launcherTasksModel->launcherList(); 1397 } 1398 1399 return QStringList(); 1400 } 1401 1402 void TasksModel::setLauncherList(const QStringList &launchers) 1403 { 1404 d->initLauncherTasksModel(); 1405 d->launcherTasksModel->setLauncherList(launchers); 1406 d->launchersEverSet = true; 1407 } 1408 1409 bool TasksModel::requestAddLauncher(const QUrl &url) 1410 { 1411 d->initLauncherTasksModel(); 1412 1413 bool added = d->launcherTasksModel->requestAddLauncher(url); 1414 1415 // If using manual and launch-in-place sorting with separate launchers, 1416 // we need to trigger a sort map update to move any window tasks to 1417 // their launcher position now. 1418 if (added && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) { 1419 d->updateManualSortMap(); 1420 d->forceResort(); 1421 } 1422 1423 return added; 1424 } 1425 1426 bool TasksModel::requestRemoveLauncher(const QUrl &url) 1427 { 1428 if (d->launcherTasksModel) { 1429 bool removed = d->launcherTasksModel->requestRemoveLauncher(url); 1430 1431 // If using manual and launch-in-place sorting with separate launchers, 1432 // we need to trigger a sort map update to move any window tasks no 1433 // longer backed by a launcher out of the launcher area. 1434 if (removed && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) { 1435 d->updateManualSortMap(); 1436 d->forceResort(); 1437 } 1438 1439 return removed; 1440 } 1441 1442 return false; 1443 } 1444 1445 bool TasksModel::requestAddLauncherToActivity(const QUrl &url, const QString &activity) 1446 { 1447 d->initLauncherTasksModel(); 1448 1449 bool added = d->launcherTasksModel->requestAddLauncherToActivity(url, activity); 1450 1451 // If using manual and launch-in-place sorting with separate launchers, 1452 // we need to trigger a sort map update to move any window tasks to 1453 // their launcher position now. 1454 if (added && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) { 1455 d->updateManualSortMap(); 1456 d->forceResort(); 1457 } 1458 1459 return added; 1460 } 1461 1462 bool TasksModel::requestRemoveLauncherFromActivity(const QUrl &url, const QString &activity) 1463 { 1464 if (d->launcherTasksModel) { 1465 bool removed = d->launcherTasksModel->requestRemoveLauncherFromActivity(url, activity); 1466 1467 // If using manual and launch-in-place sorting with separate launchers, 1468 // we need to trigger a sort map update to move any window tasks no 1469 // longer backed by a launcher out of the launcher area. 1470 if (removed && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) { 1471 d->updateManualSortMap(); 1472 d->forceResort(); 1473 } 1474 1475 return removed; 1476 } 1477 1478 return false; 1479 } 1480 1481 QStringList TasksModel::launcherActivities(const QUrl &url) 1482 { 1483 if (d->launcherTasksModel) { 1484 return d->launcherTasksModel->launcherActivities(url); 1485 } 1486 1487 return {}; 1488 } 1489 1490 int TasksModel::launcherPosition(const QUrl &url) const 1491 { 1492 if (d->launcherTasksModel) { 1493 return d->launcherTasksModel->launcherPosition(url); 1494 } 1495 1496 return -1; 1497 } 1498 1499 void TasksModel::requestActivate(const QModelIndex &index) 1500 { 1501 if (index.isValid() && index.model() == this) { 1502 d->abstractTasksSourceModel->requestActivate(mapToSource(index)); 1503 } 1504 } 1505 1506 void TasksModel::requestNewInstance(const QModelIndex &index) 1507 { 1508 if (index.isValid() && index.model() == this) { 1509 d->abstractTasksSourceModel->requestNewInstance(mapToSource(index)); 1510 } 1511 } 1512 1513 void TasksModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls) 1514 { 1515 if (index.isValid() && index.model() == this) { 1516 d->abstractTasksSourceModel->requestOpenUrls(mapToSource(index), urls); 1517 } 1518 } 1519 1520 void TasksModel::requestClose(const QModelIndex &index) 1521 { 1522 if (index.isValid() && index.model() == this) { 1523 d->abstractTasksSourceModel->requestClose(mapToSource(index)); 1524 } 1525 } 1526 1527 void TasksModel::requestMove(const QModelIndex &index) 1528 { 1529 if (index.isValid() && index.model() == this) { 1530 d->abstractTasksSourceModel->requestMove(mapToSource(index)); 1531 } 1532 } 1533 1534 void TasksModel::requestResize(const QModelIndex &index) 1535 { 1536 if (index.isValid() && index.model() == this) { 1537 d->abstractTasksSourceModel->requestResize(mapToSource(index)); 1538 } 1539 } 1540 1541 void TasksModel::requestToggleMinimized(const QModelIndex &index) 1542 { 1543 if (index.isValid() && index.model() == this) { 1544 d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(index)); 1545 } 1546 } 1547 1548 void TasksModel::requestToggleMaximized(const QModelIndex &index) 1549 { 1550 if (index.isValid() && index.model() == this) { 1551 d->abstractTasksSourceModel->requestToggleMaximized(mapToSource(index)); 1552 } 1553 } 1554 1555 void TasksModel::requestToggleKeepAbove(const QModelIndex &index) 1556 { 1557 if (index.isValid() && index.model() == this) { 1558 d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(index)); 1559 } 1560 } 1561 1562 void TasksModel::requestToggleKeepBelow(const QModelIndex &index) 1563 { 1564 if (index.isValid() && index.model() == this) { 1565 d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(index)); 1566 } 1567 } 1568 1569 void TasksModel::requestToggleFullScreen(const QModelIndex &index) 1570 { 1571 if (index.isValid() && index.model() == this) { 1572 d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(index)); 1573 } 1574 } 1575 1576 void TasksModel::requestToggleShaded(const QModelIndex &index) 1577 { 1578 if (index.isValid() && index.model() == this) { 1579 d->abstractTasksSourceModel->requestToggleShaded(mapToSource(index)); 1580 } 1581 } 1582 1583 void TasksModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops) 1584 { 1585 if (index.isValid() && index.model() == this) { 1586 d->abstractTasksSourceModel->requestVirtualDesktops(mapToSource(index), desktops); 1587 } 1588 } 1589 1590 void TasksModel::requestNewVirtualDesktop(const QModelIndex &index) 1591 { 1592 if (index.isValid() && index.model() == this) { 1593 d->abstractTasksSourceModel->requestNewVirtualDesktop(mapToSource(index)); 1594 } 1595 } 1596 1597 void TasksModel::requestActivities(const QModelIndex &index, const QStringList &activities) 1598 { 1599 if (index.isValid() && index.model() == this) { 1600 d->groupingProxyModel->requestActivities(mapToSource(index), activities); 1601 } 1602 } 1603 1604 void TasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) 1605 { 1606 if (!index.isValid() || index.model() != this || !index.data(AbstractTasksModel::IsWindow).toBool()) { 1607 return; 1608 } 1609 d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate); 1610 } 1611 1612 void TasksModel::requestToggleGrouping(const QModelIndex &index) 1613 { 1614 if (index.isValid() && index.model() == this) { 1615 const QModelIndex &target = (d->flattenGroupsProxyModel ? d->flattenGroupsProxyModel->mapToSource(mapToSource(index)) : mapToSource(index)); 1616 d->groupingProxyModel->requestToggleGrouping(target); 1617 } 1618 } 1619 1620 bool TasksModel::move(int row, int newPos, const QModelIndex &parent) 1621 { 1622 /* 1623 * NOTE After doing any modification in TasksModel::move, make sure fixes listed below are not regressed. 1624 * - https://bugs.kde.org/444816 1625 * - https://bugs.kde.org/448912 1626 * - https://invent.kde.org/plasma/plasma-workspace/-/commit/ea51795e8c571513e1ff583350ab8649bc857fc2 1627 */ 1628 1629 if (d->sortMode != SortManual || row == newPos || newPos < 0 || newPos >= rowCount(parent)) { 1630 return false; 1631 } 1632 1633 const QModelIndex &idx = index(row, 0, parent); 1634 bool isLauncherMove = false; 1635 1636 // Figure out if we're moving a launcher so we can run barrier checks. 1637 if (idx.isValid()) { 1638 if (idx.data(AbstractTasksModel::IsLauncher).toBool()) { 1639 isLauncherMove = true; 1640 // When using launch-in-place sorting, launcher-backed window tasks act as launchers. 1641 } else if ((d->launchInPlace || !d->separateLaunchers) && idx.data(AbstractTasksModel::IsWindow).toBool()) { 1642 const QUrl &launcherUrl = idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); 1643 const int launcherPos = launcherPosition(launcherUrl); 1644 1645 if (launcherPos != -1) { 1646 isLauncherMove = true; 1647 } 1648 } 1649 } else { 1650 return false; 1651 } 1652 1653 if (d->separateLaunchers && !parent.isValid() /* Exclude tasks in a group */) { 1654 int firstTask = 0; 1655 if (d->launcherTasksModel) { 1656 if (d->launchInPlace) { 1657 firstTask = d->launcherTasksModel->rowCountForActivity(activity()); 1658 } else { 1659 firstTask = launcherCount(); 1660 } 1661 } 1662 1663 // Don't allow launchers to be moved past the last launcher. 1664 if (isLauncherMove && newPos >= firstTask) { 1665 return false; 1666 } 1667 1668 // Don't allow tasks to be moved into the launchers. 1669 if (!isLauncherMove && newPos < firstTask) { 1670 return false; 1671 } 1672 } 1673 1674 // Treat flattened-out groups as single items. 1675 if (d->flattenGroupsProxyModel) { 1676 QModelIndex groupingRowIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(row, 0))); 1677 const QModelIndex &groupingRowIndexParent = groupingRowIndex.parent(); 1678 QModelIndex groupingNewPosIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(newPos, 0))); 1679 const QModelIndex &groupingNewPosIndexParent = groupingNewPosIndex.parent(); 1680 1681 // Disallow moves within a flattened-out group (TODO: for now, anyway). 1682 if (groupingRowIndexParent.isValid() && (groupingRowIndexParent == groupingNewPosIndex || groupingRowIndexParent == groupingNewPosIndexParent)) { 1683 return false; 1684 } 1685 1686 int offset = 0; 1687 int extraChildCount = 0; 1688 1689 if (groupingRowIndexParent.isValid()) { 1690 offset = groupingRowIndex.row(); 1691 extraChildCount = d->groupingProxyModel->rowCount(groupingRowIndexParent) - 1; 1692 groupingRowIndex = groupingRowIndexParent; 1693 } 1694 1695 if (groupingNewPosIndexParent.isValid()) { 1696 int extra = d->groupingProxyModel->rowCount(groupingNewPosIndexParent) - 1; 1697 1698 if (newPos > row) { 1699 newPos += extra; 1700 newPos -= groupingNewPosIndex.row(); 1701 groupingNewPosIndex = groupingNewPosIndexParent.model()->index(extra, 0, groupingNewPosIndexParent); 1702 } else { 1703 newPos -= groupingNewPosIndex.row(); 1704 groupingNewPosIndex = groupingNewPosIndexParent; 1705 } 1706 } 1707 1708 beginMoveRows(QModelIndex(), (row - offset), (row - offset) + extraChildCount, QModelIndex(), (newPos > row) ? newPos + 1 : newPos); 1709 1710 row = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingRowIndex)).row()); 1711 newPos = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingNewPosIndex)).row()); 1712 1713 // Update sort mappings. 1714 d->sortedPreFilterRows.move(row, newPos); 1715 1716 endMoveRows(); 1717 1718 if (groupingRowIndexParent.isValid()) { 1719 d->consolidateManualSortMapForGroup(groupingRowIndexParent); 1720 } 1721 1722 } else { 1723 beginMoveRows(parent, row, row, parent, (newPos > row) ? newPos + 1 : newPos); 1724 1725 // Translate to sort map indices. 1726 const QModelIndex &groupingRowIndex = mapToSource(index(row, 0, parent)); 1727 const QModelIndex &preFilterRowIndex = d->preFilterIndex(groupingRowIndex); 1728 1729 const bool groupNotDisabled = !parent.isValid() && groupMode() != GroupDisabled; 1730 QModelIndex adjacentGroupingRowIndex; // Also consolidate the adjacent group parent 1731 if (groupNotDisabled) { 1732 if (newPos > row && row + 1 < rowCount(parent)) { 1733 adjacentGroupingRowIndex = mapToSource(index(row + 1, 0, parent) /* task on the right */); 1734 } else if (newPos < row && row - 1 >= 0) { 1735 adjacentGroupingRowIndex = mapToSource(index(row - 1, 0, parent) /* task on the left */); 1736 } 1737 } 1738 1739 row = d->sortedPreFilterRows.indexOf(preFilterRowIndex.row()); 1740 newPos = d->sortedPreFilterRows.indexOf(d->preFilterIndex(mapToSource(index(newPos, 0, parent))).row()); 1741 1742 // Update sort mapping. 1743 d->sortedPreFilterRows.move(row, newPos); 1744 1745 endMoveRows(); 1746 1747 // If we moved a group parent, consolidate sort map for children. 1748 if (groupNotDisabled) { 1749 if (d->groupingProxyModel->rowCount(groupingRowIndex)) { 1750 d->consolidateManualSortMapForGroup(groupingRowIndex); 1751 } 1752 // Special case: Before moving, the task at newPos is a group parent 1753 // Before moving: [Task] [Group parent] [Other task in group] 1754 // After moving: [Group parent (not consolidated yet)] [Task, newPos] [Other task in group] 1755 if (int childCount = d->groupingProxyModel->rowCount(adjacentGroupingRowIndex); childCount && adjacentGroupingRowIndex.isValid()) { 1756 d->consolidateManualSortMapForGroup(adjacentGroupingRowIndex); 1757 if (newPos > row) { 1758 newPos += childCount - 1; 1759 // After consolidation: [Group parent (not consolidated yet)] [Other task in group] [Task, newPos] 1760 } 1761 // No need to consider newPos < row 1762 // Before moving: [Group parent, newPos] [Other task in group] [Task] 1763 // After moving: [Task, newPos] [Group parent] [Other task in group] 1764 } 1765 } 1766 } 1767 1768 // Resort. 1769 d->forceResort(); 1770 1771 if (!d->separateLaunchers) { 1772 if (isLauncherMove) { 1773 const QModelIndex &idx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos), 0); 1774 const QUrl &launcherUrl = idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); 1775 1776 // Move launcher for launcher-backed task along with task if launchers 1777 // are not being kept separate. 1778 // We don't need to resort again because the launcher is implicitly hidden 1779 // at this time. 1780 if (!idx.data(AbstractTasksModel::IsLauncher).toBool()) { 1781 const int launcherPos = d->launcherTasksModel->launcherPosition(launcherUrl); 1782 const QModelIndex &launcherIndex = d->launcherTasksModel->index(launcherPos, 0); 1783 const int sortIndex = d->sortedPreFilterRows.indexOf(d->concatProxyModel->mapFromSource(launcherIndex).row()); 1784 d->sortedPreFilterRows.move(sortIndex, newPos); 1785 1786 if (row > newPos && newPos >= 1) { 1787 const QModelIndex beforeIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos - 1), 0); 1788 if (beforeIdx.data(AbstractTasksModel::IsLauncher).toBool()) { 1789 // Search forward to skip grouped tasks 1790 int afterPos = newPos + 1; 1791 for (; afterPos < d->sortedPreFilterRows.size(); ++afterPos) { 1792 const QModelIndex tempIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(afterPos), 0); 1793 if (!appsMatch(idx, tempIdx)) { 1794 break; 1795 } 1796 } 1797 1798 const QModelIndex afterIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(afterPos), 0); 1799 if (appsMatch(beforeIdx, afterIdx)) { 1800 d->sortedPreFilterRows.move(newPos - 1, afterPos - 1); 1801 } 1802 } 1803 } 1804 // Otherwise move matching windows to after the launcher task (they are 1805 // currently hidden but might be on another virtual desktop). 1806 } else { 1807 for (int i = (d->sortedPreFilterRows.count() - 1); i >= 0; --i) { 1808 const QModelIndex &concatProxyIndex = d->concatProxyModel->index(d->sortedPreFilterRows.at(i), 0); 1809 1810 if (launcherUrl == concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()) { 1811 d->sortedPreFilterRows.move(i, newPos); 1812 1813 if (newPos > i) { 1814 --newPos; 1815 } 1816 } 1817 } 1818 } 1819 } else if (newPos > 0 && newPos < d->sortedPreFilterRows.size() - 1) { 1820 /* 1821 * When dragging an unpinned task, a pinned task can also be moved. 1822 * In this case, sortedPreFilterRows is like: 1823 * - before moving: [pinned 1 (launcher item)] [pinned 1 (window)] [unpinned] 1824 * - after moving: [pinned 1 (launcher item)] [unpinned] [pinned 1 (window)] 1825 * So also check the indexes before and after the unpinned task. 1826 */ 1827 const QModelIndex beforeIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos - 1), 0, parent); 1828 const QModelIndex afterIdx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos + 1), 0, parent); 1829 // BUG 462508: check if any item is a launcher 1830 const bool hasLauncher = beforeIdx.data(AbstractTasksModel::IsLauncher).toBool() || afterIdx.data(AbstractTasksModel::IsLauncher).toBool(); 1831 1832 if (hasLauncher && appsMatch(beforeIdx, afterIdx)) { 1833 // after adjusting: [unpinned] [pinned 1 (launcher item)] [pinned 1] 1834 d->sortedPreFilterRows.move(newPos, newPos + (row < newPos ? 1 : -1)); 1835 } 1836 } 1837 } 1838 1839 // Setup for syncLaunchers(). 1840 d->launcherSortingDirty = isLauncherMove; 1841 1842 return true; 1843 } 1844 1845 void TasksModel::syncLaunchers() 1846 { 1847 // Writes the launcher order exposed through the model back to the launcher 1848 // tasks model, committing any move() operations to persistent state. 1849 1850 if (!d->launcherTasksModel || !d->launcherSortingDirty) { 1851 return; 1852 } 1853 1854 QMap<int, QString> sortedShownLaunchers; 1855 QStringList sortedHiddenLaunchers; 1856 1857 foreach (const QString &launcherUrlStr, launcherList()) { 1858 int row = -1; 1859 QStringList activities; 1860 QUrl launcherUrl; 1861 1862 std::tie(launcherUrl, activities) = deserializeLauncher(launcherUrlStr); 1863 1864 for (int i = 0; i < rowCount(); ++i) { 1865 const QUrl &rowLauncherUrl = index(i, 0).data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); 1866 1867 // `LauncherTasksModel::launcherList()` returns data in a format suitable for writing 1868 // to persistent configuration storage, e.g. `preferred://browser`. We mean to compare 1869 // this last "save state" to a higher, resolved URL representation to compute the delta 1870 // so we need to move the unresolved URLs through `TaskTools::appDataFromUrl()` first. 1871 // TODO: This bypasses an existing lookup cache for the resolved app data that exists 1872 // in LauncherTasksModel. It's likely a good idea to eventually move these caches out 1873 // of the various models and share them among users of `TaskTools::appDataFromUrl()`, 1874 // and then also do resolution implicitly in `TaskTools::launcherUrlsMatch`, to speed 1875 // things up slightly and make the models simpler (central cache eviction, ...). 1876 if (launcherUrlsMatch(appDataFromUrl(launcherUrl).url, rowLauncherUrl, IgnoreQueryItems)) { 1877 row = i; 1878 break; 1879 } 1880 } 1881 1882 if (row != -1) { 1883 sortedShownLaunchers.insert(row, launcherUrlStr); 1884 } else { 1885 sortedHiddenLaunchers << launcherUrlStr; 1886 } 1887 } 1888 1889 // Prep sort map for source model data changes. 1890 if (d->sortMode == SortManual) { 1891 QVector<int> sortMapIndices; 1892 QVector<int> preFilterRows; 1893 1894 for (int i = 0; i < d->launcherTasksModel->rowCount(); ++i) { 1895 const QModelIndex &launcherIndex = d->launcherTasksModel->index(i, 0); 1896 const QModelIndex &concatIndex = d->concatProxyModel->mapFromSource(launcherIndex); 1897 sortMapIndices << d->sortedPreFilterRows.indexOf(concatIndex.row()); 1898 preFilterRows << concatIndex.row(); 1899 } 1900 1901 // We're going to write back launcher model entries in the sort 1902 // map in concat model order, matching the reordered launcher list 1903 // we're about to pass down. 1904 std::sort(sortMapIndices.begin(), sortMapIndices.end()); 1905 1906 for (int i = 0; i < sortMapIndices.count(); ++i) { 1907 d->sortedPreFilterRows.replace(sortMapIndices.at(i), preFilterRows.at(i)); 1908 } 1909 } 1910 1911 setLauncherList(sortedShownLaunchers.values() + sortedHiddenLaunchers); 1912 1913 // The accepted rows are outdated after the item order is changed 1914 invalidateFilter(); 1915 d->forceResort(); 1916 1917 d->launcherSortingDirty = false; 1918 } 1919 1920 QModelIndex TasksModel::activeTask() const 1921 { 1922 for (int i = 0; i < rowCount(); ++i) { 1923 const QModelIndex &idx = index(i, 0); 1924 1925 if (idx.data(AbstractTasksModel::IsActive).toBool()) { 1926 if (groupMode() != GroupDisabled && rowCount(idx)) { 1927 for (int j = 0; j < rowCount(idx); ++j) { 1928 const QModelIndex &child = index(j, 0, idx); 1929 1930 if (child.data(AbstractTasksModel::IsActive).toBool()) { 1931 return child; 1932 } 1933 } 1934 } else { 1935 return idx; 1936 } 1937 } 1938 } 1939 1940 return QModelIndex(); 1941 } 1942 1943 QModelIndex TasksModel::makeModelIndex(int row, int childRow) const 1944 { 1945 if (row < 0 || row >= rowCount()) { 1946 return QModelIndex(); 1947 } 1948 1949 if (childRow == -1) { 1950 return index(row, 0); 1951 } else { 1952 const QModelIndex &parent = index(row, 0); 1953 1954 if (childRow < rowCount(parent)) { 1955 return index(childRow, 0, parent); 1956 } 1957 } 1958 1959 return QModelIndex(); 1960 } 1961 1962 QPersistentModelIndex TasksModel::makePersistentModelIndex(int row, int childCount) const 1963 { 1964 return QPersistentModelIndex(makeModelIndex(row, childCount)); 1965 } 1966 1967 void TasksModel::classBegin() 1968 { 1969 d->usedByQml = true; 1970 } 1971 1972 void TasksModel::componentComplete() 1973 { 1974 d->componentComplete = true; 1975 1976 // Sets our source model, populating the model. 1977 d->updateGroupInline(); 1978 } 1979 1980 bool TasksModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const 1981 { 1982 // All our filtering occurs at the top-level; anything below always 1983 // goes through. 1984 if (sourceParent.isValid()) { 1985 return true; 1986 } 1987 1988 const QModelIndex &sourceIndex = sourceModel()->index(sourceRow, 0); 1989 1990 // In inline grouping mode, filter out group parents. 1991 if (d->groupInline && d->flattenGroupsProxyModel && sourceIndex.data(AbstractTasksModel::IsGroupParent).toBool()) { 1992 return false; 1993 } 1994 1995 const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString(); 1996 const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString(); 1997 1998 // Filter startup tasks we already have a window task for. 1999 if (sourceIndex.data(AbstractTasksModel::IsStartup).toBool()) { 2000 for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) { 2001 const QModelIndex &filterIndex = d->filterProxyModel->index(i, 0); 2002 2003 if (!filterIndex.data(AbstractTasksModel::IsWindow).toBool()) { 2004 continue; 2005 } 2006 2007 if ((!appId.isEmpty() && appId == filterIndex.data(AbstractTasksModel::AppId).toString()) 2008 || (!appName.isEmpty() && appName == filterIndex.data(AbstractTasksModel::AppName).toString())) { 2009 return false; 2010 } 2011 } 2012 } 2013 2014 // Filter launcher tasks we already have a startup or window task for (that 2015 // got through filtering). 2016 if (sourceIndex.data(AbstractTasksModel::IsLauncher).toBool()) { 2017 for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) { 2018 const QModelIndex &filteredIndex = d->filterProxyModel->index(i, 0); 2019 2020 if (!filteredIndex.data(AbstractTasksModel::IsWindow).toBool() && !filteredIndex.data(AbstractTasksModel::IsStartup).toBool()) { 2021 continue; 2022 } 2023 2024 if (appsMatch(sourceIndex, filteredIndex)) { 2025 return false; 2026 } 2027 } 2028 } 2029 2030 return true; 2031 } 2032 2033 bool TasksModel::lessThan(const QModelIndex &left, const QModelIndex &right) const 2034 { 2035 // In manual sort mode, sort by map. 2036 if (d->sortMode == SortManual) { 2037 return (d->sortedPreFilterRows.indexOf(d->preFilterIndex(left).row()) < d->sortedPreFilterRows.indexOf(d->preFilterIndex(right).row())); 2038 } 2039 2040 return d->lessThan(left, right); 2041 } 2042 2043 }