diff --git a/libnotificationmanager/notificationgroupingproxymodel.cpp b/libnotificationmanager/notificationgroupingproxymodel.cpp index 5ba4132c2..7960e18f2 100644 --- a/libnotificationmanager/notificationgroupingproxymodel.cpp +++ b/libnotificationmanager/notificationgroupingproxymodel.cpp @@ -1,524 +1,524 @@ /* * Copyright 2016 Eike Hein * Copyright 2018-2019 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "notificationgroupingproxymodel_p.h" #include #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(); return !aName.isEmpty() && aName == bName - && !aDesktopEntry.isEmpty() && aDesktopEntry == bDesktopEntry; + && aDesktopEntry == bDesktopEntry; } 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(); dataChanged(parent, parent); } return true; } } return false; } void NotificationGroupingProxyModel::adjustMap(int anchor, int delta) { for (int i = 0; i < rowMap.count(); ++i) { QVector *sourceRows = rowMap.at(i); QMutableVectorIterator 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{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{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 *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. 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. dataChanged(parent, parent); // Signal children count change for all other items in the group. 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 &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()) { dataChanged(parent, parent, roles); } 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 *>(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 *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) { // For group parent items, DisplayRole is mapped to AppName of the first child. switch (role) { case Notifications::IsGroupRole: return true; case Notifications::GroupChildrenCountRole: return rowCount(proxyIndex); case Notifications::IsInGroupRole: return false; case Notifications::DesktopEntryRole: for (int i = 0; i < rowCount(proxyIndex); ++i) { const QString desktopEntry = index(i, 0, proxyIndex).data(Notifications::DesktopEntryRole).toString(); if (!desktopEntry.isEmpty()) { return desktopEntry; } } return QString(); case Notifications::NotifyRcNameRole: for (int i = 0; i < rowCount(proxyIndex); ++i) { const QString notifyRcName = index(i, 0, proxyIndex).data(Notifications::NotifyRcNameRole).toString(); if (!notifyRcName.isEmpty()) { return notifyRcName; } } 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); }