QortalOS Brooklyn for Raspberry Pi 4
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1910 lines
68 KiB

/*
SPDX-FileCopyrightText: 2016 Eike Hein <[email protected]>
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 <QGuiApplication>
#include <QTimer>
#include <QUrl>
#include <numeric>
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<int> sortedPreFilterRows;
QVector<int> sortRowInsertQueue;
bool sortRowInsertQueueStale = false;
QHash<QString, int> 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<int> &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<int> 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<int> 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<int> &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<int>{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<int> &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<int>{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<int> 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<QString, int> 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<int, QByteArray> TasksModel::roleNames() const
{
if (d->windowTasksModel) {
return d->windowTasksModel->roleNames();
}
return QHash<int, QByteArray>();
}
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<QUrl> &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<int, QString> 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<int> sortMapIndices;
QVector<int> 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);
}
}