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 }