/* SPDX-FileCopyrightText: 2016 Eike Hein SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL */ #include "tasksmodel.h" #include "activityinfo.h" #include "concatenatetasksproxymodel.h" #include "flattentaskgroupsproxymodel.h" #include "taskfilterproxymodel.h" #include "taskgroupingproxymodel.h" #include "tasktools.h" #include "virtualdesktopinfo.h" #include "launchertasksmodel.h" #include "startuptasksmodel.h" #include "windowtasksmodel.h" #include "launchertasksmodel_p.h" #include #include #include #include namespace TaskManager { class Q_DECL_HIDDEN TasksModel::Private { public: Private(TasksModel *q); ~Private(); static int instanceCount; static WindowTasksModel *windowTasksModel; static StartupTasksModel *startupTasksModel; LauncherTasksModel *launcherTasksModel = nullptr; ConcatenateTasksProxyModel *concatProxyModel = nullptr; TaskFilterProxyModel *filterProxyModel = nullptr; TaskGroupingProxyModel *groupingProxyModel = nullptr; FlattenTaskGroupsProxyModel *flattenGroupsProxyModel = nullptr; AbstractTasksModelIface *abstractTasksSourceModel = nullptr; bool anyTaskDemandsAttention = false; int launcherCount = 0; SortMode sortMode = SortAlpha; bool separateLaunchers = true; bool launchInPlace = false; bool launchersEverSet = false; bool launcherSortingDirty = false; bool launcherCheckNeeded = false; QList sortedPreFilterRows; QVector sortRowInsertQueue; bool sortRowInsertQueueStale = false; QHash activityTaskCounts; static VirtualDesktopInfo *virtualDesktopInfo; static int virtualDesktopInfoUsers; static ActivityInfo *activityInfo; static int activityInfoUsers; bool groupInline = false; int groupingWindowTasksThreshold = -1; bool usedByQml = false; bool componentComplete = false; void initModels(); void initLauncherTasksModel(); void updateAnyTaskDemandsAttention(); void updateManualSortMap(); void consolidateManualSortMapForGroup(const QModelIndex &groupingProxyIndex); void updateGroupInline(); QModelIndex preFilterIndex(const QModelIndex &sourceIndex) const; void updateActivityTaskCounts(); void forceResort(); bool lessThan(const QModelIndex &left, const QModelIndex &right, bool sortOnlyLaunchers = false) const; private: TasksModel *q; }; class TasksModel::TasksModelLessThan { public: inline TasksModelLessThan(const QAbstractItemModel *s, TasksModel *p, bool sortOnlyLaunchers) : sourceModel(s) , tasksModel(p) , sortOnlyLaunchers(sortOnlyLaunchers) { } inline bool operator()(int r1, int r2) const { QModelIndex i1 = sourceModel->index(r1, 0); QModelIndex i2 = sourceModel->index(r2, 0); return tasksModel->d->lessThan(i1, i2, sortOnlyLaunchers); } private: const QAbstractItemModel *sourceModel; const TasksModel *tasksModel; bool sortOnlyLaunchers; }; int TasksModel::Private::instanceCount = 0; WindowTasksModel *TasksModel::Private::windowTasksModel = nullptr; StartupTasksModel *TasksModel::Private::startupTasksModel = nullptr; VirtualDesktopInfo *TasksModel::Private::virtualDesktopInfo = nullptr; int TasksModel::Private::virtualDesktopInfoUsers = 0; ActivityInfo *TasksModel::Private::activityInfo = nullptr; int TasksModel::Private::activityInfoUsers = 0; TasksModel::Private::Private(TasksModel *q) : q(q) { ++instanceCount; } TasksModel::Private::~Private() { --instanceCount; if (sortMode == SortActivity) { --activityInfoUsers; } if (!instanceCount) { delete windowTasksModel; windowTasksModel = nullptr; delete startupTasksModel; startupTasksModel = nullptr; delete virtualDesktopInfo; virtualDesktopInfo = nullptr; delete activityInfo; activityInfo = nullptr; } } void TasksModel::Private::initModels() { // NOTE: Overview over the entire model chain assembled here: // WindowTasksModel, StartupTasksModel, LauncherTasksModel // -> concatProxyModel concatenates them into a single list. // -> filterProxyModel filters by state (e.g. virtual desktop). // -> groupingProxyModel groups by application (we go from flat list to tree). // -> flattenGroupsProxyModel (optionally, if groupInline == true) flattens groups out. // -> TasksModel collapses (top-level) items into task lifecycle abstraction; sorts. if (!windowTasksModel) { windowTasksModel = new WindowTasksModel(); } QObject::connect(windowTasksModel, &QAbstractItemModel::rowsInserted, q, [this]() { if (sortMode == SortActivity) { updateActivityTaskCounts(); } }); QObject::connect(windowTasksModel, &QAbstractItemModel::rowsRemoved, q, [this]() { if (sortMode == SortActivity) { updateActivityTaskCounts(); forceResort(); } }); QObject::connect(windowTasksModel, &QAbstractItemModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { Q_UNUSED(topLeft) Q_UNUSED(bottomRight) if (sortMode == SortActivity && roles.contains(AbstractTasksModel::Activities)) { updateActivityTaskCounts(); } if (roles.contains(AbstractTasksModel::IsActive)) { Q_EMIT q->activeTaskChanged(); } // In manual sort mode, updateManualSortMap() may consult the sortRowInsertQueue // for new tasks to sort in. Hidden tasks remain in the queue to potentially sort // them later, when they are are actually revealed to the user. // This is particularly useful in concert with taskmanagerrulesrc's SkipTaskbar // key, which is used to hide window tasks which update from bogus to useful // window metadata early in startup. The role change then coincides with positive // app identification, which is when updateManualSortMap() becomes able to sort the // task adjacent to its launcher when required to do so. if (sortMode == SortManual && roles.contains(AbstractTasksModel::SkipTaskbar)) { updateManualSortMap(); } }); if (!startupTasksModel) { startupTasksModel = new StartupTasksModel(); } concatProxyModel = new ConcatenateTasksProxyModel(q); concatProxyModel->addSourceModel(windowTasksModel); concatProxyModel->addSourceModel(startupTasksModel); // If we're in manual sort mode, we need to seed the sort map on pending row // insertions. QObject::connect(concatProxyModel, &QAbstractItemModel::rowsAboutToBeInserted, q, [this](const QModelIndex &parent, int start, int end) { Q_UNUSED(parent) if (sortMode != SortManual) { return; } const int delta = (end - start) + 1; QMutableListIterator it(sortedPreFilterRows); while (it.hasNext()) { it.next(); if (it.value() >= start) { it.setValue(it.value() + delta); } } for (int i = start; i <= end; ++i) { sortedPreFilterRows.append(i); if (!separateLaunchers) { if (sortRowInsertQueueStale) { sortRowInsertQueue.clear(); sortRowInsertQueueStale = false; } sortRowInsertQueue.append(sortedPreFilterRows.count() - 1); } } }); // If we're in manual sort mode, we need to update the sort map on row insertions. QObject::connect(concatProxyModel, &QAbstractItemModel::rowsInserted, q, [this](const QModelIndex &parent, int start, int end) { Q_UNUSED(parent) Q_UNUSED(start) Q_UNUSED(end) if (sortMode == SortManual) { updateManualSortMap(); } }); // If we're in manual sort mode, we need to update the sort map after row removals. QObject::connect(concatProxyModel, &QAbstractItemModel::rowsRemoved, q, [this](const QModelIndex &parent, int first, int last) { Q_UNUSED(parent) if (sortMode != SortManual) { return; } if (sortRowInsertQueueStale) { sortRowInsertQueue.clear(); sortRowInsertQueueStale = false; } for (int i = first; i <= last; ++i) { sortedPreFilterRows.removeOne(i); } const int delta = (last - first) + 1; QMutableListIterator it(sortedPreFilterRows); while (it.hasNext()) { it.next(); if (it.value() > last) { it.setValue(it.value() - delta); } } }); filterProxyModel = new TaskFilterProxyModel(q); filterProxyModel->setSourceModel(concatProxyModel); QObject::connect(filterProxyModel, &TaskFilterProxyModel::virtualDesktopChanged, q, &TasksModel::virtualDesktopChanged); QObject::connect(filterProxyModel, &TaskFilterProxyModel::screenGeometryChanged, q, &TasksModel::screenGeometryChanged); QObject::connect(filterProxyModel, &TaskFilterProxyModel::activityChanged, q, &TasksModel::activityChanged); QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByVirtualDesktopChanged, q, &TasksModel::filterByVirtualDesktopChanged); QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByScreenChanged, q, &TasksModel::filterByScreenChanged); QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterByActivityChanged, q, &TasksModel::filterByActivityChanged); QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterNotMinimizedChanged, q, &TasksModel::filterNotMinimizedChanged); QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterNotMaximizedChanged, q, &TasksModel::filterNotMaximizedChanged); QObject::connect(filterProxyModel, &TaskFilterProxyModel::filterHiddenChanged, q, &TasksModel::filterHiddenChanged); groupingProxyModel = new TaskGroupingProxyModel(q); groupingProxyModel->setSourceModel(filterProxyModel); QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::groupModeChanged, q, &TasksModel::groupModeChanged); QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedAppIdsChanged, q, &TasksModel::groupingAppIdBlacklistChanged); QObject::connect(groupingProxyModel, &TaskGroupingProxyModel::blacklistedLauncherUrlsChanged, q, &TasksModel::groupingLauncherUrlBlacklistChanged); QObject::connect(groupingProxyModel, &QAbstractItemModel::rowsInserted, q, [this](const QModelIndex &parent, int first, int last) { if (parent.isValid()) { if (sortMode == SortManual) { consolidateManualSortMapForGroup(parent); } // Existence of a group means everything below this has already been done. return; } bool demandsAttentionUpdateNeeded = false; for (int i = first; i <= last; ++i) { const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0); const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString(); if (sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) { demandsAttentionUpdateNeeded = true; } // When we get a window we have a startup for, cause the startup to be re-filtered. if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) { const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString(); for (int i = 0; i < filterProxyModel->rowCount(); ++i) { QModelIndex filterIndex = filterProxyModel->index(i, 0); if (!filterIndex.data(AbstractTasksModel::IsStartup).toBool()) { continue; } if ((!appId.isEmpty() && appId == filterIndex.data(AbstractTasksModel::AppId).toString()) || (!appName.isEmpty() && appName == filterIndex.data(AbstractTasksModel::AppName).toString())) { Q_EMIT filterProxyModel->dataChanged(filterIndex, filterIndex); } } } // When we get a window or startup we have a launcher for, cause the launcher to be re-filtered. if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool() || sourceIndex.data(AbstractTasksModel::IsStartup).toBool()) { for (int i = 0; i < filterProxyModel->rowCount(); ++i) { const QModelIndex &filterIndex = filterProxyModel->index(i, 0); if (!filterIndex.data(AbstractTasksModel::IsLauncher).toBool()) { continue; } if (appsMatch(sourceIndex, filterIndex)) { Q_EMIT filterProxyModel->dataChanged(filterIndex, filterIndex); } } } } if (!anyTaskDemandsAttention && demandsAttentionUpdateNeeded) { updateAnyTaskDemandsAttention(); } }); QObject::connect(groupingProxyModel, &QAbstractItemModel::rowsAboutToBeRemoved, q, [this](const QModelIndex &parent, int first, int last) { // We can ignore group members. if (parent.isValid()) { return; } for (int i = first; i <= last; ++i) { const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0); // When a window or startup task is removed, we have to trigger a re-filter of // our launchers to (possibly) pop them back in. // NOTE: An older revision of this code compared the window and startup tasks // to the launchers to figure out which launchers should be re-filtered. This // was fine until we discovered that certain applications (e.g. Google Chrome) // change their window metadata specifically during tear-down, sometimes // breaking TaskTools::appsMatch (it's a race) and causing the associated // launcher to remain hidden. Therefore we now consider any top-level window or // startup task removal a trigger to re-filter all launchers. We don't do this // in response to the window metadata changes (even though it would be strictly // more correct, as then-ending identity match-up was what caused the launcher // to be hidden) because we don't want the launcher and window/startup task to // briefly co-exist in the model. if (!launcherCheckNeeded && launcherTasksModel && (sourceIndex.data(AbstractTasksModel::IsWindow).toBool() || sourceIndex.data(AbstractTasksModel::IsStartup).toBool())) { launcherCheckNeeded = true; } } }); QObject::connect(filterProxyModel, &QAbstractItemModel::rowsRemoved, q, [this](const QModelIndex &parent, int first, int last) { Q_UNUSED(parent) Q_UNUSED(first) Q_UNUSED(last) if (launcherCheckNeeded) { for (int i = 0; i < filterProxyModel->rowCount(); ++i) { const QModelIndex &idx = filterProxyModel->index(i, 0); if (idx.data(AbstractTasksModel::IsLauncher).toBool()) { Q_EMIT filterProxyModel->dataChanged(idx, idx); } } launcherCheckNeeded = false; } // One of the removed tasks might have been demanding attention, but // we can't check the state after the window has been closed already, // so we always have to do a full update. if (anyTaskDemandsAttention) { updateAnyTaskDemandsAttention(); } }); // Update anyTaskDemandsAttention on source data changes. QObject::connect(groupingProxyModel, &QAbstractItemModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { Q_UNUSED(bottomRight) // We can ignore group members. if (topLeft.parent().isValid()) { return; } if (roles.isEmpty() || roles.contains(AbstractTasksModel::IsDemandingAttention)) { updateAnyTaskDemandsAttention(); } if (roles.isEmpty() || roles.contains(AbstractTasksModel::AppId)) { for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { const QModelIndex &sourceIndex = groupingProxyModel->index(i, 0); // When a window task changes identity to one we have a launcher for, cause // the launcher to be re-filtered. if (sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) { for (int i = 0; i < filterProxyModel->rowCount(); ++i) { const QModelIndex &filterIndex = filterProxyModel->index(i, 0); if (!filterIndex.data(AbstractTasksModel::IsLauncher).toBool()) { continue; } if (appsMatch(sourceIndex, filterIndex)) { Q_EMIT filterProxyModel->dataChanged(filterIndex, filterIndex); } } } } } }); // Update anyTaskDemandsAttention on source model resets. QObject::connect(groupingProxyModel, &QAbstractItemModel::modelReset, q, [this]() { updateAnyTaskDemandsAttention(); }); } void TasksModel::Private::updateAnyTaskDemandsAttention() { bool taskFound = false; for (int i = 0; i < groupingProxyModel->rowCount(); ++i) { if (groupingProxyModel->index(i, 0).data(AbstractTasksModel::IsDemandingAttention).toBool()) { taskFound = true; break; } } if (taskFound != anyTaskDemandsAttention) { anyTaskDemandsAttention = taskFound; Q_EMIT q->anyTaskDemandsAttentionChanged(); } } void TasksModel::Private::initLauncherTasksModel() { if (launcherTasksModel) { return; } launcherTasksModel = new LauncherTasksModel(q); QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, q, &TasksModel::launcherListChanged); QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, q, &TasksModel::updateLauncherCount); // TODO: On the assumptions that adding/removing launchers is a rare event and // the HasLaunchers data role is rarely used, this refreshes it for all rows in // the model. If those assumptions are proven wrong later, this could be // optimized to only refresh non-launcher rows matching the inserted or about- // to-be-removed launcherTasksModel rows using TaskTools::appsMatch(). QObject::connect(launcherTasksModel, &LauncherTasksModel::launcherListChanged, q, [this]() { Q_EMIT q->dataChanged(q->index(0, 0), q->index(q->rowCount() - 1, 0), QVector{AbstractTasksModel::HasLauncher}); }); // data() implements AbstractTasksModel::HasLauncher by checking with // TaskTools::appsMatch, which evaluates ::AppId and ::LauncherUrlWithoutIcon. QObject::connect(q, &QAbstractItemModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { if (roles.contains(AbstractTasksModel::AppId) || roles.contains(AbstractTasksModel::LauncherUrlWithoutIcon)) { for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { const QModelIndex &index = q->index(i, 0); if (!index.data(AbstractTasksModel::IsLauncher).toBool()) { Q_EMIT q->dataChanged(index, index, QVector{AbstractTasksModel::HasLauncher}); } } } }); concatProxyModel->addSourceModel(launcherTasksModel); } void TasksModel::Private::updateManualSortMap() { // Empty map; full sort. if (sortedPreFilterRows.isEmpty()) { sortedPreFilterRows.reserve(concatProxyModel->rowCount()); for (int i = 0; i < concatProxyModel->rowCount(); ++i) { sortedPreFilterRows.append(i); } // Full sort. TasksModelLessThan lt(concatProxyModel, q, false); std::stable_sort(sortedPreFilterRows.begin(), sortedPreFilterRows.end(), lt); // Consolidate sort map entries for groups. if (q->groupMode() != GroupDisabled) { for (int i = 0; i < groupingProxyModel->rowCount(); ++i) { const QModelIndex &groupingIndex = groupingProxyModel->index(i, 0); if (groupingIndex.data(AbstractTasksModel::IsGroupParent).toBool()) { consolidateManualSortMapForGroup(groupingIndex); } } } return; } // Existing map; check whether launchers need sorting by launcher list position. if (separateLaunchers) { // Sort only launchers. TasksModelLessThan lt(concatProxyModel, q, true); std::stable_sort(sortedPreFilterRows.begin(), sortedPreFilterRows.end(), lt); // Otherwise process any entries in the insert queue and move them intelligently // in the sort map. } else { QMutableVectorIterator i(sortRowInsertQueue); while (i.hasNext()) { i.next(); const int row = i.value(); const QModelIndex &idx = concatProxyModel->index(sortedPreFilterRows.at(row), 0); // If a window task is currently hidden, we may want to keep it in the queue // to sort it in later once it gets revealed. // This is important in concert with taskmanagerrulesrc's SkipTaskbar key, which // is used to hide window tasks which update from bogus to useful window metadata // early in startup. Once the task no longer uses bogus metadata listed in the // config key, its SkipTaskbar role changes to false, and then is it possible to // sort the task adjacent to its launcher in the code below. if (idx.data(AbstractTasksModel::IsWindow).toBool() && idx.data(AbstractTasksModel::SkipTaskbar).toBool()) { // Since we're going to keep a row in the queue for now, make sure to // mark the queue as stale so it's cleared on appends or row removals // when they follow this sorting attempt. This frees us from having to // update the indices in the queue to keep them valid. // This means windowing system changes such as the opening or closing // of a window task which happen during the time period that a window // task has known bogus metadata, can upset what we're trying to // achieve with this exception. However, due to the briefness of the // time period and usage patterns, this is improbable, making this // likely good enough. If it turns out not to be, this decision may be // revisited later. sortRowInsertQueueStale = true; break; } else { i.remove(); } bool moved = false; // Try to move the task up to its right-most app sibling, unless this // is us sorting in a launcher list for the first time. if (launchersEverSet && !idx.data(AbstractTasksModel::IsLauncher).toBool()) { for (int i = (row - 1); i >= 0; --i) { const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(i), 0); // Once we got a match, check if the filter model accepts the potential // sibling. We don't want to sort new tasks in next to tasks it will // filter out once it sees it anyway. if (appsMatch(concatProxyIndex, idx) && filterProxyModel->acceptsRow(concatProxyIndex.row())) { sortedPreFilterRows.move(row, i + 1); moved = true; break; } } } int insertPos = 0; // If unsuccessful or skipped, and the new task is a launcher, put after // the rightmost launcher or launcher-backed task in the map, or failing // that at the start of the map. if (!moved && idx.data(AbstractTasksModel::IsLauncher).toBool()) { for (int i = 0; i < row; ++i) { const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(i), 0); if (concatProxyIndex.data(AbstractTasksModel::IsLauncher).toBool() || launcherTasksModel->launcherPosition(concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()) != -1) { insertPos = i + 1; } else { break; } } sortedPreFilterRows.move(row, insertPos); moved = true; } // If we sorted in a launcher and it's the first time we're sorting in a // launcher list, move existing windows to the launcher position now. if (moved && !launchersEverSet) { for (int i = (sortedPreFilterRows.count() - 1); i >= 0; --i) { const QModelIndex &concatProxyIndex = concatProxyModel->index(sortedPreFilterRows.at(i), 0); if (!concatProxyIndex.data(AbstractTasksModel::IsLauncher).toBool() && idx.data(AbstractTasksModel::LauncherUrlWithoutIcon) == concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon)) { sortedPreFilterRows.move(i, insertPos); if (insertPos > i) { --insertPos; } } } } } } } void TasksModel::Private::consolidateManualSortMapForGroup(const QModelIndex &groupingProxyIndex) { // Consolidates sort map entries for a group's items to be contiguous // after the group's first item and the same order as in groupingProxyModel. const int childCount = groupingProxyModel->rowCount(groupingProxyIndex); if (!childCount) { return; } const QModelIndex &leader = groupingProxyModel->index(0, 0, groupingProxyIndex); const QModelIndex &preFilterLeader = filterProxyModel->mapToSource(groupingProxyModel->mapToSource(leader)); // We're moving the trailing children to the sort map position of // the first child, so we're skipping the first child. for (int i = 1; i < childCount; ++i) { const QModelIndex &child = groupingProxyModel->index(i, 0, groupingProxyIndex); const QModelIndex &preFilterChild = filterProxyModel->mapToSource(groupingProxyModel->mapToSource(child)); const int leaderPos = sortedPreFilterRows.indexOf(preFilterLeader.row()); const int childPos = sortedPreFilterRows.indexOf(preFilterChild.row()); const int insertPos = (leaderPos + i) + ((leaderPos + i) > childPos ? -1 : 0); sortedPreFilterRows.move(childPos, insertPos); } } void TasksModel::Private::updateGroupInline() { if (usedByQml && !componentComplete) { return; } bool hadSourceModel = (q->sourceModel() != nullptr); if (q->groupMode() != GroupDisabled && groupInline) { if (flattenGroupsProxyModel) { return; } // Exempting tasks which demand attention from grouping is not // necessary when all group children are shown inline anyway // and would interfere with our sort-tasks-together goals. groupingProxyModel->setGroupDemandingAttention(true); // Likewise, ignore the window tasks threshold when making // grouping decisions. groupingProxyModel->setWindowTasksThreshold(-1); flattenGroupsProxyModel = new FlattenTaskGroupsProxyModel(q); flattenGroupsProxyModel->setSourceModel(groupingProxyModel); abstractTasksSourceModel = flattenGroupsProxyModel; q->setSourceModel(flattenGroupsProxyModel); if (sortMode == SortManual) { forceResort(); } } else { if (hadSourceModel && !flattenGroupsProxyModel) { return; } groupingProxyModel->setGroupDemandingAttention(false); groupingProxyModel->setWindowTasksThreshold(groupingWindowTasksThreshold); abstractTasksSourceModel = groupingProxyModel; q->setSourceModel(groupingProxyModel); delete flattenGroupsProxyModel; flattenGroupsProxyModel = nullptr; if (hadSourceModel && sortMode == SortManual) { forceResort(); } } // Minor optimization: We only make these connections after we populate for // the first time to avoid some churn. if (!hadSourceModel) { QObject::connect(q, &QAbstractItemModel::rowsInserted, q, &TasksModel::updateLauncherCount, Qt::UniqueConnection); QObject::connect(q, &QAbstractItemModel::rowsRemoved, q, &TasksModel::updateLauncherCount, Qt::UniqueConnection); QObject::connect(q, &QAbstractItemModel::modelReset, q, &TasksModel::updateLauncherCount, Qt::UniqueConnection); QObject::connect(q, &QAbstractItemModel::rowsInserted, q, &TasksModel::countChanged, Qt::UniqueConnection); QObject::connect(q, &QAbstractItemModel::rowsRemoved, q, &TasksModel::countChanged, Qt::UniqueConnection); QObject::connect(q, &QAbstractItemModel::modelReset, q, &TasksModel::countChanged, Qt::UniqueConnection); } } QModelIndex TasksModel::Private::preFilterIndex(const QModelIndex &sourceIndex) const { // Only in inline grouping mode, we have an additional proxy layer. if (flattenGroupsProxyModel) { return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(flattenGroupsProxyModel->mapToSource(sourceIndex))); } else { return filterProxyModel->mapToSource(groupingProxyModel->mapToSource(sourceIndex)); } } void TasksModel::Private::updateActivityTaskCounts() { // Collects the number of window tasks on each activity. activityTaskCounts.clear(); if (!windowTasksModel || !activityInfo) { return; } foreach (const QString &activity, activityInfo->runningActivities()) { activityTaskCounts.insert(activity, 0); } for (int i = 0; i < windowTasksModel->rowCount(); ++i) { const QModelIndex &windowIndex = windowTasksModel->index(i, 0); const QStringList &activities = windowIndex.data(AbstractTasksModel::Activities).toStringList(); if (activities.isEmpty()) { QMutableHashIterator i(activityTaskCounts); while (i.hasNext()) { i.next(); i.setValue(i.value() + 1); } } else { foreach (const QString &activity, activities) { ++activityTaskCounts[activity]; } } } } void TasksModel::Private::forceResort() { // HACK: This causes QSortFilterProxyModel to run all rows through // our lessThan() implementation again. q->setDynamicSortFilter(false); q->setDynamicSortFilter(true); } bool TasksModel::Private::lessThan(const QModelIndex &left, const QModelIndex &right, bool sortOnlyLaunchers) const { // Launcher tasks go first. // When launchInPlace is enabled, startup and window tasks are sorted // as the launchers they replace (see also move()). if (separateLaunchers) { if (left.data(AbstractTasksModel::IsLauncher).toBool() && right.data(AbstractTasksModel::IsLauncher).toBool()) { return (left.row() < right.row()); } else if (left.data(AbstractTasksModel::IsLauncher).toBool() && !right.data(AbstractTasksModel::IsLauncher).toBool()) { if (launchInPlace) { const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); if (rightPos != -1) { return (leftPos < rightPos); } } return true; } else if (!left.data(AbstractTasksModel::IsLauncher).toBool() && right.data(AbstractTasksModel::IsLauncher).toBool()) { if (launchInPlace) { const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); if (leftPos != -1) { return (leftPos < rightPos); } } return false; } else if (launchInPlace) { const int leftPos = q->launcherPosition(left.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); const int rightPos = q->launcherPosition(right.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()); if (leftPos != -1 && rightPos != -1) { return (leftPos < rightPos); } else if (leftPos != -1 && rightPos == -1) { return true; } else if (leftPos == -1 && rightPos != -1) { return false; } } } // If told to stop after launchers we fall through to the existing map if it exists. if (sortOnlyLaunchers && !sortedPreFilterRows.isEmpty()) { return (sortedPreFilterRows.indexOf(left.row()) < sortedPreFilterRows.indexOf(right.row())); } // Sort other cases by sort mode. switch (sortMode) { case SortVirtualDesktop: { const bool leftAll = left.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool(); const bool rightAll = right.data(AbstractTasksModel::IsOnAllVirtualDesktops).toBool(); if (leftAll && !rightAll) { return true; } else if (rightAll && !leftAll) { return false; } if (!(leftAll && rightAll)) { const QVariantList &leftDesktops = left.data(AbstractTasksModel::VirtualDesktops).toList(); QVariant leftDesktop; int leftDesktopPos = virtualDesktopInfo->numberOfDesktops(); for (const QVariant &desktop : leftDesktops) { const int desktopPos = virtualDesktopInfo->position(desktop); if (desktopPos <= leftDesktopPos) { leftDesktop = desktop; leftDesktopPos = desktopPos; } } const QVariantList &rightDesktops = right.data(AbstractTasksModel::VirtualDesktops).toList(); QVariant rightDesktop; int rightDesktopPos = virtualDesktopInfo->numberOfDesktops(); for (const QVariant &desktop : rightDesktops) { const int desktopPos = virtualDesktopInfo->position(desktop); if (desktopPos <= rightDesktopPos) { rightDesktop = desktop; rightDesktopPos = desktopPos; } } if (!leftDesktop.isNull() && !rightDesktop.isNull() && (leftDesktop != rightDesktop)) { return (virtualDesktopInfo->position(leftDesktop) < virtualDesktopInfo->position(rightDesktop)); } else if (!leftDesktop.isNull() && rightDesktop.isNull()) { return false; } else if (leftDesktop.isNull() && !rightDesktop.isNull()) { return true; } } } // fall through case SortActivity: { // updateActivityTaskCounts() counts the number of window tasks on each // activity. This will sort tasks by comparing a cumulative score made // up of the task counts for each activity a task is assigned to, and // otherwise fall through to alphabetical sorting. int leftScore = -1; int rightScore = -1; const QStringList &leftActivities = left.data(AbstractTasksModel::Activities).toStringList(); if (!leftActivities.isEmpty()) { foreach (const QString &activity, leftActivities) { leftScore += activityTaskCounts[activity]; } } const QStringList &rightActivities = right.data(AbstractTasksModel::Activities).toStringList(); if (!rightActivities.isEmpty()) { foreach (const QString &activity, rightActivities) { rightScore += activityTaskCounts[activity]; } } if (leftScore == -1 || rightScore == -1) { const int sumScore = std::accumulate(activityTaskCounts.constBegin(), activityTaskCounts.constEnd(), 0); if (leftScore == -1) { leftScore = sumScore; } if (rightScore == -1) { rightScore = sumScore; } } if (leftScore != rightScore) { return (leftScore > rightScore); } } // Fall through to source order if sorting is disabled or manual, or alphabetical by app name otherwise. // This marker comment makes gcc/clang happy: // fall through default: { if (sortMode == SortDisabled) { return (left.row() < right.row()); } else { // The overall goal of alphabetic sorting is to sort tasks belonging to the // same app together, while sorting the resulting sets alphabetically among // themselves by the app name. The following code tries to achieve this by // going for AppName first, and falling back to DisplayRole - which for // window-type tasks generally contains the window title - if AppName is // not available. When comparing tasks with identical resulting sort strings, // we sort them by the source model order (i.e. insertion/creation). Older // versions of this code compared tasks by a concatenation of AppName and // DisplayRole at all times, but always sorting by the window title does more // than our goal description - and can cause tasks within an app's set to move // around when window titles change, which is a nuisance for users (especially // in case of tabbed apps that have the window title reflect the active tab, // e.g. web browsers). To recap, the common case is "sort by AppName, then // insertion order", only swapping out AppName for DisplayRole (i.e. window // title) when necessary. QString leftSortString = left.data(AbstractTasksModel::AppName).toString(); if (leftSortString.isEmpty()) { leftSortString = left.data(Qt::DisplayRole).toString(); } QString rightSortString = right.data(AbstractTasksModel::AppName).toString(); if (rightSortString.isEmpty()) { rightSortString = right.data(Qt::DisplayRole).toString(); } const int sortResult = leftSortString.localeAwareCompare(rightSortString); // If the string are identical fall back to source model (creation/append) order. if (sortResult == 0) { return (left.row() < right.row()); } return (sortResult < 0); } } } } TasksModel::TasksModel(QObject *parent) : QSortFilterProxyModel(parent) , d(new Private(this)) { d->initModels(); // Start sorting. sort(0); connect(this, &TasksModel::sourceModelChanged, this, &TasksModel::countChanged); // Private::updateGroupInline() sets our source model, populating the model. We // delay running this until the QML runtime had a chance to call our implementation // of QQmlParserStatus::classBegin(), setting Private::usedByQml to true. If used // by QML, Private::updateGroupInline() will abort if the component is not yet // complete, instead getting called through QQmlParserStatus::componentComplete() // only after all properties have been set. This avoids delegate churn in Qt Quick // views using the model. If not used by QML, Private::updateGroupInline() will run // directly. QTimer::singleShot(0, this, [this]() { d->updateGroupInline(); }); } TasksModel::~TasksModel() { } QHash TasksModel::roleNames() const { if (d->windowTasksModel) { return d->windowTasksModel->roleNames(); } return QHash(); } int TasksModel::rowCount(const QModelIndex &parent) const { return QSortFilterProxyModel::rowCount(parent); } QVariant TasksModel::data(const QModelIndex &proxyIndex, int role) const { if (role == AbstractTasksModel::HasLauncher && proxyIndex.isValid() && proxyIndex.row() < rowCount()) { if (proxyIndex.data(AbstractTasksModel::IsLauncher).toBool()) { return true; } else { if (!d->launcherTasksModel) { return false; } for (int i = 0; i < d->launcherTasksModel->rowCount(); ++i) { const QModelIndex &launcherIndex = d->launcherTasksModel->index(i, 0); if (appsMatch(proxyIndex, launcherIndex)) { return true; } } return false; } } else if (rowCount(proxyIndex) && role == AbstractTasksModel::WinIdList) { QVariantList winIds; for (int i = 0; i < rowCount(proxyIndex); ++i) { winIds.append(index(i, 0, proxyIndex).data(AbstractTasksModel::WinIdList).toList()); } return winIds; } return QSortFilterProxyModel::data(proxyIndex, role); } void TasksModel::updateLauncherCount() { if (!d->launcherTasksModel) { return; } int count = 0; for (int i = 0; i < rowCount(); ++i) { if (index(i, 0).data(AbstractTasksModel::IsLauncher).toBool()) { ++count; } } if (d->launcherCount != count) { d->launcherCount = count; Q_EMIT launcherCountChanged(); } } int TasksModel::launcherCount() const { return d->launcherCount; } bool TasksModel::anyTaskDemandsAttention() const { return d->anyTaskDemandsAttention; } QVariant TasksModel::virtualDesktop() const { return d->filterProxyModel->virtualDesktop(); } void TasksModel::setVirtualDesktop(const QVariant &desktop) { d->filterProxyModel->setVirtualDesktop(desktop); } QRect TasksModel::screenGeometry() const { return d->filterProxyModel->screenGeometry(); } void TasksModel::setScreenGeometry(const QRect &geometry) { d->filterProxyModel->setScreenGeometry(geometry); } QString TasksModel::activity() const { return d->filterProxyModel->activity(); } void TasksModel::setActivity(const QString &activity) { d->filterProxyModel->setActivity(activity); } bool TasksModel::filterByVirtualDesktop() const { return d->filterProxyModel->filterByVirtualDesktop(); } void TasksModel::setFilterByVirtualDesktop(bool filter) { d->filterProxyModel->setFilterByVirtualDesktop(filter); } bool TasksModel::filterByScreen() const { return d->filterProxyModel->filterByScreen(); } void TasksModel::setFilterByScreen(bool filter) { d->filterProxyModel->setFilterByScreen(filter); } bool TasksModel::filterByActivity() const { return d->filterProxyModel->filterByActivity(); } void TasksModel::setFilterByActivity(bool filter) { d->filterProxyModel->setFilterByActivity(filter); } bool TasksModel::filterNotMinimized() const { return d->filterProxyModel->filterNotMinimized(); } void TasksModel::setFilterNotMinimized(bool filter) { d->filterProxyModel->setFilterNotMinimized(filter); } bool TasksModel::filterNotMaximized() const { return d->filterProxyModel->filterNotMaximized(); } void TasksModel::setFilterNotMaximized(bool filter) { d->filterProxyModel->setFilterNotMaximized(filter); } bool TasksModel::filterHidden() const { return d->filterProxyModel->filterHidden(); } void TasksModel::setFilterHidden(bool filter) { d->filterProxyModel->setFilterHidden(filter); } TasksModel::SortMode TasksModel::sortMode() const { return d->sortMode; } void TasksModel::setSortMode(SortMode mode) { if (d->sortMode != mode) { if (mode == SortManual) { d->updateManualSortMap(); } else if (d->sortMode == SortManual) { d->sortedPreFilterRows.clear(); } if (mode == SortVirtualDesktop) { if (!d->virtualDesktopInfo) { d->virtualDesktopInfo = new VirtualDesktopInfo(); } ++d->virtualDesktopInfoUsers; setSortRole(AbstractTasksModel::VirtualDesktops); } else if (d->sortMode == SortVirtualDesktop) { --d->virtualDesktopInfoUsers; if (!d->virtualDesktopInfoUsers) { delete d->virtualDesktopInfo; d->virtualDesktopInfo = nullptr; } setSortRole(Qt::DisplayRole); } if (mode == SortActivity) { if (!d->activityInfo) { d->activityInfo = new ActivityInfo(); } ++d->activityInfoUsers; d->updateActivityTaskCounts(); setSortRole(AbstractTasksModel::Activities); } else if (d->sortMode == SortActivity) { --d->activityInfoUsers; if (!d->activityInfoUsers) { delete d->activityInfo; d->activityInfo = nullptr; } d->activityTaskCounts.clear(); setSortRole(Qt::DisplayRole); } d->sortMode = mode; d->forceResort(); Q_EMIT sortModeChanged(); } } bool TasksModel::separateLaunchers() const { return d->separateLaunchers; } void TasksModel::setSeparateLaunchers(bool separate) { if (d->separateLaunchers != separate) { d->separateLaunchers = separate; d->updateManualSortMap(); d->forceResort(); Q_EMIT separateLaunchersChanged(); } } bool TasksModel::launchInPlace() const { return d->launchInPlace; } void TasksModel::setLaunchInPlace(bool launchInPlace) { if (d->launchInPlace != launchInPlace) { d->launchInPlace = launchInPlace; d->forceResort(); Q_EMIT launchInPlaceChanged(); } } TasksModel::GroupMode TasksModel::groupMode() const { if (!d->groupingProxyModel) { return GroupDisabled; } return d->groupingProxyModel->groupMode(); } void TasksModel::setGroupMode(GroupMode mode) { if (d->groupingProxyModel) { if (mode == GroupDisabled && d->flattenGroupsProxyModel) { d->flattenGroupsProxyModel->setSourceModel(nullptr); } d->groupingProxyModel->setGroupMode(mode); d->updateGroupInline(); } } bool TasksModel::groupInline() const { return d->groupInline; } void TasksModel::setGroupInline(bool groupInline) { if (d->groupInline != groupInline) { d->groupInline = groupInline; d->updateGroupInline(); Q_EMIT groupInlineChanged(); } } int TasksModel::groupingWindowTasksThreshold() const { return d->groupingWindowTasksThreshold; } void TasksModel::setGroupingWindowTasksThreshold(int threshold) { if (d->groupingWindowTasksThreshold != threshold) { d->groupingWindowTasksThreshold = threshold; if (!d->groupInline && d->groupingProxyModel) { d->groupingProxyModel->setWindowTasksThreshold(threshold); } Q_EMIT groupingWindowTasksThresholdChanged(); } } QStringList TasksModel::groupingAppIdBlacklist() const { if (!d->groupingProxyModel) { return QStringList(); } return d->groupingProxyModel->blacklistedAppIds(); } void TasksModel::setGroupingAppIdBlacklist(const QStringList &list) { if (d->groupingProxyModel) { d->groupingProxyModel->setBlacklistedAppIds(list); } } QStringList TasksModel::groupingLauncherUrlBlacklist() const { if (!d->groupingProxyModel) { return QStringList(); } return d->groupingProxyModel->blacklistedLauncherUrls(); } void TasksModel::setGroupingLauncherUrlBlacklist(const QStringList &list) { if (d->groupingProxyModel) { d->groupingProxyModel->setBlacklistedLauncherUrls(list); } } QStringList TasksModel::launcherList() const { if (d->launcherTasksModel) { return d->launcherTasksModel->launcherList(); } return QStringList(); } void TasksModel::setLauncherList(const QStringList &launchers) { d->initLauncherTasksModel(); d->launcherTasksModel->setLauncherList(launchers); d->launchersEverSet = true; } bool TasksModel::requestAddLauncher(const QUrl &url) { d->initLauncherTasksModel(); bool added = d->launcherTasksModel->requestAddLauncher(url); // If using manual and launch-in-place sorting with separate launchers, // we need to trigger a sort map update to move any window tasks to // their launcher position now. if (added && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) { d->updateManualSortMap(); d->forceResort(); } return added; } bool TasksModel::requestRemoveLauncher(const QUrl &url) { if (d->launcherTasksModel) { bool removed = d->launcherTasksModel->requestRemoveLauncher(url); // If using manual and launch-in-place sorting with separate launchers, // we need to trigger a sort map update to move any window tasks no // longer backed by a launcher out of the launcher area. if (removed && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) { d->updateManualSortMap(); d->forceResort(); } return removed; } return false; } bool TasksModel::requestAddLauncherToActivity(const QUrl &url, const QString &activity) { d->initLauncherTasksModel(); bool added = d->launcherTasksModel->requestAddLauncherToActivity(url, activity); // If using manual and launch-in-place sorting with separate launchers, // we need to trigger a sort map update to move any window tasks to // their launcher position now. if (added && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) { d->updateManualSortMap(); d->forceResort(); } return added; } bool TasksModel::requestRemoveLauncherFromActivity(const QUrl &url, const QString &activity) { if (d->launcherTasksModel) { bool removed = d->launcherTasksModel->requestRemoveLauncherFromActivity(url, activity); // If using manual and launch-in-place sorting with separate launchers, // we need to trigger a sort map update to move any window tasks no // longer backed by a launcher out of the launcher area. if (removed && d->sortMode == SortManual && (d->launchInPlace || !d->separateLaunchers)) { d->updateManualSortMap(); d->forceResort(); } return removed; } return false; } QStringList TasksModel::launcherActivities(const QUrl &url) { if (d->launcherTasksModel) { return d->launcherTasksModel->launcherActivities(url); } return {}; } int TasksModel::launcherPosition(const QUrl &url) const { if (d->launcherTasksModel) { return d->launcherTasksModel->launcherPosition(url); } return -1; } void TasksModel::requestActivate(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestActivate(mapToSource(index)); } } void TasksModel::requestNewInstance(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestNewInstance(mapToSource(index)); } } void TasksModel::requestOpenUrls(const QModelIndex &index, const QList &urls) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestOpenUrls(mapToSource(index), urls); } } void TasksModel::requestClose(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestClose(mapToSource(index)); } } void TasksModel::requestMove(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestMove(mapToSource(index)); } } void TasksModel::requestResize(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestResize(mapToSource(index)); } } void TasksModel::requestToggleMinimized(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(index)); } } void TasksModel::requestToggleMaximized(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestToggleMaximized(mapToSource(index)); } } void TasksModel::requestToggleKeepAbove(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(index)); } } void TasksModel::requestToggleKeepBelow(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(index)); } } void TasksModel::requestToggleFullScreen(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(index)); } } void TasksModel::requestToggleShaded(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestToggleShaded(mapToSource(index)); } } void TasksModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestVirtualDesktops(mapToSource(index), desktops); } } void TasksModel::requestNewVirtualDesktop(const QModelIndex &index) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestNewVirtualDesktop(mapToSource(index)); } } void TasksModel::requestActivities(const QModelIndex &index, const QStringList &activities) { if (index.isValid() && index.model() == this) { d->groupingProxyModel->requestActivities(mapToSource(index), activities); } } void TasksModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate) { if (index.isValid() && index.model() == this) { d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate); } } void TasksModel::requestToggleGrouping(const QModelIndex &index) { if (index.isValid() && index.model() == this) { const QModelIndex &target = (d->flattenGroupsProxyModel ? d->flattenGroupsProxyModel->mapToSource(mapToSource(index)) : mapToSource(index)); d->groupingProxyModel->requestToggleGrouping(target); } } bool TasksModel::move(int row, int newPos, const QModelIndex &parent) { if (d->sortMode != SortManual || row == newPos || newPos < 0 || newPos >= rowCount(parent)) { return false; } const QModelIndex &idx = index(row, 0); bool isLauncherMove = false; // Figure out if we're moving a launcher so we can run barrier checks. if (idx.isValid()) { if (idx.data(AbstractTasksModel::IsLauncher).toBool()) { isLauncherMove = true; // When using launch-in-place sorting, launcher-backed window tasks act as launchers. } else if ((d->launchInPlace || !d->separateLaunchers) && idx.data(AbstractTasksModel::IsWindow).toBool()) { const QUrl &launcherUrl = idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); const int launcherPos = launcherPosition(launcherUrl); if (launcherPos != -1) { isLauncherMove = true; } } } else { return false; } if (d->separateLaunchers) { const int firstTask = (d->launcherTasksModel ? (d->launchInPlace ? d->launcherTasksModel->rowCount() : launcherCount()) : 0); // Don't allow launchers to be moved past the last launcher. if (isLauncherMove && newPos >= firstTask) { return false; } // Don't allow tasks to be moved into the launchers. if (!isLauncherMove && newPos < firstTask) { return false; } } // Treat flattened-out groups as single items. if (d->flattenGroupsProxyModel) { QModelIndex groupingRowIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(row, 0))); const QModelIndex &groupingRowIndexParent = groupingRowIndex.parent(); QModelIndex groupingNewPosIndex = d->flattenGroupsProxyModel->mapToSource(mapToSource(index(newPos, 0))); const QModelIndex &groupingNewPosIndexParent = groupingNewPosIndex.parent(); // Disallow moves within a flattened-out group (TODO: for now, anyway). if (groupingRowIndexParent.isValid() && (groupingRowIndexParent == groupingNewPosIndex || groupingRowIndexParent == groupingNewPosIndexParent)) { return false; } int offset = 0; int extraChildCount = 0; if (groupingRowIndexParent.isValid()) { offset = groupingRowIndex.row(); extraChildCount = d->groupingProxyModel->rowCount(groupingRowIndexParent) - 1; groupingRowIndex = groupingRowIndexParent; } if (groupingNewPosIndexParent.isValid()) { int extra = d->groupingProxyModel->rowCount(groupingNewPosIndexParent) - 1; if (newPos > row) { newPos += extra; newPos -= groupingNewPosIndex.row(); groupingNewPosIndex = groupingNewPosIndexParent.model()->index(extra, 0, groupingNewPosIndexParent); } else { newPos -= groupingNewPosIndex.row(); groupingNewPosIndex = groupingNewPosIndexParent; } } beginMoveRows(QModelIndex(), (row - offset), (row - offset) + extraChildCount, QModelIndex(), (newPos > row) ? newPos + 1 : newPos); row = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingRowIndex)).row()); newPos = d->sortedPreFilterRows.indexOf(d->filterProxyModel->mapToSource(d->groupingProxyModel->mapToSource(groupingNewPosIndex)).row()); // Update sort mappings. d->sortedPreFilterRows.move(row, newPos); if (groupingRowIndexParent.isValid()) { d->consolidateManualSortMapForGroup(groupingRowIndexParent); } endMoveRows(); } else { beginMoveRows(parent, row, row, parent, (newPos > row) ? newPos + 1 : newPos); // Translate to sort map indices. const QModelIndex &groupingRowIndex = mapToSource(index(row, 0, parent)); const QModelIndex &preFilterRowIndex = d->preFilterIndex(groupingRowIndex); row = d->sortedPreFilterRows.indexOf(preFilterRowIndex.row()); newPos = d->sortedPreFilterRows.indexOf(d->preFilterIndex(mapToSource(index(newPos, 0, parent))).row()); // Update sort mapping. d->sortedPreFilterRows.move(row, newPos); // If we moved a group parent, consolidate sort map for children. if (!parent.isValid() && groupMode() != GroupDisabled && d->groupingProxyModel->rowCount(groupingRowIndex)) { d->consolidateManualSortMapForGroup(groupingRowIndex); } endMoveRows(); } // Resort. d->forceResort(); if (!d->separateLaunchers && isLauncherMove) { const QModelIndex &idx = d->concatProxyModel->index(d->sortedPreFilterRows.at(newPos), 0); const QUrl &launcherUrl = idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); // Move launcher for launcher-backed task along with task if launchers // are not being kept separate. // We don't need to resort again because the launcher is implicitly hidden // at this time. if (!idx.data(AbstractTasksModel::IsLauncher).toBool()) { const int launcherPos = d->launcherTasksModel->launcherPosition(launcherUrl); const QModelIndex &launcherIndex = d->launcherTasksModel->index(launcherPos, 0); const int sortIndex = d->sortedPreFilterRows.indexOf(d->concatProxyModel->mapFromSource(launcherIndex).row()); d->sortedPreFilterRows.move(sortIndex, newPos); // Otherwise move matching windows to after the launcher task (they are // currently hidden but might be on another virtual desktop). } else { for (int i = (d->sortedPreFilterRows.count() - 1); i >= 0; --i) { const QModelIndex &concatProxyIndex = d->concatProxyModel->index(d->sortedPreFilterRows.at(i), 0); if (launcherUrl == concatProxyIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl()) { d->sortedPreFilterRows.move(i, newPos); if (newPos > i) { --newPos; } } } } } // Setup for syncLaunchers(). d->launcherSortingDirty = isLauncherMove; return true; } void TasksModel::syncLaunchers() { // Writes the launcher order exposed through the model back to the launcher // tasks model, committing any move() operations to persistent state. if (!d->launcherTasksModel || !d->launcherSortingDirty) { return; } QMap sortedShownLaunchers; QStringList sortedHiddenLaunchers; foreach (const QString &launcherUrlStr, launcherList()) { int row = -1; QStringList activities; QUrl launcherUrl; std::tie(launcherUrl, activities) = deserializeLauncher(launcherUrlStr); for (int i = 0; i < rowCount(); ++i) { const QUrl &rowLauncherUrl = index(i, 0).data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(); // `LauncherTasksModel::launcherList()` returns data in a format suitable for writing // to persistent configuration storage, e.g. `preferred://browser`. We mean to compare // this last "save state" to a higher, resolved URL representation to compute the delta // so we need to move the unresolved URLs through `TaskTools::appDataFromUrl()` first. // TODO: This bypasses an existing lookup cache for the resolved app data that exists // in LauncherTasksModel. It's likely a good idea to eventually move these caches out // of the various models and share them among users of `TaskTools::appDataFromUrl()`, // and then also do resolution implicitly in `TaskTools::launcherUrlsMatch`, to speed // things up slightly and make the models simpler (central cache eviction, ...). if (launcherUrlsMatch(appDataFromUrl(launcherUrl).url, rowLauncherUrl, IgnoreQueryItems)) { row = i; break; } } if (row != -1) { sortedShownLaunchers.insert(row, launcherUrlStr); } else { sortedHiddenLaunchers << launcherUrlStr; } } // Prep sort map for source model data changes. if (d->sortMode == SortManual) { QVector sortMapIndices; QVector preFilterRows; for (int i = 0; i < d->launcherTasksModel->rowCount(); ++i) { const QModelIndex &launcherIndex = d->launcherTasksModel->index(i, 0); const QModelIndex &concatIndex = d->concatProxyModel->mapFromSource(launcherIndex); sortMapIndices << d->sortedPreFilterRows.indexOf(concatIndex.row()); preFilterRows << concatIndex.row(); } // We're going to write back launcher model entries in the sort // map in concat model order, matching the reordered launcher list // we're about to pass down. std::sort(sortMapIndices.begin(), sortMapIndices.end()); for (int i = 0; i < sortMapIndices.count(); ++i) { d->sortedPreFilterRows.replace(sortMapIndices.at(i), preFilterRows.at(i)); } } setLauncherList(sortedShownLaunchers.values() + sortedHiddenLaunchers); d->launcherSortingDirty = false; } QModelIndex TasksModel::activeTask() const { for (int i = 0; i < rowCount(); ++i) { const QModelIndex &idx = index(i, 0); if (idx.data(AbstractTasksModel::IsActive).toBool()) { if (groupMode() != GroupDisabled && rowCount(idx)) { for (int j = 0; j < rowCount(idx); ++j) { const QModelIndex &child = index(j, 0, idx); if (child.data(AbstractTasksModel::IsActive).toBool()) { return child; } } } else { return idx; } } } return QModelIndex(); } QModelIndex TasksModel::makeModelIndex(int row, int childRow) const { if (row < 0 || row >= rowCount()) { return QModelIndex(); } if (childRow == -1) { return index(row, 0); } else { const QModelIndex &parent = index(row, 0); if (childRow < rowCount(parent)) { return index(childRow, 0, parent); } } return QModelIndex(); } QPersistentModelIndex TasksModel::makePersistentModelIndex(int row, int childCount) const { return QPersistentModelIndex(makeModelIndex(row, childCount)); } void TasksModel::classBegin() { d->usedByQml = true; } void TasksModel::componentComplete() { d->componentComplete = true; // Sets our source model, populating the model. d->updateGroupInline(); } bool TasksModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { // All our filtering occurs at the top-level; anything below always // goes through. if (sourceParent.isValid()) { return true; } const QModelIndex &sourceIndex = sourceModel()->index(sourceRow, 0); // In inline grouping mode, filter out group parents. if (d->groupInline && d->flattenGroupsProxyModel && sourceIndex.data(AbstractTasksModel::IsGroupParent).toBool()) { return false; } const QString &appId = sourceIndex.data(AbstractTasksModel::AppId).toString(); const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString(); // Filter startup tasks we already have a window task for. if (sourceIndex.data(AbstractTasksModel::IsStartup).toBool()) { for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) { const QModelIndex &filterIndex = d->filterProxyModel->index(i, 0); if (!filterIndex.data(AbstractTasksModel::IsWindow).toBool()) { continue; } if ((!appId.isEmpty() && appId == filterIndex.data(AbstractTasksModel::AppId).toString()) || (!appName.isEmpty() && appName == filterIndex.data(AbstractTasksModel::AppName).toString())) { return false; } } } // Filter launcher tasks we already have a startup or window task for (that // got through filtering). if (sourceIndex.data(AbstractTasksModel::IsLauncher).toBool()) { for (int i = 0; i < d->filterProxyModel->rowCount(); ++i) { const QModelIndex &filteredIndex = d->filterProxyModel->index(i, 0); if (!filteredIndex.data(AbstractTasksModel::IsWindow).toBool() && !filteredIndex.data(AbstractTasksModel::IsStartup).toBool()) { continue; } if (appsMatch(sourceIndex, filteredIndex)) { return false; } } } return true; } bool TasksModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { // In manual sort mode, sort by map. if (d->sortMode == SortManual) { return (d->sortedPreFilterRows.indexOf(d->preFilterIndex(left).row()) < d->sortedPreFilterRows.indexOf(d->preFilterIndex(right).row())); } return d->lessThan(left, right); } }