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.
 
 
 
 
 
 

1227 lines
40 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 "taskgroupingproxymodel.h"
#include "abstracttasksmodel.h"
#include "tasktools.h"
#include <QSet>
namespace TaskManager
{
class Q_DECL_HIDDEN TaskGroupingProxyModel::Private
{
public:
Private(TaskGroupingProxyModel *q);
~Private();
AbstractTasksModelIface *abstractTasksSourceModel = nullptr;
TasksModel::GroupMode groupMode = TasksModel::GroupApplications;
bool groupDemandingAttention = false;
int windowTasksThreshold = -1;
QVector<QVector<int> *> rowMap;
QSet<QString> blacklistedAppIds;
QSet<QString> blacklistedLauncherUrls;
bool isGroup(int row);
bool any(const QModelIndex &parent, int role);
bool all(const QModelIndex &parent, int role);
void sourceRowsAboutToBeInserted(const QModelIndex &parent, int first, int last);
void sourceRowsInserted(const QModelIndex &parent, int start, int end);
void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last);
void sourceRowsRemoved(const QModelIndex &parent, int start, int end);
void sourceModelAboutToBeReset();
void sourceModelReset();
void sourceDataChanged(QModelIndex topLeft, QModelIndex bottomRight, const QVector<int> &roles = QVector<int>());
void adjustMap(int anchor, int delta);
void rebuildMap();
bool shouldGroupTasks();
void checkGrouping(bool silent = false);
bool isBlacklisted(const QModelIndex &sourceIndex);
bool tryToGroup(const QModelIndex &sourceIndex, bool silent = false);
void formGroupFor(const QModelIndex &index);
void breakGroupFor(const QModelIndex &index, bool silent = false);
private:
TaskGroupingProxyModel *q;
};
TaskGroupingProxyModel::Private::Private(TaskGroupingProxyModel *q)
: q(q)
{
}
TaskGroupingProxyModel::Private::~Private()
{
qDeleteAll(rowMap);
}
bool TaskGroupingProxyModel::Private::isGroup(int row)
{
if (row < 0 || row >= rowMap.count()) {
return false;
}
return (rowMap.at(row)->count() > 1);
}
bool TaskGroupingProxyModel::Private::any(const QModelIndex &parent, int role)
{
bool is = false;
for (int i = 0; i < q->rowCount(parent); ++i) {
if (q->index(i, 0, parent).data(role).toBool()) {
return true;
}
}
return is;
}
bool TaskGroupingProxyModel::Private::all(const QModelIndex &parent, int role)
{
bool is = true;
for (int i = 0; i < q->rowCount(parent); ++i) {
if (!q->index(i, 0, parent).data(role).toBool()) {
return false;
}
}
return is;
}
void TaskGroupingProxyModel::Private::sourceRowsAboutToBeInserted(const QModelIndex &parent, int first, int last)
{
Q_UNUSED(parent)
Q_UNUSED(first)
Q_UNUSED(last)
}
void TaskGroupingProxyModel::Private::sourceRowsInserted(const QModelIndex &parent, int start, int end)
{
// We only support flat source models.
if (parent.isValid()) {
return;
}
adjustMap(start, (end - start) + 1);
bool shouldGroup = shouldGroupTasks(); // Can be slightly expensive; cache return value.
for (int i = start; i <= end; ++i) {
if (!shouldGroup || !tryToGroup(q->sourceModel()->index(i, 0))) {
q->beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count());
rowMap.append(new QVector<int>{i});
q->endInsertRows();
}
}
checkGrouping();
}
void TaskGroupingProxyModel::Private::sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
{
// We only support flat source models.
if (parent.isValid()) {
return;
}
for (int i = first; i <= last; ++i) {
for (int j = 0; j < rowMap.count(); ++j) {
const QVector<int> *sourceRows = rowMap.at(j);
const int mapIndex = sourceRows->indexOf(i);
if (mapIndex != -1) {
// Remove top-level item.
if (sourceRows->count() == 1) {
q->beginRemoveRows(QModelIndex(), j, j);
delete rowMap.takeAt(j);
q->endRemoveRows();
// Dissolve group.
} else if (sourceRows->count() == 2) {
const QModelIndex parent = q->index(j, 0);
q->beginRemoveRows(parent, 0, 1);
rowMap[j]->remove(mapIndex);
q->endRemoveRows();
// We're no longer a group parent.
Q_EMIT q->dataChanged(parent, parent);
// Remove group member.
} else {
const QModelIndex parent = q->index(j, 0);
q->beginRemoveRows(parent, mapIndex, mapIndex);
rowMap[j]->remove(mapIndex);
q->endRemoveRows();
// Various roles of the parent evaluate child data, and the
// child list has changed.
Q_EMIT q->dataChanged(parent, parent);
}
break;
}
}
}
}
void TaskGroupingProxyModel::Private::sourceRowsRemoved(const QModelIndex &parent, int start, int end)
{
// We only support flat source models.
if (parent.isValid()) {
return;
}
adjustMap(start + 1, -((end - start) + 1));
checkGrouping();
}
void TaskGroupingProxyModel::Private::sourceModelAboutToBeReset()
{
q->beginResetModel();
}
void TaskGroupingProxyModel::Private::sourceModelReset()
{
rebuildMap();
q->endResetModel();
}
void TaskGroupingProxyModel::Private::sourceDataChanged(QModelIndex topLeft, QModelIndex bottomRight, const QVector<int> &roles)
{
for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
const QModelIndex &sourceIndex = q->sourceModel()->index(i, 0);
QModelIndex proxyIndex = q->mapFromSource(sourceIndex);
if (!proxyIndex.isValid()) {
return;
}
const QModelIndex parent = proxyIndex.parent();
// If a child item changes, its parent may need an update as well as many of
// the data roles evaluate child data. See data().
// TODO: Some roles do not need to bubble up as they fall through to the first
// child in data(); it _might_ be worth adding constraints here later.
if (parent.isValid()) {
Q_EMIT q->dataChanged(parent, parent, roles);
}
// When Private::groupDemandingAttention is false, tryToGroup() exempts tasks
// which demand attention from being grouped. Therefore if this task is no longer
// demanding attention, we need to try grouping it now.
if (!parent.isValid() && !groupDemandingAttention && roles.contains(AbstractTasksModel::IsDemandingAttention)
&& !sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) {
if (shouldGroupTasks() && tryToGroup(sourceIndex)) {
q->beginRemoveRows(QModelIndex(), proxyIndex.row(), proxyIndex.row());
delete rowMap.takeAt(proxyIndex.row());
q->endRemoveRows();
} else {
Q_EMIT q->dataChanged(proxyIndex, proxyIndex, roles);
}
} else {
Q_EMIT q->dataChanged(proxyIndex, proxyIndex, roles);
}
}
}
void TaskGroupingProxyModel::Private::adjustMap(int anchor, int delta)
{
for (int i = 0; i < rowMap.count(); ++i) {
QVector<int> *sourceRows = rowMap.at(i);
QMutableVectorIterator<int> it(*sourceRows);
while (it.hasNext()) {
it.next();
if (it.value() >= anchor) {
it.setValue(it.value() + delta);
}
}
}
}
void TaskGroupingProxyModel::Private::rebuildMap()
{
qDeleteAll(rowMap);
rowMap.clear();
const int rows = q->sourceModel()->rowCount();
rowMap.reserve(rows);
for (int i = 0; i < rows; ++i) {
rowMap.append(new QVector<int>{i});
}
checkGrouping(true /* silent */);
}
bool TaskGroupingProxyModel::Private::shouldGroupTasks()
{
if (groupMode == TasksModel::GroupDisabled) {
return false;
}
if (windowTasksThreshold != -1) {
// We're going to check the number of window tasks in the source model
// against the grouping threshold. In practice that means we're ignoring
// launcher and startup tasks. Startup tasks because they're very short-
// lived (i.e. forming/breaking groups as they come and go would be very
// noisy) and launcher tasks because we expect consumers to budget for
// them in the threshold they set.
int windowTasksCount = 0;
for (int i = 0; i < q->sourceModel()->rowCount(); ++i) {
const QModelIndex &idx = q->sourceModel()->index(i, 0);
if (idx.data(AbstractTasksModel::IsWindow).toBool()) {
++windowTasksCount;
}
}
return (windowTasksCount > windowTasksThreshold);
}
return true;
}
void TaskGroupingProxyModel::Private::checkGrouping(bool silent)
{
if (shouldGroupTasks()) {
for (int i = (rowMap.count()) - 1; i >= 0; --i) {
if (isGroup(i)) {
continue;
}
if (tryToGroup(q->sourceModel()->index(rowMap.at(i)->constFirst(), 0), silent)) {
q->beginRemoveRows(QModelIndex(), i, i);
delete rowMap.takeAt(i); // Safe since we're iterating backwards.
q->endRemoveRows();
}
}
} else {
for (int i = (rowMap.count()) - 1; i >= 0; --i) {
breakGroupFor(q->index(i, 0), silent);
}
}
}
bool TaskGroupingProxyModel::Private::isBlacklisted(const QModelIndex &sourceIndex)
{
// Check app id against blacklist.
if (blacklistedAppIds.count() && blacklistedAppIds.contains(sourceIndex.data(AbstractTasksModel::AppId).toString())) {
return true;
}
// Check launcher URL (sans query items) against blacklist.
if (blacklistedLauncherUrls.count()) {
const QUrl &launcherUrl = sourceIndex.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
const QString &launcherUrlString = launcherUrl.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery);
if (blacklistedLauncherUrls.contains(launcherUrlString)) {
return true;
}
}
return false;
}
bool TaskGroupingProxyModel::Private::tryToGroup(const QModelIndex &sourceIndex, bool silent)
{
// NOTE: We only group window tasks at this time. If this ever changes, the
// implementation of data() will have to be adjusted significantly, as for
// many roles it currently falls through to the first child item when dealing
// with requests for the parent (e.g. IsWindow).
if (!sourceIndex.data(AbstractTasksModel::IsWindow).toBool()) {
return false;
}
// If Private::groupDemandingAttention is false and this task is demanding
// attention, don't group it at this time. We'll instead try to group it once
// it no longer demands attention (see sourceDataChanged()).
if (!groupDemandingAttention && sourceIndex.data(AbstractTasksModel::IsDemandingAttention).toBool()) {
return false;
}
// Blacklist checks.
if (isBlacklisted(sourceIndex)) {
return false;
}
// Meat of the matter: Try to add this source row to a sub-list with source rows
// associated with the same application.
for (int i = 0; i < rowMap.count(); ++i) {
const QModelIndex &groupRep = q->sourceModel()->index(rowMap.at(i)->constFirst(), 0);
// Don't match a row with itself.
if (sourceIndex == groupRep) {
continue;
}
// Don't group windows with anything other than windows.
if (!groupRep.data(AbstractTasksModel::IsWindow).toBool()) {
continue;
}
if (appsMatch(sourceIndex, groupRep)) {
const QModelIndex parent = q->index(i, 0);
if (!silent) {
const int newIndex = rowMap.at(i)->count();
if (newIndex == 1) {
q->beginInsertRows(parent, 0, 1);
} else {
q->beginInsertRows(parent, newIndex, newIndex);
}
}
rowMap[i]->append(sourceIndex.row());
if (!silent) {
q->endInsertRows();
Q_EMIT q->dataChanged(parent, parent);
}
return true;
}
}
return false;
}
void TaskGroupingProxyModel::Private::formGroupFor(const QModelIndex &index)
{
// Already in group or a group.
if (index.parent().isValid() || isGroup(index.row())) {
return;
}
// We need to grab a source index as we may invalidate the index passed
// in through grouping.
const QModelIndex &sourceTarget = q->mapToSource(index);
for (int i = (rowMap.count() - 1); i >= 0; --i) {
const QModelIndex &sourceIndex = q->sourceModel()->index(rowMap.at(i)->constFirst(), 0);
if (!appsMatch(sourceTarget, sourceIndex)) {
continue;
}
if (tryToGroup(sourceIndex)) {
q->beginRemoveRows(QModelIndex(), i, i);
delete rowMap.takeAt(i); // Safe since we're iterating backwards.
q->endRemoveRows();
}
}
}
void TaskGroupingProxyModel::Private::breakGroupFor(const QModelIndex &index, bool silent)
{
const int row = index.row();
if (!isGroup(row)) {
return;
}
// The first child will move up to the top level.
QVector<int> extraChildren = rowMap.at(row)->mid(1);
// NOTE: We're going to do remove+insert transactions instead of a
// single reparenting move transaction to save on complexity in the
// proxies above us.
// TODO: This could technically be optimized, though it's very
// unlikely to be ever worth it.
if (!silent) {
q->beginRemoveRows(index, 0, extraChildren.count());
}
rowMap[row]->resize(1);
if (!silent) {
q->endRemoveRows();
// We're no longer a group parent.
Q_EMIT q->dataChanged(index, index);
q->beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count() + (extraChildren.count() - 1));
}
for (int i = 0; i < extraChildren.count(); ++i) {
rowMap.append(new QVector<int>{extraChildren.at(i)});
}
if (!silent) {
q->endInsertRows();
}
}
TaskGroupingProxyModel::TaskGroupingProxyModel(QObject *parent)
: QAbstractProxyModel(parent)
, d(new Private(this))
{
}
TaskGroupingProxyModel::~TaskGroupingProxyModel()
{
}
QModelIndex TaskGroupingProxyModel::index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || column != 0) {
return QModelIndex();
}
if (parent.isValid() && row < d->rowMap.at(parent.row())->count()) {
return createIndex(row, column, d->rowMap.at(parent.row()));
}
if (row < d->rowMap.count()) {
return createIndex(row, column, nullptr);
}
return QModelIndex();
}
QModelIndex TaskGroupingProxyModel::parent(const QModelIndex &child) const
{
if (child.internalPointer() == nullptr) {
return QModelIndex();
} else {
const int parentRow = d->rowMap.indexOf(static_cast<QVector<int> *>(child.internalPointer()));
if (parentRow != -1) {
return index(parentRow, 0);
}
// If we were asked to find the parent for an internalPointer we can't
// locate, we have corrupted data: This should not happen.
Q_ASSERT(parentRow != -1);
}
return QModelIndex();
}
QModelIndex TaskGroupingProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
{
if (!sourceIndex.isValid() || sourceIndex.model() != sourceModel()) {
return QModelIndex();
}
for (int i = 0; i < d->rowMap.count(); ++i) {
const QVector<int> *sourceRows = d->rowMap.at(i);
const int childIndex = sourceRows->indexOf(sourceIndex.row());
const QModelIndex parent = index(i, 0);
if (childIndex == 0) {
// If the sub-list we found the source row in is larger than 1 (i.e. part
// of a group, map to the logical child item instead of the parent item
// the source row also stands in for. The parent is therefore unreachable
// from mapToSource().
if (d->isGroup(i)) {
return index(0, 0, parent);
// Otherwise map to the top-level item.
} else {
return parent;
}
} else if (childIndex != -1) {
return index(childIndex, 0, parent);
}
}
return QModelIndex();
}
QModelIndex TaskGroupingProxyModel::mapToSource(const QModelIndex &proxyIndex) const
{
if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
return QModelIndex();
}
const QModelIndex &parent = proxyIndex.parent();
if (parent.isValid()) {
if (parent.row() < 0 || parent.row() >= d->rowMap.count()) {
return QModelIndex();
}
return sourceModel()->index(d->rowMap.at(parent.row())->at(proxyIndex.row()), 0);
} else {
// Group parents items therefore equate to the first child item; the source
// row logically appears twice in the proxy.
// mapFromSource() is not required to handle this well (consider proxies can
// filter out rows, too) and opts to map to the child item, as the group parent
// has its Qt::DisplayRole mangled by data(), and it's more useful for trans-
// lating dataChanged() from the source model.
return sourceModel()->index(d->rowMap.at(proxyIndex.row())->at(0), 0);
}
return QModelIndex();
}
int TaskGroupingProxyModel::rowCount(const QModelIndex &parent) const
{
if (!sourceModel()) {
return 0;
}
if (parent.isValid() && parent.model() == this) {
// Don't return row count for top-level item at child row: Group members
// never have further children of their own.
if (parent.parent().isValid()) {
return 0;
}
if (parent.row() < 0 || parent.row() >= d->rowMap.count()) {
return 0;
}
const uint rowCount = d->rowMap.at(parent.row())->count();
// If this sub-list in the map only has one entry, it's a plain item, not
// parent to a group.
if (rowCount == 1) {
return 0;
} else {
return rowCount;
}
}
return d->rowMap.count();
}
bool TaskGroupingProxyModel::hasChildren(const QModelIndex &parent) const
{
if ((parent.model() && parent.model() != this) || !sourceModel()) {
return false;
}
return rowCount(parent);
}
int TaskGroupingProxyModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
}
QVariant TaskGroupingProxyModel::data(const QModelIndex &proxyIndex, int role) const
{
if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
return QVariant();
}
const QModelIndex &parent = proxyIndex.parent();
const bool isWindowGroup = (!parent.isValid() && d->isGroup(proxyIndex.row()));
// For group parent items, this will map to the first child task.
const QModelIndex &sourceIndex = mapToSource(proxyIndex);
if (!sourceIndex.isValid()) {
return QVariant();
}
if (role == AbstractTasksModel::IsGroupable) {
return !d->isBlacklisted(sourceIndex);
}
if (isWindowGroup) {
// For group parent items, DisplayRole is mapped to AppName of the first child.
if (role == Qt::DisplayRole) {
const QString &appName = sourceIndex.data(AbstractTasksModel::AppName).toString();
// Groups are formed by app id or launcher URL; neither requires
// AppName to be available. If it's not, fall back to the app id
/// rather than an empty string.
if (appName.isEmpty()) {
return sourceIndex.data(AbstractTasksModel::AppId);
}
return appName;
} else if (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;
} else if (role == AbstractTasksModel::MimeType) {
return QStringLiteral("windowsystem/multiple-winids");
} else if (role == AbstractTasksModel::MimeData) {
// FIXME TODO: Implement.
return QVariant();
} else if (role == AbstractTasksModel::IsGroupParent) {
return true;
} else if (role == AbstractTasksModel::ChildCount) {
return rowCount(proxyIndex);
} else if (role == AbstractTasksModel::IsActive) {
return d->any(proxyIndex, AbstractTasksModel::IsActive);
} else if (role == AbstractTasksModel::IsClosable) {
return d->all(proxyIndex, AbstractTasksModel::IsClosable);
} else if (role == AbstractTasksModel::IsMovable) {
// Moving groups makes no sense.
return false;
} else if (role == AbstractTasksModel::IsResizable) {
// Resizing groups makes no sense.
return false;
} else if (role == AbstractTasksModel::IsMaximizable) {
return d->all(proxyIndex, AbstractTasksModel::IsMaximizable);
} else if (role == AbstractTasksModel::IsMaximized) {
return d->all(proxyIndex, AbstractTasksModel::IsMaximized);
} else if (role == AbstractTasksModel::IsMinimizable) {
return d->all(proxyIndex, AbstractTasksModel::IsMinimizable);
} else if (role == AbstractTasksModel::IsMinimized) {
return d->all(proxyIndex, AbstractTasksModel::IsMinimized);
} else if (role == AbstractTasksModel::IsKeepAbove) {
return d->all(proxyIndex, AbstractTasksModel::IsKeepAbove);
} else if (role == AbstractTasksModel::IsKeepBelow) {
return d->all(proxyIndex, AbstractTasksModel::IsKeepBelow);
} else if (role == AbstractTasksModel::IsFullScreenable) {
return d->all(proxyIndex, AbstractTasksModel::IsFullScreenable);
} else if (role == AbstractTasksModel::IsFullScreen) {
return d->all(proxyIndex, AbstractTasksModel::IsFullScreen);
} else if (role == AbstractTasksModel::IsShadeable) {
return d->all(proxyIndex, AbstractTasksModel::IsShadeable);
} else if (role == AbstractTasksModel::IsShaded) {
return d->all(proxyIndex, AbstractTasksModel::IsShaded);
} else if (role == AbstractTasksModel::IsVirtualDesktopsChangeable) {
return d->all(proxyIndex, AbstractTasksModel::IsVirtualDesktopsChangeable);
} else if (role == AbstractTasksModel::VirtualDesktops) {
QStringList desktops;
for (int i = 0; i < rowCount(proxyIndex); ++i) {
desktops.append(index(i, 0, proxyIndex).data(AbstractTasksModel::VirtualDesktops).toStringList());
}
desktops.removeDuplicates();
return desktops;
} else if (role == AbstractTasksModel::ScreenGeometry) {
// TODO: Nothing needs this for now and it would add complexity to
// make it a list; skip it until needed. Once it is, do it similarly
// to the AbstractTasksModel::VirtualDesktop case.
return QVariant();
} else if (role == AbstractTasksModel::Activities) {
QStringList activities;
for (int i = 0; i < rowCount(proxyIndex); ++i) {
activities.append(index(i, 0, proxyIndex).data(AbstractTasksModel::Activities).toStringList());
}
activities.removeDuplicates();
return activities;
} else if (role == AbstractTasksModel::IsDemandingAttention) {
return d->any(proxyIndex, AbstractTasksModel::IsDemandingAttention);
} else if (role == AbstractTasksModel::SkipTaskbar) {
return d->all(proxyIndex, AbstractTasksModel::SkipTaskbar);
}
}
return sourceIndex.data(role);
}
void TaskGroupingProxyModel::setSourceModel(QAbstractItemModel *sourceModel)
{
if (sourceModel == QAbstractProxyModel::sourceModel()) {
return;
}
beginResetModel();
if (QAbstractProxyModel::sourceModel()) {
QAbstractProxyModel::sourceModel()->disconnect(this);
}
QAbstractProxyModel::setSourceModel(sourceModel);
d->abstractTasksSourceModel = dynamic_cast<AbstractTasksModelIface *>(sourceModel);
if (sourceModel) {
d->rebuildMap();
using namespace std::placeholders;
auto dd = d.data();
connect(sourceModel,
&QSortFilterProxyModel::rowsAboutToBeInserted,
this,
std::bind(&TaskGroupingProxyModel::Private::sourceRowsAboutToBeInserted, dd, _1, _2, _3));
connect(sourceModel, &QSortFilterProxyModel::rowsInserted, this, std::bind(&TaskGroupingProxyModel::Private::sourceRowsInserted, dd, _1, _2, _3));
connect(sourceModel,
&QSortFilterProxyModel::rowsAboutToBeRemoved,
this,
std::bind(&TaskGroupingProxyModel::Private::sourceRowsAboutToBeRemoved, dd, _1, _2, _3));
connect(sourceModel, &QSortFilterProxyModel::rowsRemoved, this, std::bind(&TaskGroupingProxyModel::Private::sourceRowsRemoved, dd, _1, _2, _3));
connect(sourceModel, &QSortFilterProxyModel::modelAboutToBeReset, this, std::bind(&TaskGroupingProxyModel::Private::sourceModelAboutToBeReset, dd));
connect(sourceModel, &QSortFilterProxyModel::modelReset, this, std::bind(&TaskGroupingProxyModel::Private::sourceModelReset, dd));
connect(sourceModel, &QSortFilterProxyModel::dataChanged, this, std::bind(&TaskGroupingProxyModel::Private::sourceDataChanged, dd, _1, _2, _3));
} else {
d->rowMap.clear();
}
endResetModel();
}
TasksModel::GroupMode TaskGroupingProxyModel::groupMode() const
{
return d->groupMode;
}
void TaskGroupingProxyModel::setGroupMode(TasksModel::GroupMode mode)
{
if (d->groupMode != mode) {
d->groupMode = mode;
d->checkGrouping();
Q_EMIT groupModeChanged();
}
}
bool TaskGroupingProxyModel::groupDemandingAttention() const
{
return d->groupDemandingAttention;
}
void TaskGroupingProxyModel::setGroupDemandingAttention(bool group)
{
if (d->groupDemandingAttention != group) {
d->groupDemandingAttention = group;
d->checkGrouping();
Q_EMIT groupDemandingAttentionChanged();
}
}
int TaskGroupingProxyModel::windowTasksThreshold() const
{
return d->windowTasksThreshold;
}
void TaskGroupingProxyModel::setWindowTasksThreshold(int threshold)
{
if (d->windowTasksThreshold != threshold) {
d->windowTasksThreshold = threshold;
d->checkGrouping();
Q_EMIT windowTasksThresholdChanged();
}
}
QStringList TaskGroupingProxyModel::blacklistedAppIds() const
{
return d->blacklistedAppIds.values();
}
void TaskGroupingProxyModel::setBlacklistedAppIds(const QStringList &list)
{
const QSet<QString> &set = QSet<QString>(list.cbegin(), list.cend());
if (d->blacklistedAppIds != set) {
d->blacklistedAppIds = set;
// checkGrouping() will gather and group up what's newly-allowed under the changed
// blacklist.
d->checkGrouping();
// Now break apart what we need to.
for (int i = (d->rowMap.count() - 1); i >= 0; --i) {
if (d->isGroup(i)) {
const QModelIndex &groupRep = index(i, 0);
if (set.contains(groupRep.data(AbstractTasksModel::AppId).toString())) {
d->breakGroupFor(groupRep); // Safe since we're iterating backwards.
}
}
}
Q_EMIT blacklistedAppIdsChanged();
}
}
QStringList TaskGroupingProxyModel::blacklistedLauncherUrls() const
{
return d->blacklistedLauncherUrls.values();
}
void TaskGroupingProxyModel::setBlacklistedLauncherUrls(const QStringList &list)
{
const QSet<QString> &set = QSet<QString>(list.cbegin(), list.cend());
if (d->blacklistedLauncherUrls != set) {
d->blacklistedLauncherUrls = set;
// checkGrouping() will gather and group up what's newly-allowed under the changed
// blacklist.
d->checkGrouping();
// Now break apart what we need to.
for (int i = (d->rowMap.count() - 1); i >= 0; --i) {
if (d->isGroup(i)) {
const QModelIndex &groupRep = index(i, 0);
const QUrl &launcherUrl = groupRep.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
const QString &launcherUrlString = launcherUrl.toString(QUrl::RemoveQuery);
if (set.contains(launcherUrlString)) {
d->breakGroupFor(groupRep); // Safe since we're iterating backwards.
}
}
}
Q_EMIT blacklistedLauncherUrlsChanged();
}
}
void TaskGroupingProxyModel::requestActivate(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestActivate(mapToSource(index));
}
}
void TaskGroupingProxyModel::requestNewInstance(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
d->abstractTasksSourceModel->requestNewInstance(mapToSource(index));
}
void TaskGroupingProxyModel::requestOpenUrls(const QModelIndex &index, const QList<QUrl> &urls)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
d->abstractTasksSourceModel->requestOpenUrls(mapToSource(index), urls);
}
void TaskGroupingProxyModel::requestClose(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestClose(mapToSource(index));
} else {
const int row = index.row();
for (int i = (rowCount(index) - 1); i >= 1; --i) {
const QModelIndex &sourceChild = mapToSource(this->index(i, 0, index));
d->abstractTasksSourceModel->requestClose(sourceChild);
}
d->abstractTasksSourceModel->requestClose(mapToSource(TaskGroupingProxyModel::index(row, 0)));
}
}
void TaskGroupingProxyModel::requestMove(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestMove(mapToSource(index));
}
}
void TaskGroupingProxyModel::requestResize(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestResize(mapToSource(index));
}
}
void TaskGroupingProxyModel::requestToggleMinimized(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(index));
} else {
const bool goalState = !index.data(AbstractTasksModel::IsHidden).toBool();
for (int i = 0; i < rowCount(index); ++i) {
const QModelIndex &child = this->index(i, 0, index);
if (child.data(AbstractTasksModel::IsHidden).toBool() != goalState) {
d->abstractTasksSourceModel->requestToggleMinimized(mapToSource(child));
}
}
}
}
void TaskGroupingProxyModel::requestToggleMaximized(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestToggleMaximized(mapToSource(index));
} else {
const bool goalState = !index.data(AbstractTasksModel::IsMaximized).toBool();
QModelIndexList inStackingOrder;
for (int i = 0; i < rowCount(index); ++i) {
const QModelIndex &child = this->index(i, 0, index);
if (child.data(AbstractTasksModel::IsMaximized).toBool() != goalState) {
inStackingOrder << mapToSource(child);
}
}
std::sort(inStackingOrder.begin(), inStackingOrder.end(), [](const QModelIndex &a, const QModelIndex &b) {
return (a.data(AbstractTasksModel::StackingOrder).toInt() < b.data(AbstractTasksModel::StackingOrder).toInt());
});
for (const QModelIndex &sourceChild : qAsConst(inStackingOrder)) {
d->abstractTasksSourceModel->requestToggleMaximized(sourceChild);
}
}
}
void TaskGroupingProxyModel::requestToggleKeepAbove(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(index));
} else {
const bool goalState = !index.data(AbstractTasksModel::IsKeepAbove).toBool();
for (int i = 0; i < rowCount(index); ++i) {
const QModelIndex &child = this->index(i, 0, index);
if (child.data(AbstractTasksModel::IsKeepAbove).toBool() != goalState) {
d->abstractTasksSourceModel->requestToggleKeepAbove(mapToSource(child));
}
}
}
}
void TaskGroupingProxyModel::requestToggleKeepBelow(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(index));
} else {
const bool goalState = !index.data(AbstractTasksModel::IsKeepBelow).toBool();
for (int i = 0; i < rowCount(index); ++i) {
const QModelIndex &child = this->index(i, 0, index);
if (child.data(AbstractTasksModel::IsKeepBelow).toBool() != goalState) {
d->abstractTasksSourceModel->requestToggleKeepBelow(mapToSource(child));
}
}
}
}
void TaskGroupingProxyModel::requestToggleFullScreen(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(index));
} else {
const bool goalState = !index.data(AbstractTasksModel::IsFullScreen).toBool();
for (int i = 0; i < rowCount(index); ++i) {
const QModelIndex &child = this->index(i, 0, index);
if (child.data(AbstractTasksModel::IsFullScreen).toBool() != goalState) {
d->abstractTasksSourceModel->requestToggleFullScreen(mapToSource(child));
}
}
}
}
void TaskGroupingProxyModel::requestToggleShaded(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestToggleShaded(mapToSource(index));
} else {
const bool goalState = !index.data(AbstractTasksModel::IsShaded).toBool();
for (int i = 0; i < rowCount(index); ++i) {
const QModelIndex &child = this->index(i, 0, index);
if (child.data(AbstractTasksModel::IsShaded).toBool() != goalState) {
d->abstractTasksSourceModel->requestToggleShaded(mapToSource(child));
}
}
}
}
void TaskGroupingProxyModel::requestVirtualDesktops(const QModelIndex &index, const QVariantList &desktops)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestVirtualDesktops(mapToSource(index), desktops);
} else {
QVector<QModelIndex> groupChildren;
const int childCount = rowCount(index);
groupChildren.reserve(childCount);
for (int i = (childCount - 1); i >= 0; --i) {
groupChildren.append(mapToSource(this->index(i, 0, index)));
}
for (const QModelIndex &idx : groupChildren) {
d->abstractTasksSourceModel->requestVirtualDesktops(idx, desktops);
}
}
}
void TaskGroupingProxyModel::requestNewVirtualDesktop(const QModelIndex &index)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestNewVirtualDesktop(mapToSource(index));
} else {
QVector<QModelIndex> groupChildren;
const int childCount = rowCount(index);
groupChildren.reserve(childCount);
for (int i = (childCount - 1); i >= 0; --i) {
groupChildren.append(mapToSource(this->index(i, 0, index)));
}
for (const QModelIndex &idx : groupChildren) {
d->abstractTasksSourceModel->requestNewVirtualDesktop(idx);
}
}
}
void TaskGroupingProxyModel::requestActivities(const QModelIndex &index, const QStringList &activities)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestActivities(mapToSource(index), activities);
} else {
QVector<QModelIndex> groupChildren;
const int childCount = rowCount(index);
groupChildren.reserve(childCount);
for (int i = (childCount - 1); i >= 0; --i) {
groupChildren.append(mapToSource(this->index(i, 0, index)));
}
for (const QModelIndex &idx : groupChildren) {
d->abstractTasksSourceModel->requestActivities(idx, activities);
}
}
}
void TaskGroupingProxyModel::requestPublishDelegateGeometry(const QModelIndex &index, const QRect &geometry, QObject *delegate)
{
if (!d->abstractTasksSourceModel || !index.isValid() || index.model() != this) {
return;
}
if (index.parent().isValid() || !d->isGroup(index.row())) {
d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(index), geometry, delegate);
} else {
for (int i = 0; i < rowCount(index); ++i) {
d->abstractTasksSourceModel->requestPublishDelegateGeometry(mapToSource(this->index(i, 0, index)), geometry, delegate);
}
}
}
void TaskGroupingProxyModel::requestToggleGrouping(const QModelIndex &index)
{
const QString &appId = index.data(AbstractTasksModel::AppId).toString();
const QUrl &launcherUrl = index.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
const QString &launcherUrlString = launcherUrl.toString(QUrl::RemoveQuery);
if (d->blacklistedAppIds.contains(appId) || d->blacklistedLauncherUrls.contains(launcherUrlString)) {
d->blacklistedAppIds.remove(appId);
d->blacklistedLauncherUrls.remove(launcherUrlString);
if (d->groupMode != TasksModel::GroupDisabled) {
d->formGroupFor(index.parent().isValid() ? index.parent() : index);
}
} else {
d->blacklistedAppIds.insert(appId);
d->blacklistedLauncherUrls.insert(launcherUrlString);
if (d->groupMode != TasksModel::GroupDisabled) {
d->breakGroupFor(index.parent().isValid() ? index.parent() : index);
}
}
// Update IsGroupable data role for all relevant top-level items. We don't need to update
// for group members since they've just been inserted -- it's logically impossible to
// toggle grouping _on_ from a group member.
for (int i = 0; i < d->rowMap.count(); ++i) {
if (!d->isGroup(i)) {
const QModelIndex &idx = TaskGroupingProxyModel::index(i, 0);
if (idx.data(AbstractTasksModel::AppId).toString() == appId
|| launcherUrlsMatch(idx.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl(), launcherUrl, IgnoreQueryItems)) {
Q_EMIT dataChanged(idx, idx, QVector<int>{AbstractTasksModel::IsGroupable});
}
}
}
Q_EMIT blacklistedAppIdsChanged();
Q_EMIT blacklistedLauncherUrlsChanged();
}
}
#include "moc_taskgroupingproxymodel.cpp"