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.
 
 
 
 
 
 

426 lines
16 KiB

/*
Copyright (c) 2009 Stephen Kelly <steveire@gmail.com>
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 <QMetaMethod>
// 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>("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<int> &roles = QVector<int>())
{
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<int>)");
bool success = m.invoke(q, Qt::DirectConnection,
Q_ARG(QModelIndex, topLeft),
Q_ARG(QModelIndex, bottomRight),
Q_ARG(QVector<int>, 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<int> &roles = QVector<int>());
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<int> &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<int>)),
this, SLOT(sourceDataChanged(QModelIndex,QModelIndex,QVector<int>)));
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<int>)),
this, SLOT(_q_sourceDataChanged(QModelIndex,QModelIndex,QVector<int>)));
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<int>)),
this, SLOT(sourceDataChanged(QModelIndex,QModelIndex,QVector<int>)));
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"