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 }