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