Brooklyn/plasma/workspace/libnotificationmanager/notificationgroupingproxymodel.cpp
2022-03-05 22:41:29 +05:00

520 lines
17 KiB
C++

/*
SPDX-FileCopyrightText: 2016 Eike Hein <hein@kde.org>
SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "notificationgroupingproxymodel_p.h"
#include <QDateTime>
#include "notifications.h"
using namespace NotificationManager;
NotificationGroupingProxyModel::NotificationGroupingProxyModel(QObject *parent)
: QAbstractProxyModel(parent)
{
}
NotificationGroupingProxyModel::~NotificationGroupingProxyModel() = default;
bool NotificationGroupingProxyModel::appsMatch(const QModelIndex &a, const QModelIndex &b) const
{
const QString aName = a.data(Notifications::ApplicationNameRole).toString();
const QString bName = b.data(Notifications::ApplicationNameRole).toString();
const QString aDesktopEntry = a.data(Notifications::DesktopEntryRole).toString();
const QString bDesktopEntry = b.data(Notifications::DesktopEntryRole).toString();
const QString aOriginName = a.data(Notifications::OriginNameRole).toString();
const QString bOriginName = b.data(Notifications::OriginNameRole).toString();
return !aName.isEmpty() && aName == bName && aDesktopEntry == bDesktopEntry && aOriginName == bOriginName;
}
bool NotificationGroupingProxyModel::isGroup(int row) const
{
if (row < 0 || row >= rowMap.count()) {
return false;
}
return (rowMap.at(row)->count() > 1);
}
bool NotificationGroupingProxyModel::tryToGroup(const QModelIndex &sourceIndex, bool silent)
{
// 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 = sourceModel()->index(rowMap.at(i)->constFirst(), 0);
// Don't match a row with itself.
if (sourceIndex == groupRep) {
continue;
}
if (appsMatch(sourceIndex, groupRep)) {
const QModelIndex parent = index(i, 0);
if (!silent) {
const int newIndex = rowMap.at(i)->count();
if (newIndex == 1) {
beginInsertRows(parent, 0, 1);
} else {
beginInsertRows(parent, newIndex, newIndex);
}
}
rowMap[i]->append(sourceIndex.row());
if (!silent) {
endInsertRows();
Q_EMIT dataChanged(parent, parent);
}
return true;
}
}
return false;
}
void NotificationGroupingProxyModel::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 NotificationGroupingProxyModel::rebuildMap()
{
qDeleteAll(rowMap);
rowMap.clear();
const int rows = sourceModel()->rowCount();
rowMap.reserve(rows);
for (int i = 0; i < rows; ++i) {
rowMap.append(new QVector<int>{i});
}
checkGrouping(true /* silent */);
}
void NotificationGroupingProxyModel::checkGrouping(bool silent)
{
for (int i = (rowMap.count()) - 1; i >= 0; --i) {
if (isGroup(i)) {
continue;
}
// FIXME support skip grouping hint, maybe?
// The new grouping keeps every notification separate, still, so perhaps we don't need to
if (tryToGroup(sourceModel()->index(rowMap.at(i)->constFirst(), 0), silent)) {
beginRemoveRows(QModelIndex(), i, i);
delete rowMap.takeAt(i); // Safe since we're iterating backwards.
endRemoveRows();
}
}
}
void NotificationGroupingProxyModel::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 = mapToSource(index);
for (int i = (rowMap.count() - 1); i >= 0; --i) {
const QModelIndex &sourceIndex = sourceModel()->index(rowMap.at(i)->constFirst(), 0);
if (!appsMatch(sourceTarget, sourceIndex)) {
continue;
}
if (tryToGroup(sourceIndex)) {
beginRemoveRows(QModelIndex(), i, i);
delete rowMap.takeAt(i); // Safe since we're iterating backwards.
endRemoveRows();
}
}
}
void NotificationGroupingProxyModel::setSourceModel(QAbstractItemModel *sourceModel)
{
if (sourceModel == QAbstractProxyModel::sourceModel()) {
return;
}
beginResetModel();
if (QAbstractProxyModel::sourceModel()) {
QAbstractProxyModel::sourceModel()->disconnect(this);
}
QAbstractProxyModel::setSourceModel(sourceModel);
if (sourceModel) {
rebuildMap();
// FIXME move this stuff into separate slot methods
connect(sourceModel, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int start, int end) {
if (parent.isValid()) {
return;
}
adjustMap(start, (end - start) + 1);
for (int i = start; i <= end; ++i) {
if (!tryToGroup(this->sourceModel()->index(i, 0))) {
beginInsertRows(QModelIndex(), rowMap.count(), rowMap.count());
rowMap.append(new QVector<int>{i});
endInsertRows();
}
}
checkGrouping();
});
connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) {
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) {
beginRemoveRows(QModelIndex(), j, j);
delete rowMap.takeAt(j);
endRemoveRows();
// Dissolve group.
} else if (sourceRows->count() == 2) {
const QModelIndex parent = index(j, 0);
beginRemoveRows(parent, 0, 1);
rowMap[j]->remove(mapIndex);
endRemoveRows();
// We're no longer a group parent.
Q_EMIT dataChanged(parent, parent);
// Remove group member.
} else {
const QModelIndex parent = index(j, 0);
beginRemoveRows(parent, mapIndex, mapIndex);
rowMap[j]->remove(mapIndex);
endRemoveRows();
// Various roles of the parent evaluate child data, and the
// child list has changed.
Q_EMIT dataChanged(parent, parent);
// Signal children count change for all other items in the group.
Q_EMIT dataChanged(index(0, 0, parent), index(rowMap.count() - 1, 0, parent), {Notifications::GroupChildrenCountRole});
}
break;
}
}
}
});
connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &parent, int start, int end) {
if (parent.isValid()) {
return;
}
adjustMap(start + 1, -((end - start) + 1));
checkGrouping();
});
connect(sourceModel, &QAbstractItemModel::modelAboutToBeReset, this, &NotificationGroupingProxyModel::beginResetModel);
connect(sourceModel, &QAbstractItemModel::modelReset, this, [this] {
rebuildMap();
endResetModel();
});
connect(sourceModel,
&QAbstractItemModel::dataChanged,
this,
[this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {
for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
const QModelIndex &sourceIndex = this->sourceModel()->index(i, 0);
QModelIndex proxyIndex = 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 dataChanged(parent, parent, roles);
}
Q_EMIT dataChanged(proxyIndex, proxyIndex, roles);
}
});
}
endResetModel();
}
QModelIndex NotificationGroupingProxyModel::index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || column != 0) {
return QModelIndex();
}
if (parent.isValid() && row < rowMap.at(parent.row())->count()) {
return createIndex(row, column, rowMap.at(parent.row()));
}
if (row < rowMap.count()) {
return createIndex(row, column, nullptr);
}
return QModelIndex();
}
QModelIndex NotificationGroupingProxyModel::parent(const QModelIndex &child) const
{
if (child.internalPointer() == nullptr) {
return QModelIndex();
} else {
const int parentRow = 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 NotificationGroupingProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
{
if (!sourceIndex.isValid() || sourceIndex.model() != sourceModel()) {
return QModelIndex();
}
for (int i = 0; i < rowMap.count(); ++i) {
const QVector<int> *sourceRows = 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 (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 NotificationGroupingProxyModel::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() >= rowMap.count()) {
return QModelIndex();
}
return sourceModel()->index(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.
// NOTE we changed that to be last
if (rowMap.isEmpty()) { // FIXME
// How can this happen? (happens when closing a group)
return QModelIndex();
}
return sourceModel()->index(rowMap.at(proxyIndex.row())->constLast(), 0);
}
return QModelIndex();
}
int NotificationGroupingProxyModel::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() >= rowMap.count()) {
return 0;
}
const int rowCount = 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 rowMap.count();
}
bool NotificationGroupingProxyModel::hasChildren(const QModelIndex &parent) const
{
if ((parent.model() && parent.model() != this) || !sourceModel()) {
return false;
}
return rowCount(parent);
}
int NotificationGroupingProxyModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
}
QVariant NotificationGroupingProxyModel::data(const QModelIndex &proxyIndex, int role) const
{
if (!proxyIndex.isValid() || proxyIndex.model() != this || !sourceModel()) {
return QVariant();
}
const QModelIndex &parent = proxyIndex.parent();
const bool isGroup = (!parent.isValid() && this->isGroup(proxyIndex.row()));
// For group parent items, this will map to the last child task.
const QModelIndex &sourceIndex = mapToSource(proxyIndex);
if (!sourceIndex.isValid()) {
return QVariant();
}
if (isGroup) {
switch (role) {
case Notifications::IsGroupRole:
return true;
case Notifications::GroupChildrenCountRole:
return rowCount(proxyIndex);
case Notifications::IsInGroupRole:
return false;
// Combine all notifications into one for some basic grouping
case Notifications::BodyRole: {
QString body;
for (int i = 0; i < rowCount(proxyIndex); ++i) {
const QString stringData = index(i, 0, proxyIndex).data(role).toString();
if (!stringData.isEmpty()) {
if (!body.isEmpty()) {
body.append(QLatin1String("<br>"));
}
body.append(stringData);
}
}
return body;
}
case Notifications::DesktopEntryRole:
case Notifications::NotifyRcNameRole:
case Notifications::OriginNameRole:
for (int i = 0; i < rowCount(proxyIndex); ++i) {
const QString stringData = index(i, 0, proxyIndex).data(role).toString();
if (!stringData.isEmpty()) {
return stringData;
}
}
return QString();
case Notifications::ConfigurableRole: // if there is any configurable child item
for (int i = 0; i < rowCount(proxyIndex); ++i) {
if (index(i, 0, proxyIndex).data(Notifications::ConfigurableRole).toBool()) {
return true;
}
}
return false;
case Notifications::ClosableRole: // if there is any closable child item
for (int i = 0; i < rowCount(proxyIndex); ++i) {
if (index(i, 0, proxyIndex).data(Notifications::ClosableRole).toBool()) {
return true;
}
}
return false;
}
} else {
switch (role) {
case Notifications::IsGroupRole:
return false;
// So a notification knows with how many other items it is in a group
case Notifications::GroupChildrenCountRole:
if (proxyIndex.parent().isValid()) {
return rowCount(proxyIndex.parent());
}
break;
case Notifications::IsInGroupRole:
return parent.isValid();
}
}
return sourceIndex.data(role);
}