/* Copyright (c) 2009 Stephen Kelly This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "krecursivefilterproxymodel.h" #include // Maintainability note: // This class invokes some Q_PRIVATE_SLOTs in QSortFilterProxyModel which are // private API and could be renamed or removed at any time. // If they are renamed, the invocations can be updated with an #if (QT_VERSION(...)) // If they are removed, then layout{AboutToBe}Changed Q_SIGNALS should be used when the source model // gets new rows or has rowsremoved or moved. The Q_PRIVATE_SLOT invocation is an optimization // because layout{AboutToBe}Changed is expensive and causes the entire mapping of the tree in QSFPM // to be cleared, even if only a part of it is dirty. // Stephen Kelly, 30 April 2010. // All this is temporary anyway, the long term solution is support in QSFPM: https://codereview.qt-project.org/151000 class KRecursiveFilterProxyModelPrivate { Q_DECLARE_PUBLIC(KRecursiveFilterProxyModel) KRecursiveFilterProxyModel *q_ptr; public: KRecursiveFilterProxyModelPrivate(KRecursiveFilterProxyModel *model) : q_ptr(model), completeInsert(false) { qRegisterMetaType("QModelIndex"); } inline QMetaMethod findMethod(const char *signature) const { Q_Q(const KRecursiveFilterProxyModel); const int idx = q->metaObject()->indexOfMethod(signature); Q_ASSERT(idx != -1); return q->metaObject()->method(idx); } // Convenience methods for invoking the QSFPM Q_SLOTS. Those slots must be invoked with invokeMethod // because they are Q_PRIVATE_SLOTs inline void invokeDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) { Q_Q(KRecursiveFilterProxyModel); // required for Qt 5.5 and upwards, see commit f96baeb75fc in qtbase static const QMetaMethod m = findMethod("_q_sourceDataChanged(QModelIndex,QModelIndex,QVector)"); bool success = m.invoke(q, Qt::DirectConnection, Q_ARG(QModelIndex, topLeft), Q_ARG(QModelIndex, bottomRight), Q_ARG(QVector, roles)); Q_UNUSED(success); Q_ASSERT(success); } inline void invokeRowsInserted(const QModelIndex &source_parent, int start, int end) { Q_Q(KRecursiveFilterProxyModel); static const QMetaMethod m = findMethod("_q_sourceRowsInserted(QModelIndex,int,int)"); bool success = m.invoke(q, Qt::DirectConnection, Q_ARG(QModelIndex, source_parent), Q_ARG(int, start), Q_ARG(int, end)); Q_UNUSED(success); Q_ASSERT(success); } inline void invokeRowsAboutToBeInserted(const QModelIndex &source_parent, int start, int end) { Q_Q(KRecursiveFilterProxyModel); static const QMetaMethod m = findMethod("_q_sourceRowsAboutToBeInserted(QModelIndex,int,int)"); bool success = m.invoke(q, Qt::DirectConnection, Q_ARG(QModelIndex, source_parent), Q_ARG(int, start), Q_ARG(int, end)); Q_UNUSED(success); Q_ASSERT(success); } inline void invokeRowsRemoved(const QModelIndex &source_parent, int start, int end) { Q_Q(KRecursiveFilterProxyModel); static const QMetaMethod m = findMethod("_q_sourceRowsRemoved(QModelIndex,int,int)"); bool success = m.invoke(q, Qt::DirectConnection, Q_ARG(QModelIndex, source_parent), Q_ARG(int, start), Q_ARG(int, end)); Q_UNUSED(success); Q_ASSERT(success); } inline void invokeRowsAboutToBeRemoved(const QModelIndex &source_parent, int start, int end) { Q_Q(KRecursiveFilterProxyModel); static const QMetaMethod m = findMethod("_q_sourceRowsAboutToBeRemoved(QModelIndex,int,int)"); bool success = m.invoke(q, Qt::DirectConnection, Q_ARG(QModelIndex, source_parent), Q_ARG(int, start), Q_ARG(int, end)); Q_UNUSED(success); Q_ASSERT(success); } void sourceDataChanged(const QModelIndex &source_top_left, const QModelIndex &source_bottom_right, const QVector &roles = QVector()); void sourceRowsAboutToBeInserted(const QModelIndex &source_parent, int start, int end); void sourceRowsInserted(const QModelIndex &source_parent, int start, int end); void sourceRowsAboutToBeRemoved(const QModelIndex &source_parent, int start, int end); void sourceRowsRemoved(const QModelIndex &source_parent, int start, int end); /** Force QSortFilterProxyModel to re-evaluate whether to hide or show index and its parents. */ void refreshAscendantMapping(const QModelIndex &index); QModelIndex lastFilteredOutAscendant(const QModelIndex &index); bool completeInsert; QModelIndex lastHiddenAscendantForInsert; }; void KRecursiveFilterProxyModelPrivate::sourceDataChanged(const QModelIndex &source_top_left, const QModelIndex &source_bottom_right, const QVector &roles) { QModelIndex source_parent = source_top_left.parent(); Q_ASSERT(source_bottom_right.parent() == source_parent); // don't know how to handle different parents in this code... // Tell the world. invokeDataChanged(source_top_left, source_bottom_right, roles); // We can't find out if the change really matters to us or not, for a lack of a dataAboutToBeChanged signal (or a cache). // TODO: add a set of roles that we care for, so we can at least ignore the rest. // Even if we knew the visibility was just toggled, we also can't find out what // was the last filtered out ascendant (on show, like sourceRowsAboutToBeInserted does) // or the last to-be-filtered-out ascendant (on hide, like sourceRowsRemoved does) // So we have to refresh all parents. QModelIndex sourceParent = source_parent; while (sourceParent.isValid()) { invokeDataChanged(sourceParent, sourceParent, roles); sourceParent = sourceParent.parent(); } } QModelIndex KRecursiveFilterProxyModelPrivate::lastFilteredOutAscendant(const QModelIndex &idx) { Q_Q(KRecursiveFilterProxyModel); QModelIndex last = idx; QModelIndex index = idx.parent(); while (index.isValid() && !q->filterAcceptsRow(index.row(), index.parent())) { last = index; index = index.parent(); } return last; } void KRecursiveFilterProxyModelPrivate::sourceRowsAboutToBeInserted(const QModelIndex &source_parent, int start, int end) { Q_Q(KRecursiveFilterProxyModel); if (!source_parent.isValid() || q->filterAcceptsRow(source_parent.row(), source_parent.parent())) { // If the parent is already in the model (directly or indirectly), we can just pass on the signal. invokeRowsAboutToBeInserted(source_parent, start, end); completeInsert = true; } else { // OK, so parent is not in the model. // Maybe the grand parent neither.. Go up until the first one that is. lastHiddenAscendantForInsert = lastFilteredOutAscendant(source_parent); } } void KRecursiveFilterProxyModelPrivate::sourceRowsInserted(const QModelIndex &source_parent, int start, int end) { Q_Q(KRecursiveFilterProxyModel); if (completeInsert) { // If the parent is already in the model, we can just pass on the signal. completeInsert = false; invokeRowsInserted(source_parent, start, end); return; } bool requireRow = false; for (int row = start; row <= end; ++row) { if (q->filterAcceptsRow(row, source_parent)) { requireRow = true; break; } } if (!requireRow) { // The new rows doesn't have any descendants that match the filter. Filter them out. return; } // Make QSFPM realize that lastHiddenAscendantForInsert should be shown now invokeDataChanged(lastHiddenAscendantForInsert, lastHiddenAscendantForInsert); } void KRecursiveFilterProxyModelPrivate::sourceRowsAboutToBeRemoved(const QModelIndex &source_parent, int start, int end) { invokeRowsAboutToBeRemoved(source_parent, start, end); } void KRecursiveFilterProxyModelPrivate::sourceRowsRemoved(const QModelIndex &source_parent, int start, int end) { Q_Q(KRecursiveFilterProxyModel); invokeRowsRemoved(source_parent, start, end); // Find out if removing this visible row means that some ascendant // row can now be hidden. // We go up until we find a row that should still be visible // and then make QSFPM re-evaluate the last one we saw before that, to hide it. QModelIndex toHide; QModelIndex sourceAscendant = source_parent; while (sourceAscendant.isValid()) { if (q->filterAcceptsRow(sourceAscendant.row(), sourceAscendant.parent())) { break; } toHide = sourceAscendant; sourceAscendant = sourceAscendant.parent(); } if (toHide.isValid()) { invokeDataChanged(toHide, toHide); } } KRecursiveFilterProxyModel::KRecursiveFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent), d_ptr(new KRecursiveFilterProxyModelPrivate(this)) { setDynamicSortFilter(true); } KRecursiveFilterProxyModel::~KRecursiveFilterProxyModel() { delete d_ptr; } bool KRecursiveFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { // TODO: Implement some caching so that if one match is found on the first pass, we can return early results // when the subtrees are checked by QSFPM. if (acceptRow(sourceRow, sourceParent)) { return true; } QModelIndex source_index = sourceModel()->index(sourceRow, 0, sourceParent); Q_ASSERT(source_index.isValid()); bool accepted = false; const int numChildren = sourceModel()->rowCount(source_index); for (int row = 0, rows = numChildren; row < rows; ++row) { if (filterAcceptsRow(row, source_index)) { accepted = true; break; } } return accepted; } QModelIndexList KRecursiveFilterProxyModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const { if (role < Qt::UserRole) { return QSortFilterProxyModel::match(start, role, value, hits, flags); } QModelIndexList list; if (!sourceModel()) return list; QModelIndex proxyIndex; Q_FOREACH (const QModelIndex &idx, sourceModel()->match(mapToSource(start), role, value, hits, flags)) { proxyIndex = mapFromSource(idx); if (proxyIndex.isValid()) { list << proxyIndex; } } return list; } bool KRecursiveFilterProxyModel::acceptRow(int sourceRow, const QModelIndex &sourceParent) const { return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); } void KRecursiveFilterProxyModel::setSourceModel(QAbstractItemModel *model) { // Standard disconnect of the previous source model, if present if (sourceModel()) { disconnect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)), this, SLOT(sourceDataChanged(QModelIndex,QModelIndex,QVector))); disconnect(sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(sourceRowsAboutToBeInserted(QModelIndex,int,int))); disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(sourceRowsInserted(QModelIndex,int,int))); disconnect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsAboutToBeRemoved(QModelIndex,int,int))); disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); } QSortFilterProxyModel::setSourceModel(model); // Disconnect in the QSortFilterProxyModel. These methods will be invoked manually // in invokeDataChanged, invokeRowsInserted etc. // // The reason for that is that when the source model adds new rows for example, the new rows // May not match the filter, but maybe their child items do match. // // Source model before insert: // // - A // - B // - - C // - - D // - - - E // - - - F // - - - G // - H // - I // // If the A F and L (which doesn't exist in the source model yet) match the filter // the proxy will be: // // - A // - B // - - D // - - - F // // New rows are inserted in the source model below H: // // - A // - B // - - C // - - D // - - - E // - - - F // - - - G // - H // - - J // - - K // - - - L // - I // // As L matches the filter, it should be part of the KRecursiveFilterProxyModel. // // - A // - B // - - D // - - - F // - H // - - K // - - - L // // when the QSortFilterProxyModel gets a notification about new rows in H, it only checks // J and K to see if they match, ignoring L, and therefore not adding it to the proxy. // To work around that, we make sure that the QSFPM slot which handles that change in // the source model (_q_sourceRowsAboutToBeInserted) does not get called directly. // Instead we connect the sourceModel signal to our own slot in *this (sourceRowsAboutToBeInserted) // Inside that method, the entire new subtree is queried (J, K *and* L) to see if there is a match, // then the relevant Q_SLOTS in QSFPM are invoked. // In the example above, we need to tell the QSFPM that H should be queried again to see if // it matches the filter. It did not before, because L did not exist before. Now it does. That is // achieved by telling the QSFPM that the data changed for H, which causes it to requery this class // to see if H matches the filter (which it now does as L now exists). // That is done in sourceRowsInserted. if (!model) { return; } disconnect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)), this, SLOT(_q_sourceDataChanged(QModelIndex,QModelIndex,QVector))); disconnect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(_q_sourceRowsAboutToBeInserted(QModelIndex,int,int))); disconnect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(_q_sourceRowsInserted(QModelIndex,int,int))); disconnect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(_q_sourceRowsAboutToBeRemoved(QModelIndex,int,int))); disconnect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(_q_sourceRowsRemoved(QModelIndex,int,int))); // Slots for manual invoking of QSortFilterProxyModel methods. connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector)), this, SLOT(sourceDataChanged(QModelIndex,QModelIndex,QVector))); connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(sourceRowsAboutToBeInserted(QModelIndex,int,int))); connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(sourceRowsInserted(QModelIndex,int,int))); connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsAboutToBeRemoved(QModelIndex,int,int))); connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(sourceRowsRemoved(QModelIndex,int,int))); } #include "moc_krecursivefilterproxymodel.cpp"